02 JVM垃圾回收机制

1 对象回收判断

1.1 引用计数法

每个对象有个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。

  • 引用计数法优点:实现逻辑简单
  • 引用计数法缺点:无法解决循环引用问题;目前没有在使用
image-20210912202423590.png

1.2 可达性分析算法

从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到表示可以回收

GC root对象:

  1. 虚拟机栈中引用的对象

虚拟机栈中的引用的对象可以作为GC Root。我们程序在虚拟机的栈中执行,每次函数调用都是一次入栈。在栈中包括局部变量表和操作数栈,局部变量表中的变量可能为引用类型(reference),他们引用的对象即可作为GC Root。不过随着函数调用结束出栈,这些引用便会消失。

  1. 方法区中类静态属性引用的对象

我们在类中使用的static声明的引用类型字段

Class Dog {
    private static Object tail;
} 
  1. 方法区中常量引用的对象

在类中使用final声明的引用类型字段

Class Dog {
    private final Object tail;
} 
  1. 本地方法栈中引用的对象

native本地方法引用的对象

1.3 四种引用:强、软、弱和虚引用

  1. 强引用
  • 只有所有GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  1. 软引用
  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
  • 可以配合引用队列来释放软引用自身
  1. 弱引用
  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
  • 可以配合引用队列来释放弱引用自身
  1. 虚引用
  • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
  1. 终结器引用
  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次GC 时才能回收被引用对象
image-20210912205058143.png

2 垃圾回收算法

2.1 标记清除(Mark Sweep)

分为标记清除两阶段:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。

  • 速度较快
  • 会造成内存碎片,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。
image-20210912205341390.png

2.2 标记整理(Mark Compact)

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  • 速度慢
  • 没有内存碎片
image-20210912205445180.png

2.3 复制(Copy)

将可用内存容量划分为大小相等的两块,每次只用其中一块。当这块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。自带整理功能,这样不会产生大量不连续的内存空间,适合新生代垃圾回收。

  • 不会有内存碎片,但效率也不是很高
  • 需要占用双倍内存空间

注解:初看标记整理和复制算法没有什么区别,但是实现的复杂度还是有差别的, 尤其是在需要多线程并行的支持上, "整理"这种"原地"复制会带来很大的复杂度,这种复杂度肯定会带来开销,远没有复制到新内存区简单高效。

3 分代垃圾回收

当前商业虚拟机的垃圾收集都采用分代收集。将上述三种算法整合,不同的区域使用不同的垃圾回收算法。

根据各个年代的特点采取最适当的收集算法:

  1. 在新生代中,每次垃圾收集时候都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
  2. 老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
image-20210912210316306.png

分配流程:

  1. 对象首先分配在伊甸园区域。
  2. 新生代空间不足时,触发minor gc,伊甸园 和 from存活的对象使用 copy 复制到 to中,存活的对象年龄加1 并且交换 from to区域的对象。minor gc 会引发stop the word,暂停其它用户线程,等垃圾回收结束,用户线程才恢复运行。
  3. 当对象寿命超过阈值时,会晋升至老年代,最大寿命15 (4bit)。
  4. 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长。

4 垃圾回收器

4.1 串行垃圾回收器

Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一选择。

  • 单线程
  • 垃圾回收发生的时候,其它线程都暂停
  • 适用于堆内存较小的时候
  • 适合个人电脑
-XX:+UseSerialGC = Serial + SerialOld  // -XX:+UseSerialGC   添加该参数来显示的使用串行垃圾收集器
image-20210912211201627.png

4.2 吞吐量优先垃圾回收器

  • 多线程
  • 堆内存较大,多核CPU
  • 让单位时间内,STW的时间最短 0.2 0.2 = 0.4
-XX:+UseParallelGC    ~    -XX:+UseParallelOldGC // JDK1.8默认开启,只要开启UseParallelGC,就对应开启
-XX:+UseAdaptiveSizePolicy  // 自适应动态调整伊甸园和幸存区的内存比例
-XX:GCTimeRatio=ratio       // 目标1:1 / (1 + ratio)  一般设置ratio为19,20分钟垃圾回收不超过1分钟;会动态调整堆空间大小适应
-XX:MaxGCPauseMillis=ms     // 目标2:最大暂停用户线程时间,默认200ms
-XX:ParallelGCThreads=n     // 垃圾回收线程数
                            // 垃圾回收时,CPU会飚得很高 
image-20210912211602082.png

4.3 响应时间优先垃圾回收器

  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让单次STW的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
-XX:+UseConcMarkSweepGC   ~    -XX:+UseParNewGC     ~     SerialOld
-XX:ParallelGCThreads=n   ~    -XX:ConcGCThread=threads // ParallelGCThreads为4,则ConcGCThread应该是ParallelGCThreads的1/4,对CPU占用没有Par那么高
-XX:CMSInitiatingOccupancyFraction=percent // 执行CMS执行占比,预留空间给浮动垃圾
-XX:+CMSScavengeBeforeRemark   // 在CMS垃圾标记前开启新生代垃圾回收,这样重新标记对象要少得多,Full GC时间从接近2秒,降低到300ms左右
//CMS致命问题:CMS会产生内存碎片,如果内存碎片过多,垃圾回收会退化到SerialOld单线程垃圾回收器

初始标记:仅仅单线程标记GC Roots的直接关联对象,并且STW,这个过程非常短暂,可以忽略不计;

并发标记:使用GC Roots Tracing算法,进行跟踪标记RC Roots间接相关的对象,不会STW;

重新标记:因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以需要重新标记;

清除垃圾:与用户线程并行执行垃圾回收,使用清除算法

CMS缺点:因为与用户工作程一起并发执行,所以会边清理,一边会产生新的垃圾

image-20210912212254321.png

4.4 G1 垃圾回收器

Garbage First,优先回收最有价值的垃圾区域。2004 论文发布;2009 JDK 6u14体验;2012 JDK 7u4官方支持;2017 JDK 9默认,同时废弃了CMS垃圾回收。

G1垃圾回收器,使用标记-整理算法,可以避免CMS标记-清除算法产生的内存碎片问题;在两个Region区域之间,则是使用复制算法。JDK8没有默认G1垃圾回收器,需要手动开启G1

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region (区域)
  • 整体上是标记 + 整理算法,两个区域之间是复制算法
-XX:+UseG1GC
-XX:G1HeapRegionSize=size   // 设置Region区域大小
-XX:MaxGCPauseMillis=time   // 设置暂停目标,默认是200ms

4.4.1 G1垃圾回收阶段

  • Young Collection
  • Young Collection + Concurrent Mark
  • Mixed Collection

4.4.2 Young Collection阶段 (会STW)

  • 如果伊甸园进行垃圾回收,则会将伊甸园区存活的对象使用复制算法到Survivor区
image-20210913193628752.png
image-20210913193650165.png
  • 当Survivor进行垃圾回收时,对象年龄超过15次,放入老年代;年龄不足15次放入另一个Survivor区域
image-20210913194453584.png

4.4.3 Young Collection + CM(新生代回收 + CM)

  • 在Young GC 时会 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
image-20210913195021110.png

4.4.4 Mixed Collection(混合回收)

会对E、S、O进行全面垃圾回收

  • 最终标记(Remark)会STW
  • 拷贝存活(Evacuation)会STW ,并不是所有老年代区域都会回收,而是回收最有价值
-XX:MaxGCPauseMillis=ms //GC最大的暂停时间
image-20210913195240176.png

4.4.5 Young Collection 跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题

如果遍历整个老年代根对象,显然效率会非常低;老年代设计对应一个卡表,每个卡512K,如果某个卡中的对象引用了对象,我们将此卡标记为脏卡,减少扫描范围,提升垃圾回收效率。

image-20210913195532222.png

4.4.6 Remark 重标记

在对象引用改变之前,采用写屏障,表示未处理完毕;同时将对象存入一个引用队列进行处理

5 垃圾回收调优

调优原则:让长时间存活对象尽快晋升,如果长时间存活对象大量停留在新生代,新生代采用复制算法,复制来复制去,性能较低而且是个负担

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

5.2 确定调优目标

科学运算,追求高吞吐量;互联网项目追求低延迟;高吞吐量垃圾回收,目前没有太多选择就一个ParallelGC;

低延迟垃圾回收,可以选CMS,G1, ZGC。目前互联公司还是很多在用CMS,JDK9 默认G1,不推荐CMS;因为CMS采用标记-清除算法会产生内存碎片,内存碎片多了之后会退化为serialOld,产生大幅度、长时间停顿,给用户的体验是不稳定

5.3 调优考虑方面

  1. 数据是不是太多
  • resultSet = statement.executeQuery("select * from 大表") ,可以加限定条数 limit n
  1. 数据表示是否太臃肿
  • 对象图
  • java对象最小也是16字节,Integer 16字节, int 4;所以我们在选则数据类型时尽量选用基本数据类型
  1. 是否存在内存泄漏
  • 比如定义了一个静态的Map,static Map map ,然后不停地向里面添加数据
  • 在内存紧张时,可以使用软引用
  • 在内存不足时,可以使用弱引用
  • 缓存数据时,尽量使用第三方缓存实现,比如redis/memcache,减少对堆内存依赖

5.4 新生代调优

  • 新生代的特点
    • 所有的new 操作的内存分配非常廉价
      • TLAB thread-local allocation buffer,线程局部缓冲区,线程使用自己私有区域分配对象内存
    • 死亡对象的回收代价是零;因为采用复制算法,存活的对象使用复制算法到Survivor区域,剩下都是需要被回收的
    • 大部分对象用过即死,只有少数对象存活
    • Minor GC 的时间远远低于Full GC
    • 新生代优化空间更大一些
  • 新生代还是需要调大一些,因为新生代采用复制算法,需要移动对象,复制算法性能效率较低。新生代能容纳所有【并发量 * (请求 - 响应)】的数据
    • 幸存区大到能保留【当前活跃对象 + 需要晋升对象】,原则就是让真正需要进入老年代的对象才进入老年代。
    • 晋升阈值配置得当,让长时间存活对象尽快晋升

5.5 老年代调优

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC 那么老年代已经足够大了;如果有Full GC 则先尝试调优新生代
  • 观察发生Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容