1

Java 垃圾回收(GC)知识点整理

基础篇

1、GC判断什么是垃圾
  • 引用计数器:根据引用次数计算,存在循环引用的问题(A引用B,但A、B都没有其他对象引用,两者都是垃圾)。
  • GC Root:可达性分析,从根向下搜索标记, 一般从栈帧的局部变量表开始,寻找他们的引用对象,再从引用对象找其他的变量;常见Root:线程栈变量、JNI指针、常量池、方法区静态变量。
2、垃圾回收算法
  • Mark-Sweep:标记清除,分为标记和清除两个阶段,先标记,再清除。不能整理内存空间,存在碎片化内存空间,分配大内存可能触发新一轮垃圾回收。
  • Coping:复制算法,内存分为两个大小相等的块,每次用其中一块,一块用完将存活对象移动到另一块并清除当前块。优点:解决内存碎片问题,只需要移动堆指针,简单高效;缺点:内存缩小一半;注:研究表明98%对象朝生夕死,因此使用大的Eden区+两块小的S区,将Eden存活对象放进其中一块S区,<u>当S区空间不够,由老年代内存进行分配担保</u>。
  • Mark-Compact:标记整理(带压缩),让存活对象移动,解决内存碎片问题,但效率略低。
3、三色标记算法

并发情况下一边工作一边标记;把对象逻辑上分成三种类型(黑、白、灰),从GC Root上开始标记。

  • 白色:还未标记的对象,即要被回收的垃圾。
  • 灰色:标记了对象,确认不是垃圾,但还未标记其成员变量。
  • 黑色:标记了对象,确认不是垃圾,成员变量也标记完成。

漏标问题:对于漏标的对象,即白色对象从灰色对象中断开引用,此时黑色对象引用了白色对象,此时从黑色对象开始标记,发现黑色不用往下标记,但白色对象虽然被黑色对象引用了,却不能变成灰色,此时进行回收,造成异常。

由于并发阶段的存在,那就有可能在并行运行期间之前的标记过的对象的引用关系可能被改变,就会出现白对象漏标的情况,这种情况发生的前提是:把一个白对象的引用存到黑对象的字段里,如果这个情况发生,因为标记为黑色的对象认为是扫描完成的,不会再对它进行扫描。某个白对象失去了所有能从灰对象到达它的引用路径。对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误。

CMS:三色标记用于并发标记阶段。对于漏标问题,CMS采用:Increamental Update,即将黑色标记变成灰色(黑+白=灰),但并发阶段,这种变色方式的标记会出现被其他线程修改的问题,导致了异常,这也是在1.9后CMS被废弃原因。

G1:三色标记称为STAB,Snapshot At Begining 初始化快照,通过对初始化标记放在栈中存储,被再次引用的对象从栈中拿出,剩余的被清理,因此不存在此类问题

ZGC:采用颜色指针,其类指针不再压缩是8字节即8*8=64位指针,18位空+4位颜色标记+42位对象 地址=64,因此2^42=4T,ZGC可以管理4T空间。

并发标记漏标问题处理方案:

  • CMS:写屏障+增量更新
  • G1:写屏障+SATB
  • ZGC:读屏障

注:写屏障、读屏障就是类似AOP一样,写屏障是对赋值操作进行屏障切面,在这个切面加一些处理。

4、对象进入老年代
  • 大对象直接进入老年代,

    JVM参数设置:-XX:PretenureSizeThreshold=10000000(默认1M) -XX:+UseSerialGC
  • 一次minorGC之后,幸存的对象survivor区存放不了。
  • 经历过15次minorGC之后还存活(可设置进入老年代年龄)。
  • 老年代内存担保分配机制:每次minorGc之前JVM会计算老年代剩余可用空间,若其小于年轻代所有对象之和。

    查看是否有设置参数: -XX:HandlePromotionFailure (JDK1.8默认设置)。如设置了,看老年代可用空间大小是否大于历次minorGC之后进入老年底的对象的平均大小,如小于则直接触发FullGC。

    对象年龄动态判断:当前放对象的Survivor区域里,一批相同年龄段的对象的总大小大于这块空间的50%,那么大于等于这批对象年龄最大值的对象,可以直接进入老年代。这样是希望那些可能是长期存活的对象尽早进入老年代。对象动态年龄判断机制一般是在minor GC之后触发。

5、方法区中类的回收
  • 所有实例对象都被回收,那就没有指针指向类。
  • 类加载器classLoader被回收。
  • 该类的所有的java.lang.Class对象没有在任何不地方被引用,也就是没有在任何地方被反射调用。无法在任何地方通过反射调用类的方法。

总结:实例全被回收、类加载器被回收、没有任何地方引用(包括反射)。可以回收,但不是一定回收。

6、其他知识点
1、finalize方法和两次标记

​ 第一次标记并进行一次筛选:筛选的条件是此对象是否有必要进行finalize()方法;如没有覆盖finalize(),则对象直接被回收。

​ 第二次标记:如果覆盖了finalize(),则查看能否与引用链上的变量建立联系(把自己赋值给某个类变量或者对象的成员变量)。

2、MinorGC和FullGC

​ MinorGC停顿时间远小于FullGC,是因为新生代存活的对象一般较少,标记时间较短;而老年代大多数对象是存活的,标记时间较长。

垃圾回收器篇

1、概览

​ 垃圾回收器发展:从分代带不分代,从支持小内存到大内存。

垃圾回收器关系图

垃圾回收器常见搭配:

年轻代年老代备注
SerialSerial Old不常用,淘汰了;一般支持几百M
ParNewCMS一般支持几十G
PSPO1.8默认;一般支持几个G
2、Serial和Serial Old

Serial 串行收集器:单线程、STW(stop-the-world)、新生代是复制算法

Serial Old:Serial 的老年代版本、标记-整理算法

缺点:单线程、STW。

优点:简单高效。

适用场景:适用于Client模式下的虚拟机。

3、Parallel Scavenge、Parallel Old、ParNew
Parallel Scavenge

​ 新生代,复制算法,是Serial的并行(多线程)版本,高吞吐量可以高效利用CPU时间,尽快完成程序运算任务,适合后台任务。

Parallel Scavenge与CMS比较
  • CMS:尽可能缩短垃圾收集时用户停顿时间。
  • Parallel Scavenge:可控制吞吐量,吞吐优先;

    吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集器时间);

    例:总运行时间100min,其中垃圾回收1分钟,则吞吐量=99%;停顿时间越短,越适合用户交互程序;

    参数:

    -XX:MaxGCPauseMillis=<N> 尽可能保证内存时间不超过设定值(越小次数越多),最大暂停时间,单位毫秒。
    -XX:GCTimeRatio=<N> 吞吐量倒数,垃圾回收占总时间比率(公式:1/(N+1),例如N=19 则1/20=5%,即5%时间用于垃圾回收,默认N=99,即1%)。
    -XX:+UseAdaptiveSizePolicy 开启后不需要手工指定新生代大小比例等参数,自适应调节策略。

    暂停时间和吞吐量是相互矛盾的,不可兼容。

Parallel Old

​ Parallel Scavenge的老年代版本,使用多线程标记-整理算法。

ParNew

​ 只用于新生代,和PS一样,都是多线程的,不同的是ParNew是为了配合CMS使用而增强的PS,也会STW。Serial的多线程版本,其余行为和Serial一致,适用Server模式下虚拟机,只有Serial和ParNew可以和CMS配合使用;-XX:ParllerGCThreads配置线程数

Ps:在注重吞吐量以及CPU资源敏感场合,可优先使用PS+PO。

4、CMS

​ CMS:Concurrent Mark Sweep,获取最短回收停顿时间为目标的收集器,适用B/S服务端,注重服务响应速度,希望停顿时间短。

​ CMS缺陷:参考三色标记算法,原因是并发标记的问题。

注:CMS垃圾回收器没有在任何一个版本JDK上设置为默认GC,在1.9后更是完全移除,原因是设计有缺陷。

CMS回收过程
  • 初始标记:标记垃圾,从根GC Root开始,Stop The World(STW),因为不用清理,所以停顿短暂(单线程)。
  • 并发标记:跟随垃圾对象,进行跟踪,部分垃圾转为不是垃圾,最耗时,但此阶段与程序并发执行。
  • 重新标记:再次标记,STW(多线程)。
  • 并发清理:程序执行时并发清理。
CMS缺点
  • 并发标记过程中会新生成一部分垃圾,CMS不能对新生成的浮动垃圾进行回收,浮动垃圾会在下次GC时候进行回收。

    例如:初始标记A、B、C,并发阶段产生浮动垃圾D,此时C对象被引用不再是垃圾。重新标记A、B,并发清理A、B,浮动垃圾D下次GC再清理。

    浮动垃圾并不是影响CMS被抛弃的主要原因。另外在大量垃圾时,CMS回收采用的居然是Serial做串行垃圾回收(只能搭配串行收集器)导致在非常大内存需要做垃圾回收时出现GC时间巨长的问题。

  • CPU资源敏感度高,用户线程会和GC线程抢占CPU资源。
  • 执行过程中的不确定性:在执行过程中,可能存在上一次垃圾回收还没有执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发整理阶段,一边回收、系统一边运行,也许没有回收完就再次触发FullGC。也就是“concurrent mode failure”,此时会STW,当CMS产生concurrent mode failure就会进入备用方案,此时由Serial Old(单线程收集模式)垃圾收集器代替CMS,因此这是CMS搭配Serial Old的原因。
CMS参数
  • -XX:+UseConcMarkSweepGC 老年代启用CMS
  • –XX:ParallelGCThreads=n 并发回收线程数量:(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / n
  • -XX:+UseCMSCompactAtFullCollection FullGC后做压缩整理
  • -XX:+CMSFullGCBeforeCompaction 多少次FullGC后压缩一次,默认是0,每次都会压缩
  • -XX:CMSInitiatingOccupancyFraction 当堆满之后,并行收集器便开始进行垃圾收集,默认是92,即百分比。
5、G1(Garbage First
G1简介

​ 分区回收(不分代),内存分成一块一块区域称为Region,复制算法,优先清理垃圾区,有STW,有FullGC,为每个区标记不同的年代,有Old、survivor、Eden、humongous(大对象)

​ G1将堆划分为多个大小相等的独立区域(Region),JVM最多分配2048个Region。一般region大小等于堆大小除以2048,比如堆大小为4096M,则region大小为2M,可用参数-XX:G1HeapRegionSize指定大小。在运行中JVM会不停的给年轻代增加更多的Region,但最大是60%。年轻代中Eden和survive的比例是8:1:1。

​ G1保留了年轻代和老年代的概念,但是不再是物理隔阂了。默认年轻代对堆内存的占比是5%,可通过参数 -XX:G1NewSizePercent调整。

​ G1对大对象的处理与其他的不同,直接放入Humongous区。G1中大对象的判定准则是超过一个Region的50%。对于一个超过单个region大小的对象会放入连续的region区域。大对象放入H区,是为了节省老年代空间。

​ 在G1中,不论是年轻代还是老年代,回收算法主要使用复制算法,将一个region中的存活对象复制到另外一个region,不像CMS那样回收完之后还要整理内存碎片(单独的region大小2M,不算内存碎片)。

​ G1可预测的停顿,能够建立预测模型,在指定时间内消耗在垃圾回收上的时间(近乎实时的垃圾收集器特征)。

​ 每个区域都有一个通过remember set,把相关引用信息记录到remember set,使用remember set避免全局扫描。

G1垃圾回收器

G1回收过程
  • 初始标记:会STW,标记GCRoots直接引用的对象。
  • 并发标记:和CMS并发标记一样。
  • 最终标记:和CMS的重新标记一样。
  • 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(-XX:MaxGCPauseMillis)来制定回收计划(默认200毫秒)。会优先回收耗时较小的region区域的垃圾。
G1回收方式

G1三种回收方式:youngGC、fullGC、mixedGC

  • youngGC:假设eden区已经堆满,需要出发GC,默认一次GC时间200Ms,但是预计算得知回收eden去只需要10MS,这时G1会放弃回收,而是增加eden区域的数目,直到一次youngGC所需时间接近于指定时间,才会出发youngGC。
  • mixedGC:老年代region的堆占有率达到指定标准时出发,回收所有的eden区和部分old去以及H区,采用复制算法。
  • FullGC:mixed过程中,如没有足够的空的region支撑复制算法时触发,fullGC中会STW(采用标记清除算法),正如CMS中并行收集失败会串行收集。
G1参数
  • -XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器。
  • -XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同。
  • -XX:G1HeapRegionSize=n 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb。
  • -XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比。值为 0 则表示一直执行GC循环。默认值为 45。
  • -XX:G1NewSizePercent 年轻代占比,默认年轻代对堆内存的占比是5%。
  • -XX:MaxGCPauseMillis 根据用户所期望的GC停顿时间,来制定回收计划(默认200毫秒)。
使用场景
  • 50%以上的堆被存活对象占用。
  • 对象分配和晋升速度变化非常大。
  • 垃圾回收时间长。
  • 8GB以上堆内存。
  • 停顿时间在500ms以内。
6、ZGC

ZGC:采用颜色指针,其类指针不再压缩是8字节即8*8=64位指针,18位空+4位颜色标记+42位对象 地址=64,因此2^42=4T,ZGC可以管理4T空间。只支持64位操作系统。复制并压缩方式,使用了读屏障,指针跳跃。

(待补充,注:美团面试被问及过ZGC)

调优案例篇

调优总结

1、年轻代大小选择

  • 响应时间优先:尽可能大,此时年轻代发生手机频率小,同时减少到达老年代的对象
  • 吞吐量优先:尽可能大,垃圾回收可以设置为并行

2、年老代选择

  • 响应时间优先:考虑并发会话率和会话持续时间,过小则频率过高,过大则回收时间过长
  • 吞吐量优先:很大的年轻代,较小的年老代,减少中期对象存活。

3、其他参数优化

  • 偏向锁延时。
  • 设置进入老年代年龄。
  • 防止同一批对象超过存活区50%,避免同一批对象进入老年代。
  • 关闭class verify。
调优工具

artahs:开源调优工具,阿里出品,可参考Github,调优可说使用过此产品

  • -help 查看参数。
  • dashboard 仪表盘。
  • jad class 可用于反编译,用于查看程序版本。
  • jvm 类型jinfo功能。
  • redefine class 动态替换class文件,热部署。
  • thread 类型jstack thread -b 可以查看死锁。
JVM参数
  • jps -l -m 查看运行的java程序PID
  • jstat -gc <pid> 查看gc信息(jstat -gc <id> 1000 20 每秒执行一次,共执行20次)
  • jmap -dump:format=b,file=D:xxx.txt <pid> 堆快照转储(要设置崩溃时自动生成)
  • jmap -heap <pid> 显示java堆详情,如垃圾回收器种类,参数配置,分代情况等
  • jhat //用于分析dump生成的文件
  • jstack -l <pid> 显示线程的相关信息,比如死锁问题排查

案例1:亿级流量的电商网站(ParNew + CMS)

背景:

  • 正常每秒50个订单;促销期间每秒1000单;以每秒1000单计算,分布式三台系统(内存8G),每台300单。
  • 假设每个订单对象为1KB,300KB/s对象生成。
  • 考虑到库存、优惠、积分等计算,放大20倍;即300*20KB/s。这些对象1s后都是垃圾。

8G服务器,分配4GB给JVM,参数配置如下:

-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8
计算出:堆-老年代:1.5G        堆-伊甸区:1.2G        堆-S0:150M        堆-S1:150M        方法区:256M

计算:

​ 按照60MB每秒的对象生成速度,20s后Eden区满,触发MinorGC,按照对象回收95%计算,估计到S0区对象为100M,这100M对象同龄,总和大于S0区50%;根据动态对象年龄判断原则,这100M对象会被挪到老年代,到达老年代后会变成垃圾。

总结:

​ 系统业务对象生命周期短,不该进入老年代,survivor区太小导致对象进入老年代,同时老年代内存空间无需这么大,应该把对象尽量留在新生代。

优化:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8
主要调整了-Xmn年轻代参数
堆-老年代:1G    堆-伊甸区:1.6G        堆-S0:200M    堆-S1:200M    方法区:256M    栈:1M

优化后:

​ 25s后Eden区满,触发MinorGC,估计到S0区对象为100M,不足50%,不会进入老年代,会经历多次MinorGC。

优化思路:

​ 让短期存活的对象尽量留在survivor区,不要进老年代,从而减少FullGC。

继续优化:

​ 关于对象年龄什么时候进入老年代:本例中MinorGC触发时间在25s,大多数对象存活周期在几秒内变成垃圾,因此可以将年龄降低,从默认的15->5;长对象存活时间在2分钟左右,移动到老年代减少survivor区的占用。

关于对象多大进入老年代:预估大对象大小,一般1M认为很大了。

优化后参数:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5 -XX:PertenureSizeThreshold=1M -xx:+UseParNewGC -XX:+UseConcMarkSweepGC

​ 使用了ParNew+CMS,计算FullGC大概时间,根据进入老年代数量,预估价每隔半个小时到一个小时才会触发一次,老年代空间分配担保机制在此处几乎不会存在,因此不大可能出现担保失败造成的FullGC。

综上:

​ 只要年轻代设置合理,老年代几乎可以使用默认值。最终结果如下:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5 -XX:PertenureSizeThreshold=1M -xx:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection -XX:+CMSFullGCBeforeCompaction=0

猫的名字
13 声望3 粉丝

大龄(28)Java程序员,分享一下面试经验和个人整理的资料。