一、JVM 类加载器
1. 类加载过程
- 加载:将class文件读到内存
- 验证:验证文件格式是否正确
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用,将一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄,这是所谓的静态链接过程
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
- 使用
- 销毁
2. 双亲委派机制
- BootstrapClassLoader:引导类加载器
- 负责加载支撑JVM运行的位于JRE的lib目录下的核心类库
- ExtClassLoader:扩展类加载器
- 负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
- AppClassLoader:应用程序类加载器
- 负责加载ClassPath路径下的类包,主要是加载我们自己写的那些类
- 自定义类加载器
- 负责加载用户自定义路径下的类包
具体流程:
- 首先,检查指定名称的类是否加载过,如果加载过了就不用加载,直接返回
- 如果没有加载过,那么判断一下是否有父类加载器,如果有父类加载器,则由父类加载器加载(即调用parent.loadClass(name, false)),或者调用bootstrap类加载器来加载
- 如果父类加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成加载
3. 为什么要使用双亲委派机制?
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样可以防止核心API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
二、JVM整体结构及内存模型
1.JVM 运行时数据区
- 线程共享:
- 堆:对象实例
- 方法区:常量、静态变量、类信息
- 线程私有:
- 虚拟机栈:栈帧 -> 局部变量表、操作数栈、动态链接、方法出口
- 本地方法栈:通过JNI调用JVM底层native方法
- 程序计数器:存储下一条指令的地址
2. JVM内存参数
- -Xms:初始堆内存大小(物理内存的1/64)
- -Xmx:最大堆内存大小(物理内存的1/4)
- -Xmn:新生代内存大小
- -Xss:栈内存空间大小,设置的越小,说明一个栈里分配的栈帧就越少,但是对于JVM来说能开启的线程数就越多
- -XX:MaxMetaspaceSize:元空间最大值,默认是1
- -XX:MetaspaceSize:元空间触发full gc的初始阈值,以字节为单位,默认21M,收集器会对该值进行调整,如果释放了大量的空间,就适当降低该值,如果释放了很少空间,那么在不超过MaxMetaspaceSize的情况下,适当提高该值
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常是由于永久代或元空间发生了大小调整,基于这个情况,一般将一般情况下,将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并且设置的比初始值要大,对于8G的物理机来说,一般会将这两个初始值设置为256M
3. 对象的创建流程
- 类加载检查
- 分配内存
- 初始化
- 设置对象头
- 执行init方法
3.1 类加载检查
当虚拟机遇到new指令时,首先去检查指令的参数是否能在常量池中定位到一个类的引用符号,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,那么必须先执行类的加载过程
3.2 分配内存
在类加载检查通过后,将为新对象分配内存,等同于把一块确定大小的内存从java堆中划分出来
存在两个问题:
- 如何划分内存?
- 指针碰撞(默认):如果java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离
- 空闲列表:如果java堆中的内存不是规整的,那么就没办法进行指针碰撞了,虚拟机就必须维护一个表,记录哪些内存是可用的,在分配的时候从列表中找出一块足够大的空间分配给对象实例,并更新表中记录
- 在并发情况下,可能出现正在给对象A分配存储,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况?
- CAS:采用CAS加上失败重试的方式保证操作的原子性来对分配内存的动作进行同步处理
- 本地线程分配缓冲(TLAB):把内存分配的动作按照线程划分在不同的空间中进行,及每个线程在java堆中预先分配一小块内存,通过-XX:+/-UseTALB参数来设定虚拟机是否使用TLAB(JVM默认会开启-XX:+UseTLAB),,-XX:TLABSize指定TLAB大小
3.3 初始化
内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值,保证了对象实例字段在java代码中可以不赋初值就直接使用
3.4 设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息,这些信息放在对象头Object Header中
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域
- 对象头
- Mark Word标记字段:对象自身运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
- Klass pointer 类型指针:对象指向它的类元素指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 实例数据
- 对齐填充
3.5 执行init方法
为属性赋值以及执行构造方法
4. 指针压缩
- JDK1.6 update 14 开始,在64位操作系统中,JVM支持指针压缩
- 在64位平台的Hotspot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大带宽,同时GC也会承受较大压力
- 为了减少64位平台下的内存消耗,启用指针压缩
- 堆内存小于4G时,不需要使用指针压缩,JVM会直接去除32位地址,即使用低虚拟地址空间
- 堆内存大于32G时,指针压缩会失效,会强制使用64位来对java对象寻址,就会出现1的问题,所以堆内存不要大于32G为好
- 指针压缩默认开启-XX:+UseCompressedOops,禁止指针压缩-XX:-UseCompressedOops
5. JVM逃逸分析
JVM通过逃逸分析确定该对象不会被外部访问,如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,减轻了垃圾回收的压力
- 对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,可能被外部方法所引用,例如作为调用参数传递到其他地方中
- 开启逃逸分析参数:-XX:+DoEscapeAnalysis,来优化对象内存分配位置,使其通过标量替换优先在栈上进行分配,JDK7之后默认开启
- 标量替换:通过逃逸分析确定对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所替代,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配,开启标量替换参数:-XX:+EliminateAllocations,JDK7之后默认开启
6. 对象在Eden区分配
大多数情况下,对象在新生代中的Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Young GC
- minor GC/ Young GC:指新生代的垃圾收集操作,Young GC 非常频繁,回收速度一般也比较快
- Major GC/Full GC:一般回收老年代、新生代、方法区的垃圾,Full GC 速度一般比Young GC 慢10倍以上
Eden 区 与 Survivor 区默认8:1:1
大量对象分配在Eden区,Eden区满了以后会触发Young GC,可能99%的对象都成为垃圾对象被回收掉,剩余存活的对象就会挪到空的那块Survivor区,下次Eden区满了之后又触发Young GC,把Eden区和Survivor区的对象进行回收,把剩余存活的对象挪到另一外空的Survivor区,因为新生代都是朝生夕死的,存活的时间短,所以JVM默认8:1:1的比例是很合适的,让Eden区尽量大,Survivor区够用即可
JVM默认开启-XX:+UseAdaptiveSizePolicy,会导致这个8:1:1比例自动变化,如果不想这个比例有变化,可以设置参数-XX:-UseAdaptiveSizePolicy
7. 什么对象会进入老年代
- 大对象直接进入老年代:
- 大对象就是需要大量存储空间的对象,如字符串,数组。
- JVM参数-XX:PretenureSizeThreshould可以设置大对象的大小,如果对象超过了这个大小直接进入老年代,这个参数只在Serial和ParNew两个收集器下有效
- 如设置JVM参数:-XX:PretenureSizeThreshould=1000000(字节),-XX:+UseSerialGC
- 长期存活的对象进入老年代
- 如果对象在Eden出生,并经过第一次Young GC后仍然存活,并且能够被Survivor区容纳的话,它被移动到Survivor空间中,并将对象年龄设置为1,对象在Survivor中每经过一次Young GC,它的年龄就增加1,当他的年龄增加到一定的程度的时候,就会晋升到老年代
- 可以通过设置参数:-XX:MaxTenuringThreshould来设置年龄阈值,默认15岁,CMS收集器默认6岁,不同收集器略微不同
- 对象动态年龄判断
- 当一批对象总大小大于当前放置对象的Survivor区大小的50%(可指定该大小)的时候,这些大于等于这批对象年龄最大值的对象,就可以直接进入老年代
- 例如Survivor区有一批对象,年龄1+年龄2+年龄n的对象总大小超过Survivor区域的50%,此时就会把年龄n及以上的对象放入老年代,一般在Young GC后触发该判断机制
- 可以通过设置参数:-XXTargetSurvivorRatio指定
- 老年代空间分配担保机制
- 年轻代每次Young GC 都会计算老年代剩余可用空间
- 如果这个可用空间小于年轻代里所有对象大小之和(包括垃圾对象)
- 就会看参数:-XX:-HandlePromotionFailure的参数是否设置了,JDK1.8默认已经设置
- 如果有这个参数,就会看老年代的可用内存大小,是否大于之前每次Young GC后进入老年代对象的平均大小
- 如果上一步的结果小于或者参数没有设置,那么就会触发一次Full GC,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够的空间存放新对象就会发生OOM
- 如果Young GC之后剩余存活的需要挪动到老年代的对象大于老年代可用空间,那么以后触发Full GC,Full GC完成之后还是没有空间放存活的对象,则也会发生OOM
8. 对象内存回收
8.1 如何确定垃圾对象
- 引用计数法
- 给对象添加一个引用计数器,每当有一个地方引用它,计数器+1,当引用失效,计算器-1,任何计数器为0的对象就是不可能再被使用的
- 优势:效率高,实现简单
- 缺点:很难解决互相循环引用问题
- 可达性分析算法(默认)
- 将GC Roots 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
- **GC Roots根节点:**线程栈的本地变量,静态变量,本地方法栈变量等
8.2 常见引用类型
- 强引用:普通变量的引用
- 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况下不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用回收掉
- 弱引用:将对象用WeakReferences软引用类型包裹,GC会直接回收掉,很少使用
- 虚引用:最弱的引用关系,几乎不用
8.3 finalize()方法最终判定对象是否存活
- 标记前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链
- 第一次标记并进行一次筛选
- 筛选条件是有没有必要执行finalize()方法
- 当对象没有覆盖finalize()方法,对象直接被回收
- 第二次标记
- 如果对象覆盖了finalize方法,如果该对象重新与引用链上的任何一个对象关联,那么在第二次标记时,它将移出即将回收的集合,如果对象没有逃脱,就会被回收
- 一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize()方法自我救命的机会只有一次
8.4 如何判断一个类是无用的类
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
三、垃圾收集算法
1.分代收集理论
当前虚拟机的垃圾收集都采用分代收集算法,一般是将java堆分为新生代和老年代,然后根据各个年代的特点选择合适的垃圾收集算法
- 在新生代中,每次收集都会有大量对象死去,所以选择复制算法,只需要付出少量对象的复制成本,就可以完成每次垃圾收集
- 在老年代中,对象存货的几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择标记-清除或标记-整理算法进行垃圾收集
- 注意:标记-清除和标记-整理速度要比复制算法慢10倍左右
2. 垃圾收集算法
2.1 标记-复制算法
将内存分为大小相同的两块,每次使用其中的一块,将这一块内存使用完后,就将还存活的对象复制到另外一块去,然后再把使用的空间一次清理掉,这样对每次的内存回收都是对内存区间的一半进行回收
2.2 标记-清除算法
算法分为“标记”和“清除”阶段,标记存活的对象,统一回收所有未被标记的对象,也可以反过来
会产生两个明显问题
- 效率问题:如果需要标记的对象太多,效率不高
- 空间问题:标记清除后会产生大量不连续的碎片
2.3 标记-整理算法
根据老年代的特点特出的一种标记算法,标记的过程与标记-清除算法一样,但后续不是直接进行回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
3. 垃圾收集器
- 年轻代:Serial、ParNew、Parallel
- 老年代:CMS、Serial Old、Parallel Old
- G1,ZGC
3.1 Serial收集器(-XX:+UseSerialGC(年轻代), -XX:+UseSerialOldGC(老年代))
串行收集器,是最基本、历史最悠久的垃圾收集器,单线程,只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(Stop The World),直到它收集结束
- 新生代采用复制算法,老年代采用标记整理算法
- 简单高效,但是STW给用户带来不良用户体验
Serial Old 收集器是Serial收集器的老年代版本,同样是单线程收集器,它主要有两大用途:
- 在jdk1.5及之前版本中与Parallel收集器搭配使用
- 作为CMS收集器的后备方案
3.2 Parallel 收集器(-XX:+UseParallelGC(年轻代),-XX:UseParallelOldGC(老年代))
Parallel收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其行为(控制参数、收集算法、回收策略等等)和Serial类似,默认收集线程数跟CPU核数相同,可以使用-XXParallelGCThreads指定收集的线程数,但一般不推荐修改
Parallel收集器关注点是吞吐量(高效率的利用CPU),CMS等收集器的关注点更多的是用户线程的停顿时间(提高用户体验),所谓的吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗的比值
- 新生代采用复制算法,老年代采用标记-整理算法
Parallel Old收集器是Parallel收集器的老年代版本,使用多线程和标记-整理算法,在注重吞吐量以及CPU资源的场合,都可以优先考虑Parallel收集器和Parallel Old收集器
JDK8默认的新生代和老年代收集器
3.3 ParNew收集器(-XX:UseParNewGC)
ParNew收集器其实和Parallel收集器类似,区别主要在于它可以和CMS收集器配合使用,
新生代采用复制算法,老年代采用标记-整理算法
3.4 CMS收集器(-XX:+UseConcMarkSweepGC(old))
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,非常注重用户体验,它是Hotspot虚拟机第一款真正意义上的并发收集器,它第一次实现了基本上让垃圾收集线程与用户线程同时工作
CMS收集器是一种标记-清除算法实现的,运作过程主要分为四个步骤:
- 初始标记:暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快
- 并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整合对象图的过程,这个过程耗时很长,但是不需要停顿用户线程,可以与垃圾收集器一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运作二导致的标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记
- 并发清理:开启用户线程,,同时GC线程开始对未标记的区域做清扫,这个阶段如果有新增对象会被标记为黑色不做任何处理
- 并发重置:重置本次GC过程中的标记数据
- 优点:
- 并发收集,低停顿
- 缺点:
- 对CPU资源敏感(会和CPU抢资源)
- 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下次GC再清理了)
- 它使用回收算法标记-清除 算法会导致收集结束时会有大量的空间碎片产生,可以通过-XX:+UseCMSCompactAFullCollection可以让JVM在执行完标记清除后在做整理
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发Full GC,也就是 Concurrent mode failure,此时会进入stop the world,用Serial Old垃圾收集器来回收
- CMS的相关核心参数
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:Full GC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次Full GC后压缩一次,默认是0,表示每次Full GC之后都压缩一次
- -XX:+CMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次Young GC,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段
- -XX:+CMSParallelInitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW
3.5 G1收集器(-XX:+UseG1GC)
G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
- G1将java堆分为多个大小相等的独立区域(Region),JVM最多可以有2048个region
- 一般region大小等于堆大小除以2048,如堆大小为4096M,则region大小为2M,当然也可以用参数-XX:G1HeapRegionSize手动指定Region大小,但是推荐默认计算方式
- G1保留了年轻代和老年代的概念,但不再是物理隔阂了,他们都是可以不连续的Region集合
- 默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200M左右的内存,对应大概100个Region,可以通过-XX:G1NewSizePercent设置新生代占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过-XX:G1MaxNewSizePercent调整,年轻代中的Eden和Survivor对应的Region也跟之前一样,默认8:1:1
- 一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后又会变成老年代,也就是说Region的区域功能可能会动态变化
- G1垃圾收集器对于对象什么时候移动到老年代跟之前一样,唯一不同的是大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代,在G1中大对象的判断规则就是一个大对象超过了一个Region的50%,比如,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous区,而且一个大对象如果太大,可能会横跨多个Region来存放
- Humongous区专门存放短期的巨型对象,不用直接进入老年代,可以节约老年代空间,避免老年代空间不够的GC开销
- Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收
- G1收集器一次GC的运作过程大致分为以下几个步骤
- 初始标记:暂停所有其他线程,并记录下GC Roots直接能引用的对象,速度很快
- 并发标记:同CMS的并发标记
- 最终标记:同CMS的最终标记
- 筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只停顿200ms,那么就只会回收800个Region(Collection Set, 要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间使用户控制的,而且停顿用户线程将大幅提高收集效率,不管是年轻代还是老年代,回收算法主要用的是复制算法,将一个Region中存活的对象复制到另外一个Region中,这种不会像CMS那样回收完因为有很多碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片
- G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region,比如一个Region花200ms能回收10M垃圾,另一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1会有限选择后面的的Region回收,这种使用Region划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限时间内尽可能高的收集效率
- G1收集器参数设置
- -XX:+UseG1GC:使用G1收集器
- -XX:ParallelGCThreads:指定GC工作的线程数量
- -XX:G1HeapRegionSize:指定分区大小(1-32MB,且必须是2的n次幂),默认将整堆划分为2048个分区
- -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
- -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
- -XX:G1MaxNewSizePercent:新生代内存的最大空间
- -XX:TargetSurvivorRatio:Survivor区的填充容量,默认50%,Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过Survivor区的50%,此时把年龄n及以上的对象放入老年代
- -XX:MaxTenuringThreshold:最大年龄阈值,默认15
- -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到堆内存的阈值(默认45%),则执行新生代和老年代的混合收集(Mixed GC),比如默认堆有2048个Region,如果有接近1000个Region都是老年代的Region,则可能就要Mixed GC
- -XX:G1MixedGCLiveThresholdPercent:默认85%,Region中的存活对象低于这个值时才会回收Region,如果超过这个值,存活对象过多,回收的意义不大
- -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选,默认8次,在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单词停顿时间过长
- -XX:G1HeapWastePercent:默认5%,GC过程中空出来的Region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程中就会不断空出新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收结束
- G1垃圾收集器优化建议
- 假设参数 -XX:MaxGCPauseMills设置的值很大,导致系统运行很久,年轻代可能占用了堆内存的60%,此时才触发年轻代GC
- 那么存活下来的对象就可能会很多,此时就会导致Survivor区域放不下那么多对象,就会进入老年代
- 或者是你年轻代GC过后,存活下来的对象过多,导致进入Survivor区后触发动态年龄判断规则,达到Survivor区域的50%,也会快速导致对象进入老年代中
- 所以核心还是在调节 XX:MaxGCPauseMills这个参数的值,在保证他的年轻代GC别太频繁的同时,还得考虑每次GC过后存活的对象有多少,避免存活的对象太多快速进入老年代,频繁触发mixed gc
- 什么场景适合使用G1?
- 50%以上的堆被存活对象引用
- 对象分配和晋升的速度变化非常大
- 垃圾回收的时间特别长,超过1s
- 8G以上的堆内存
- 停顿时间是500ms以内
3.6 ZGC收集器(-XX:+UseZGC)
ZGC是一款JDK11中新加入的具有实验性质的低延迟垃圾收集器
- ZGC的目标有4个:
- 支持TB级别的堆
- 最大GC停顿时间不超过10ms
- 奠定未来GC特性基础
- 最糟糕的情况下吞吐量会降低15%
- ZGC内存布局
ZGC是一款基于Region的内存布局,暂时不分代,使用了读屏障、颜色指针等技术实现了可并发标记-整理算法的,以低延迟为首要目标的一款垃圾收集器
- ZGC的Region可以有大、中、小三类容量
- 小型Region:容量固定为2M,用于放置小于256KB的对象
- 中型Region:容量固定为32M,用于放置大于等于256KB并小于4M的对象
- 大型Region:容量不固定,可以动态变化,但必须为2M的正整数倍,用于放置4M或以上的大对象,每个大型Region中只会存放一个大对象,这也预示着虽然名字叫做大型Region,最小容量可低至4M,大型Region在ZGC中是不会被重新分配,因为复制一个大对象代价非常高昂
- ZGC的运作过程
- 并发标记:与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记和最终标记也会出现短暂停顿,与G1不同的是,ZGC的标记是在指针上,而不是在对象上进行的,标记阶段会更新软色指针中的Marked 0,Marked 1标志位
- 并发预备重分配:这个阶段需要根据特定的查询条件统计出本次收集过程要清理哪些Region,将这些Region组成重分配集,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本去省去G1中记忆集的维护成本
- 并发重分配:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每一个Region维护一个转发表,记录从旧对象到新对象的转向关系,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为自愈能力
- 并发重映射:重映射所作的就是修正整个堆中执向重放你配集中旧对象的所有引用,但是ZGC中对象引用存在自愈功能,所以这个重映射操作并不是很迫切,ZGC很巧妙的把并发重映射阶段要做的工作,合并到下一次垃圾收集的并发标记阶段去完成,反正他们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销,一旦所有的指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了
- ZGC存在的问题
- ZGC最大的问题就是浮动垃圾,ZGC停顿的时间是在10ms以内,但是ZGC的执行时间还是远大于这个时间。
- 例如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量新的对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次回收的对象就是浮动垃圾
- **解决方案:**目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是治标不治本的方案,如果要从根本上解决问题,还是要引入分代收集,让新生对象在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集
- ZGC触发时机
- 定时触发:默认为不使用,可通过ZCollectionInterval参数配置
- 预热触发:最多三次,在堆内存达到10%,20%,30%时触发,主要统计GC时间,为其他GC机制使用
- 分配速率:基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC
- 主动触发:默认开启,可通过ZProactive参数配置,距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的时间跟(49 * 一次GC持续的最大时间),超过则触发
4. 三色标记
在并发标记过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生,因此引入三色标记,即把GC Roots可达性分析遍历对象过程中遇到的对象,按照是否访问过这个条件记录成以下三种颜色:
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过,黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一遍,黑色对象不可能不经过灰色对象指向某个白色对象
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有扫描过,
- 白色:表示对象尚未被垃圾收集器访问过,显然在可达性分析刚刚开始阶段,所有对象都是白色的,在分析结束阶段,任然是白色的对象,即代表不可达
- 多标-浮动垃圾
- 在并发标记过程中,如果由于方法运行结束导致局部变量GC Root被销毁,这个GC Root引用的对象之前又被扫描过,那么本轮GC不会回收这部分内存,这部分本应该回收但没有回收的内存,被称之为浮动垃圾,浮动垃圾并不会影响垃圾回收的正确性,只是需要在下一轮垃圾回收中才会清除
- 另外,针对并发标记、清理标记开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进入垃圾回收,这部分对象期间可能会变成垃圾,这也是浮动垃圾的一部分
- 漏标-读写屏障
- 漏标会导致被引用的对象当成垃圾误删除,这是严重的BUG,必须解决,有两种解决方案:
- 增量更新(Incremental Update):当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次,可以理解为:黑色对象一旦新插入了指向白色对象的引用后,他就变成灰色对象了
- 原始快照(SATB):当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫到白色对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮的GC清理中存活下来,待下一轮GC的时候重新扫描,这个对象也有可能是浮动垃圾),以上无论是对引用关系记录的插入还是删除,虚拟机的记录都是通过写屏障来实现的
- 漏标会导致被引用的对象当成垃圾误删除,这是严重的BUG,必须解决,有两种解决方案:
- 对于读写屏障,以Java Hotspot为例,其并发标记时对漏标的处理如下
- CMS:写屏障+增量更新
- G1,Shenandoah:写屏障 + SATB
- ZGC:读屏障
- 为什么G1用SATB?CMS使用增量更新?
- SATB相对增量更新效率会高(当然可能会造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是做简单标记,等到下一轮GC,再深度扫描
5. 如何选择垃圾收集器
- 优先调整堆的大小让服务器自己选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
- 如果允许停顿时间超过1s,选择并行或者JVM自己选择
- 如果响应时间最重要,并且不能超过1s,选择并发收集器
- 4G以下可以用Parallel,4-8G可以使用ParNew + CMS,8G以上用G1,几百G以上用ZGC
6. 安全点与安全域
- 安全点:指代码中一些特定的位置,当线程运行到这些位置的时候,它的状态时确定的,这样JVM就可以安全的进行一些操作,如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发,安全点的位置主要有以下几种
- 方法返回之前
- 调用某个方法后
- 抛出异常的位置
- 循环的末尾
大致就是当垃圾收集器需要中断线程的时候,不是直接对线程进行操作,仅仅是简单设置一个标志位,各个线程执行过程时,会不停的主动去轮询这个标志,一旦发现中断标志为真时,就自己在最近的安全点上主动中断挂起。轮询的地方和安全点是重合的
- 安全区域:Safe Point 是对正在执行的线程设定的,如果一个线程处于Sleep或者中断状态,它就不能响应JVM的中断请求,再运行到Safe Point上,因此JVM引入了Safe Region,Safe Region是指在一段代码中,引用关系不会发生变化,在这个区域内的任何地方开始GC都是安全的
四、JVM调用命令
1.JPS
通过JPS命令查看程序进程id
2.JMAP
2.1 查看内存信息,实例个数以及占用内存大小,如:
jmap -histo 14660
jmap -histo 14660 > ./log 输出为log.txt文档
打开log.txt文件,内容如下
- num:序号
- instances:实例数量
- bytes:占用空间大小
- class name:类名称
- [C -> char[]
- [S -> short[]
- [I -> int[]
- [B -> byte[]
- [[I -> int[][]
2.2 查看堆信息
jmap -heap [进程ID]
2.3 堆内存dump
jmap -dump:format=b,file=eureka.hprof [进程ID]
示例
‐Xms10M ‐Xmx10M ‐XX:+PrintGCDetails ‐XX:+HeapDumpOnOutOfMemoryError ‐XX:HeapDumpPath=D:\jvm.dump
- 可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./路径
- 可以用jvisualvm命令工具导入该dump文件分析
3. Jstack
3.1 查找死锁
Jstack [进程ID]
- Thread-1:线程名
- prio:优先级5
- tid:线程Id
- nid:线程对应本地线程标识nid
- java.lang.Thread.State:BLOCKED 线程状态
还可以使用Jvisualvm自动检测死锁
3.2 找出CPU占用最高的线程堆栈信息
- 使用命令 top -p ,显示java进程的内存情况,pid是java进程号
- 按H,获取每个线程的内存情况
- 找到内存和CPU占用最高的线程PID,如19664
- 转为十六进制得到0x4cd0
- 执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中4cd0这个线程所在行的后10行,从堆栈中可以发现导致CPU飙高的调用方法
- 查看对应的堆栈信息找出可能存在问题的代码
4. Jinfo
查看正在运行的java应用程序扩展参数
4.1 查看JVM参数
jinfo -flags [进程ID]
4.2 查看java系统参数
jinfo -sysprops [进程ID]
5. Jstat
Jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令格式如下:
// JDK1.8为例
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
5.1 垃圾回收统计
Jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况
- S0C:第一个幸存区的大小,单位KB
- S1C:第二个幸存区大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区使用大小
- EC:伊甸园区大小
- EU:伊甸园区的使用大小
- OC老年代的大小:
- OU:老年代的使用大小
- MC:方法区的大小(元空间)
- MU:方法区的使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间,单位s
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间,单位s
- GCT:垃圾回收消耗总时间,单位s
5.2 堆内存统计
jstat -gccapacity [进程ID]
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- EC:伊甸园区的大小
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:老年代大小
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类的空间大小
- YGC:年轻代GC次数
- FGC:老年代GC次数
5.3 新生代垃圾回收统计
Jstat -gcnew [进程ID]
- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- TT:对象在新生代存活的次数
- MTT:对象在新生代存活的最大次数
- DSS:期望的幸存区大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗的时间
5.4 新生代内存统计
jstat -gcnewcapacity [进程ID]
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0CMX:最大幸存1区大小
- S0C:当前幸存1区大小
- S1CMX:最大幸存2区大小
- S1C:当前幸存2区大小
- ECMX:最大伊甸园区大小
- EC:当前伊甸园区大小
- YGC:年轻代垃圾回收次数
- FGC:老年代回收次数
5.5 老年代垃圾回收统计
jstat -gcold [进程ID]
- MC:方法区大小
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- OC:老年代大小
- OU:老年代使用大小
- YGC:新生代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
5.6 老年代内存统计
jstat -gcoldcapacity [进程ID]
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:老年代大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收总消耗时间
5.7元数据空间统计
jstat -gcmetacapacity [进程ID]
6. Arthas工具
- 执行java -jar arthas-boot.jar,选择要进入的进程序号,进入arthas
- 输入dashboard可以查看进程的运行情况,线程,内存,GC,运行环境等信息
- 输入thread可以查看线程的详细信息
- 输入thread加上线程ID可以查看线程堆栈
- 输入thread -b可以查看线程死锁
- 输入jad 加类的全名,可以反编译,这样方面我们可以查看线上的代码是否是正确的的版本
- 使用ognl命令可以查看线上系统变量值,甚至可以修改变量值
7.Full GC 比 Young GC还多的原因有哪些
- 元空间不够导致多余的Full GC
- 显示调用System.gc()造成多余的Full GC,这种在线上一般通过 -XX:+DisableExplicitGC禁用,如果加上了这个JVM参数,那么代码中调用System.gc()没有任何效果
- 老年代空间分配担保机制
8. GC日志
- 可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析GC日志得到关键性指标,分析GC原因,调优JVM参数
- 打印GC日志方法,在JVM中添加参数,%t代表时间
‐Xloggc:./gc‐%t.log ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M
java ‐jar ‐Xloggc:./gc‐%t.log ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M microservice‐eureka‐server.jar