JVM进阶
字节码
字节码为编译后的class文件,比如java、scala这些语言都是可编译成字节码的,字节码借助jvm就可以在任何平台运行,可以理解成跨平台的实现
一、运行时数据区
在程序运行时,由jvm提供的几块内存区域,分别为以下几个区域:
- 本地方法栈:执行native关键字的方法栈
- java方法栈:执行类中方法的栈,每执行一个方法叫压栈、执行完就会出栈
- 程序计数器:线程上下切换时,记录当前执行线程,以及下次要执行的线程地址
- 堆:存储jvm中所产生的所有的对象
- 方法区:包含了类中的所有信息,比如静态变量、成员变量、类信息、常量池等等
- 执行引擎
- 解释器:执行内存中每个字节码class文件中的指令
- 垃圾回收器:处理jvm在内存中产生的对象
- JIT编译器:将热点的字节码文件翻译成机器指令,且会缓存此指令
- 执行引擎
堆
-
-Xms: ms(memory start), 指定堆的初始化内存大小,等价于-- -XX:initialHeapSize
-
-Xmx:mx(memory max),指定堆的最大内存大小,等价于-XX:MaxHeapSize
-
一般会根据-Xms和-Xmx设置为一样,这样JVM就不需要再GC之后去修改内存大小了,提高了效率,默认情况下,初始化内存大小 = 物理内存大小 / 64, 最大内存大小 = 物理内存大小 / 4
-
新生代为刚刚产生的对象、老年代为经过很多次GC还未被清除的对象
-
通过-XX:NewRatio参数来配置新生代和老年代的比例分配,默认为2,表示新生代占用1,老年代占用2,也就是新生代占堆区总大小的1/3
-
新生代
在默认情况下(Eden区: S0区: S1区)的比例关系为(8:1:1), 也就是Eden区占新生代大小的8 / 10, 可以通过-XX: SurvivorRatio调整具体大小比例
- Eden区:新对象进来都会先放到此区域(除非对象的大小都超过了Eden区的最大大小,那么就只能进入老年代)
- Surviror0: 也称为fr- - om区
- Surviror0: 也称为to区,都是用来存放MinorGC(YGC)收集后的对象
GC介绍
- Young GC / Minor GC: 负责对新生代进行垃圾回收
- Old GC / Major GC: 负责对老年代进行垃圾回收,目前只有CMS垃圾收集器会单独对老年代进行垃圾收集, 其他垃圾收集器基本都是整堆回收的时候对老年代进行垃圾回收
- Full GC: 整堆回收,也会对方法区进行垃圾回收
可达性分析法
所谓的GC ROOTS就是对象被引用的一个过程中的线路,比如A对象引用B对象,在GC ROOTS中的展示就是A -> B 以树形结构展示, 那它具体是包含了哪些引用呢?
- 线程中虚拟机栈中正在执行的方法中的方法参数,局部变量所对应的对象引用
- 线程中本地方法栈中正在执行的方法中的方法参数、局部变量所对应的对象引用
- 方法区中保存的类信息中的静态变量属性所对应的对象引用
- 方法区中保存的类信息中的常量池属性所对应的对象引用
什> 么时候触发垃圾回收?
在Eden区的大小被对象放满了之后,就会触发MinorGC去收集未被使用的对象,如果还在使用的话,就从Eden区移到S0区、当第二次MinorGC的时候,如果S0区的对象还是没有被回收就会继续转移到S1区,并标记一下被GC了2次,当这样反复的被GC了16次之后,就会被放入到老年代!, 大对象如果Eden区放不下就会直接放在老年代中!
程序计数器
- 是物理寄存器的抽象实现
- 用来记录待执行的下一条指令地址
- 它是程序控制流的指示器,循环、if else、异常处理、线程恢复等都需要依赖它来完成
- 解释器工作时就是通过它来获取下一条需要执行的字节码指令的
- 它是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域
虚拟机栈(Java栈 || Java方法栈)
- 每一个Java方法都属于一个栈帧
- 栈帧有哪些参数呢?
- 局部变量表(很多个Slot)
- 操作数栈(执行字节码过程中用于计算使用)
- 方法返回地址
- 动态链接
- 附加信息
- 栈帧有哪些参数呢?
- 方法的栈帧会根据调用链路依次压栈
- 虚拟机栈是线程私有的
- 方法调用层数过多,可能出现StackOverFlowError
- 可以通过-Xss来设置虚拟机栈大小
二、类加载子系统
加载class文件步骤:比如一个编译后的class文件:Helloword.class会经过如下几个步骤:
- 加载:将字节码文件加载到内存中
- 链接:链接分为三个步骤
- 验证:验证待加载的class文件是否正确,比如验证文件格式
- 准备:为static变量分配内存并赋值
- 解析:将符号引用解析为直接引用(符号引用:class的名称, 直接引用: class最终的内存地址)
- 初始化:将数据初始化到对象中
类加载器的分类
-
引导类加载器(BootStrapClassLoader)
-
BootStrapClassLoader: 加载的是jre/lib下面的jar包中的类
-
ExtClassLoader: 加载的是jre/lib/ext下面的jar包中的类
-
-
自定义类加载器(继承实现ClassLoader类)
- AppClassLoader: 加载的是自定义的类的路径的类(也就是你指定什么类 -> classpath, 就加载什么类)
- WebAppClassLoader
双亲委派机制
双亲委派作用:避免类的重复加载、防止核心api被篡改
从AppClassLoader -> ExtClassLoader -> BootStrapClassLoader
描述:
从AppClassLoader.loadClass()加载一个类的时候,它首先不是拿自己本身的ClassLoader去加载,而是使用ExtClassLoader去加载,当ExtClassLoader加载不了的时候,就再次向上找BootStrapClassLoader去加载此类,如果BootStrapClassLoader都加载不了这个类,那么最终还是由AppClassLoader 加载此类。当类.class.getClassLoader() == null就表示此类是被BootStrapClassLoader而加载的类
Tomcat为什么要自定义类加载器?
- 一个tomcat可以跑多个应用,而多个引用可能会出现相同path + class的类出现,那么在加载的时候就可能在加载完A应用后,就放弃了B应用class的加载。
- Tomcat就针对这种极端情况,就针对不同的应用使用继承自定义的方式(WebAppClassLoader)去加载,也就是每个应用使用自己的类加载器去加载自己应用的类,这样就不会有冲突情况,两个应用存在相同path + class也不会过滤掉不加载的情况。
- 在JVM中判断一个类是不是被加载的逻辑是: 类名 + 对应的类加载器实例
三、GC算法
3.1、标记清除算法
标记清除算法是常用的垃圾回收算法,针对某块内存空间,比如新生代、老年代,如果可用内存不够了,就会暂停JVM虚拟机(stop the world),暂停用户线程的执行,然后执行算法进行垃圾回收。
具体使用步骤:
1、标记阶段:从GC ROOT开始遍历,找到可回收的对象,并对可回收的对象进行记录。
2、清除阶段:堆内存空间进行线性遍历,如果发现对象头(MARK WORD)中被记录删除了,那么就删除回收它
缺点:
1、出现内存碎片 (也就是整个内存空间,有的被标记了可回收,有的不可回收)
2、效率不高 (因为需要遍历整个内存空间)
3.2、复制算法
复制算法思路:将内存划分为两块,一块属于对象存储区域、另一块属于空闲区域,在进行垃圾回收时,将可达性对象复制到另一个没有被使用的内存块中,然后再清除当前内存块中的所有可回收的对象,每次GC都是按照此方法区执行。(适用于新生代GC)
优点:
1、没有标记和清除阶段,通过GC ROOTS找到可达对象,直接复制,不需要修改对象头,效率高
2、不会出现内存碎片
缺点:
1、占用内存空间较多, 因为要空出一块空余内存来复制删除使用
2、对象复制后,对象存放地址发生变化,需要额外的时间修改栈帧中记录的引用地址
3、如果可达性对象较多,垃圾对象比较少,那么复制算法效率就会比较低,所以垃圾对象多的情况下,复制算法比较适合,因为它是整个区域区域的复制
3.3、标记整理算法
标记整理算法分为三阶段提交:
- 第一阶段:从GC ROOTS找到并标记可达对象
- 第二阶段:将所有存活对象移动到内存的另一端(新的一个内存区域)
- 第三阶段:清理边界外所有的空间
总结GC算法
GC算法对比:
从速度/开销对比 | 标记-清除 | 标记-整理 | 复制 |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(有碎片,也就是内存区域不连续) | 少(无碎片) | 最多 |
移动对象 | 否 | 是 | 是 |
3.4、分代收集算法
不同对象的存活时长不一致,就针对新生代、老年代采用不同的GC算法进行回收
默认几乎所有的垃圾回收器都是采用分代收集算法进行垃圾回收的
-
新生代中对象存活的时间比较短,那么就可以利用复制算法,它适用于垃圾对象比较多的情况下去进行回收,但是会产生一定的碎片
老年代中存活时间比较长,所以不适用复制算法,可以使用标记清除或标记整理算法, 个人建议采用CMS垃圾回收算法,因为相对以下两个算法,标记清除算法比标记整理更为高效点,但是标记整理更为完美点(很慢) -
CMS垃圾回收器采用的就是标记-清除算法
-
Serial Old垃圾回收器采用的就是标记-整理算法
3.5、GC参数设置
-XX:+PrintCommandLineFlags 查看使用的垃圾收集器
-XX:+UseSerialGC 指定使用Serial GC和Serial Old GC
-XX:+UseParNewGC 指定新时代使用ParNew GC
-XX:+UseConcMarkSweepGC 指定老年代使用CMS GC
-XX:+UseParallelGC 指定新生代使用Parallel GC
-XX:+UseParallelOldGC 指定老年代使用Parallel Old GC
GC详情图
详解CMS垃圾回收器
CMS整个垃圾收集过程更长了,但是STW的时间变短了,而且在垃圾回收过程中大部分时间用户线程都还在执行,所以用户不会出现短暂的延迟等,但是吞吐量变低了(单位时间内执行的用户线程更少了)
CMS分为几个阶段:
-
初始标记阶段:
- STW 暂停所有工作线程
- 标记GC ROOTS能直接可达的对象
- 一旦标记完,就恢复工作线程继续执行
-
并发标记阶段:
- 从初始阶段标记出的对象,开始遍历整个老年代,标记出所有的可达对象
- 耗时比较长
- 不需要STW,用户线程与垃圾收集线程一起执行
- 三色标记
-
重新标记阶段:
-
上个阶段标记的对象,可能有误差,需要进行修正
-
需要STW,但是时间也不是很长
-
增量更新
-
根据三色标记,重新标记对象
-
比如在finally中将已定义为垃圾对象变成了可达性对象,重复利用,就需要重新标记
-
-
并发清除阶段:
- 删除垃圾对象
- 由于不需要移动对象,这个阶段可以和用户线程一起执行,不需要STW
CMS总结
如果在并发标记、并发清楚的过程汇总,由于用户线程一直在执行,而工作线程也在同时工作,就可能产生,用户线程正在执行的时候,而工作线程也在回收垃圾对象,那么有新对象进入了老年代,但是空间也不够,那么就会导致“concurrent mode failure”,此时就会利用Serial Old来做一次垃圾收集,就会做一次全局的STW, 在并发清除的过程中,可能产生新的垃圾,这些垃圾统称为”浮动垃圾“,只能等到下一次GC时来处理
由于采用的是标记-清除算法,所以会产生内存碎片,可以通过参数-XX:+UseCMSCompactAtFullCollection可以让JVM在执行完标记-清除后再做一次整理,也可以通过-XX:CMSFullGCsBeforeCompaction来指定多少次GC后来做处理,默认是0,表示每次GC后都会整理。
四、杂七杂八信息
对象创建过程
- 类加载器检查
- 分配内存(指针碰撞、空闲列表等方式)
- 初始化“零值”, 基本类型是默认值0, 引用类型是null
- 设置对象头(类实例、元数据信息、对象的哈希码、GC分代年龄)
- 执行init,此方法是字节码自动生成的,是一个实例构造器
- 对象栈上分配(对象创建后分配到堆的哪个位置,就需要经历逃逸分析最终决定)
对象栈上分配流程
- 对象创建后会被分配到新生代 young gc中进行管理
- 如果young gc也就是eden区放不下,就尝试放在survivor区
- 如果survivor区也放不下,则放入到old区
- 大对象直接放入old区(大对象的定义即超过新生代区域的50%以上)
为什么JVM放弃永久代而选择元空间
- 内存限制:JVM加载的class总数,方法的大小很难确定,因此不好制定其大小
- 降低了OOM:使用的是元空间存放在本地内存(上限较大)中的方式来替换永久代,这样可以降低OOM发生的可能性
- 提升GC的性能:永久代是通过Full GC进行垃圾回收的,也就是和老年代同时实现垃圾回收,替换元空间简化了Full GC的过程,可以在不进行暂停的情况下并发的垃圾进行回收,提升了GC的性能
- JRockit没有永久代:Oracle合并Hotspot和JRockit,而JRockit没有永久代
对象由什么组成?
- 对象头(包含锁状态markword、GC分带年龄、线程持有锁、哈希码等等)
- 实例数据
- 对其填充
一个对象(一般16byte)组成,在排除很多字段的情况下
类加载的理解?
类加载器是JAVA中的一种加载形式,在加载字节码到内存中时会使用双亲委派机制,使用递归的形式不断向尝试去加载父类的加载器,如果父类加载器为null,则调用本地方法,交由启动类加载器加载,所以说ExtClassLoader(加载jre/ext/lib/*.jar)的父类加载器为BootStrapClassLoader(jre/ext/lib/rt.jar)
JDK调优命令
-
jps 列出当前机器正在运行的进程
-
jinfo 查看或修改虚拟机参数
- 栗子:jinfo -flags
-flag <name>
:显示指定名称的 JVM 标志(flag)的值。-flags
:显示正在运行的 Java 进程的所有 JVM 标志的名称和值。-sysprops
:显示正在运行的 Java 进程的系统属性(System Properties)的名称和值。
- 栗子:jinfo -flags
-
jstat 监视虚拟机各种运行状态信息
- 栗子:jstat [option] pid [interval [count]]
-
-class
:显示类装载相关的统计信息,如装载数量、卸载数量、总装载类数等。 -
-gc
:显示垃圾回收相关的统计信息,包括新生代和老年代的情况,如垃圾回收时间、吞吐量、堆大小等。 -
-gccapacity
:显示垃圾回收堆的容量统计信息,包括新生代和老年代的容量、已使用空间、垃圾回收器标记等。 -
-gcutil
:显示垃圾回收相关的统计信息,包括新生代和老年代的使用率、垃圾回收时间等。 -
-compiler
:显示即时编译器相关的统计信息,如编译任务数量、编译耗时等。
- jstack 生成虚拟机当前时刻线程快照
- 栗子:jstack [option] pid
-l
:除了堆栈信息外,显示关于锁的附加信息,包括每个线程等待的锁及锁的拥有者等信息。-F
:当jstack
无法连接到 Java 进程时,强制输出线程堆栈信息。通常在进程卡死或无响应时使用。-m
:输出混合模式,显示 Java 和本地(C/C++)堆栈信息。-h
或--help
:显示帮助信息,列出jstack
的所有选项。
- 栗子:jstack [option] pid
- jmap 生成堆转储快照
- 栗子:jmap [option] pid
-heap
:显示 Java 进程的堆内存使用情况,包括堆大小、已使用内存、垃圾回收器信息等。-histo[:live]
:显示 Java 进程中各个类的实例数量和占用内存大小,如果指定了live
参数,则只统计活动对象。-dump:[live,]format=b,file=<filename>
:生成 Java 进程的内存映像文件,如果指定了live
参数,则只包含活动对象。format
参数指定输出文件的格式,可以是b
(二进制)或hprof
(Hprof 格式)。file
参数指定输出文件的路径和名称。
- 栗子:jmap [option] pid
怎么确定一个对象是否可回收(是否是垃圾)?
- 可达性分析 也就是是否是可达对象,如果可达则不可回收,反之为垃圾对象
- 引用指向对象 是否有引用也就是指针指向这个对象
什么是运行时数据区?
- 方法区
- 方法区是各个线程共享的内存区域,在虚拟机启动时创建
- 方法区描述为堆的一个逻辑部分
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
- 程序计数器
- 需要CPU去调度线程,在这过程中还可能一直切换线程
栈帧结构与动态链接的作用是什么?
- 局部变量表:保存方法的局部变量
- 操作数栈:JVM使用一种基于栈的指令集,在执行运算时,实在操作数栈上执行
- 动态链接:指向运行时常量池的引用
- 方法返回地址:方法返回地址和附加信息
类的生命周期
- 加载:编写.java文件、编译成.class文件
- 连接
- 验证:验证字节码文件是否可执行
- 准备:准备基本数据类型初始化
- 解析:字符引用解析成直接引用
- 初始化:调用类的静态方法/new出来对象的时候就会进行初始化工作
- 使用:使用对象操作
- 卸载:被垃圾回收器回收
对象一定分配在堆上面吗?
不一定的,也有可能分配在栈的,因为JDK有一个优化的手段是通过逃逸分析来减少内存堆分配的压力
什么是逃逸分析?
逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析与外形分析相关联,当变量(或者对象)在方法中分配后,其指针有可能被返回或被全局引用,这样就会被其他方法或者线程所引用,这种现象就称为指针(或者引用)的逃逸。
通俗讲就是一个对象new出来后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸。当方法逃逸了之后,就存储在堆中,反之存储在栈中!, 使用“ -xx: -DoEscapeAnalysis ” 的JVM参数将所有对象都存储在堆中
**
// 当外部调用此方法,并且返回了对象引用此方法就是逃逸,就称为方法逃逸,此对象就存储在堆中
public static User createUser() {
User user = new User();
user.setName("123");
return user;
}
// 当外部调用此方法,未返回了对象引用此方法就是未逃逸,此对象就存储在栈中
public static String createUser() {
User user = new User();
user.setName("123");
return user.toString();
}
public static void createUser() {
User user = new User();
user.setName("123");
}
逃逸分析优点:
- 栈上分配:如果对象分配在栈上,随着方法出栈后,对象随着销毁。
- 同步消除:线程同步本身是相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全的消除掉
- 变量替换:如果是基本类型,不可拆分,那么就称之为标量,把一个JAVA对象拆散,将其用到的成员变量恢复到原始类型来访问,这个过程就称为标量替换,加入逃逸分析能够证明一个对象不会被方法外部访问,那么这个对象可以被拆散,那么可以不创建对象,直接用创建若干个成员变量来代替,可以让对象的成员变量在栈上分配和读写。
频繁出现minor gc怎么做?
优化Minor GC频繁问题:由于新生代空间较小,Eden区很快被填满,就会导致频繁的Minor GC, 因此需要通过调大-Xmn分配的内存来降低Minor GC频率
# 1000 代表1秒钟打印一次
jstat -gc <进程号> 1000
问题排查思路
1、使用top命令获取到进程pid
2、根据printf '0x%x\n' <pid> 获取到对应的十六进制
3、根据jstack命令 <pid> | grep pid的十六进制id -A 20 就可以看到对应的cpu飙高的代码行了
导出还在存活的文件的快照信息。
jmap -dump:live, file=/home/sdc/demo/file.hprof <pid>
标签:进阶,对象,回收,零到,GC,垃圾,JVM,线程,加载
From: https://www.cnblogs.com/curryAhui/p/18174656