条理清晰的JVM垃圾回收入门知识

垃圾定位法

  1. 引用计数法
    1. 该方法通过给对象增加引用计数器,记录当前对象被引用的次数,当引用次数为0时则会被标记为垃圾
    2. 当出现循环引用的情况则无法解决,造成内存泄漏. 即 : A→B, B→A
  2. 可达性分析法
    1. 通过规定的一些GC根节点出发查找,如果一个对象和根节点间没有引用路径可以访问到或者说没有引用链,则该对象则不可达,会被标记为垃圾
    2. 如这种情况, D和C则为垃圾Root→A→B; D→C;
    3. Root节点: 本地方法栈虚拟机栈引用的对象; 常量类静态变量引用的对象;

    引用类型

    1. 强引用 : 如new Object()这种方式实例化出来的都是强引用。当调用它的变量置为NULL或者方法执行完毕,虚拟机栈栈帧弹出则引用消失,会被回收.
    2. 软引用 : 被SoftRefrence类型包裹的对象为软引用对象,当GC时会标记该类对象,在发生内存不足GC时会优先清理这类对象; SoftRefrence不会被回收,但是包裹的对象会被回收.例如: SoftRefrence<List<String>> softObjRefrence = new SoftRefreence<>(new ArrayList<String>());中的List会被回收掉; 可以作为临时缓存使用
    3. 弱引用 : 被弱引用包装的对象被GC扫描到会在当次GC回收. 例如: WeakReference<String> weakObj = new WeakRefrence<>(new String("临时字符"));像线程本地变量的Key就是使用的弱引用包装,有可能会发生内存泄漏的情况.当Key被回收掉则无法访问到Value.但Value还被Map强引用到;
    4. 虚引用 : 无法直接使用虚引用获取对象,但是当虚引用对象被回收时系统会给我们发送通知,我们可以用该机制感知GC时机完成资源释放操作; 无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知
    5. 引用使用场景参考: 软引用、弱引用、虚引用-他们的特点及应用场景你不可不知的Java引用类型之——虚引用强软弱虚引用,只有体会过了,才能记住Reference的与finalize

垃圾回收算法

  1. 标记-清除算法

    1. 两阶段操作 : 第一阶段标记出来垃圾对象,第二阶段回收垃圾对象
    2. 类似于一堵墙被砸开了很多个洞, 该回收方式会产生很多的内存碎片,如果下次分配内存没有足够大的空间分配则会提前触发GC
      Mark-Sweep
  2. 标记-整理(压缩)算法

    1. 三阶段操作 : 第一阶段标记垃圾对象; 第二阶段将存活对象向一端移动; 第三阶段释放存活对象端边界以外的内存
    2. 该方式解决了内存碎片的问题,但是由于需要对存活对象移动,回收效率不高
      Mark-Compact
  3. 复制法

    1. 复制算法主要为了中和内存碎片和内存效率的平衡
    2. 复制算法模型将内存分为两块,每次只使用其中一块,当回收时将存活对象复制到未使用的内存空间上,将已使用的内存空间直接清理掉
    3. 由于要分出部分区域执行复制,所以内存使用率不高
    4. JVM中通过分为Ende和两个Survivor区域.以8:1:1的内存分配提高利用率.每次使用其中一个Survivor区
      Cpoying
  4. 分代收集法

    1. 新生代使用复制法
    2. 老年代使用标记整理算法

垃圾收集器

垃圾收集器在进行垃圾回收时会STW(stop the word),此时所有线程会在安全点阻塞直到回收完毕.新生代收集器大部分采用复制法回收,老年代收集器使用标记-整理算法进行回收
收集器配合关系

安全点

  1. 为什么需要安全点 : 执行垃圾回收时防止在运行过程中导致的漏标错标的情况.所以垃圾收集时需要等所有线程都跑到安全点并中断时才会开始执行
  2. 详细解释 : 深入学习JVM-JVM 安全点和安全区域Java程序执行过程中的 安全点、安全区域

新生代

  1. Serial

    单线程垃圾收集器,垃圾的标记以及删除都会STW.使用复制算法
    Serial

  2. ParNew

    多线程垃圾收集器,垃圾标记和删除都会STW.使用复制算法
    ParNew

  3. Parallel Scavenge(1.8默认)

    1. 吞吐量优先的多线程收集器.可自适应动态调节虚拟机参数,这是和ParNew的重要区别.其主要设置堆内存和最大垃圾收集停顿时间-XX:MaxGCPauseMillis(单次垃圾收集时间)以及吞吐量大小-XX:GCTimeRatio(垃圾收集时间占总运行时间比率)还有自适应调节策略开关-XX:+UseAdaptiveSizePolicy,该开关开启后就无需指定新生代老年代大小、比例等细节参数
    2. 吞吐量是指应用程序线程用时占程序总用时的比例。 例如,吞吐量99/100意味着100秒的程序执行时间应用程序线程运行了99秒, 而在这一时间段内GC线程只运行了1秒
    3. 通过平衡单次GC时间和GC次数来保证GC的吞吐量
    4. 例如: 通过尽可能少运行GC来最大化吞吐量;单个GC需要花更多时间来完成, 从而导致更高的平均和最大暂停时间;频繁地运行GC以便更快速地完成。 这反过来又增加了开销并导致吞吐量下降

老年代

  1. Serial Old

    单线程老年代收集器,垃圾的标记以及删除都会STW.使用标记-整理算法
    Serial Old

  2. Parallel Old(1.8默认)

    多线程老年代收集器,垃圾的标记以及删除都会STW.使用标记-整理算法
    Parallel Old

  3. CMS

    1. 停顿时间最短垃圾收集器实现原理原型(有BUG)
    2. 执行流程分为 : 初始标记并发标记并发预清理(MinioGC,减少重新标记期间对新生代扫描时长)重新标记(会扫描新生代中对象,由于可达性分析法和跨代引用情况)并发清理阶段
    3. 其中初始标记重新标记阶段需要STW.其他阶段不会影响程序运行(重新标记失败后会执行Serial Old方式收集)
    4. 并发清除阶段耗时比较长,所以和用户线程并发执行
    5. 采用标记清除算法,容易造成频繁GC; 会产生浮动垃圾问题;
      CIM

G1

  • 像CMS收集器一样,能与应用程序线程并发执行
  • 整理空闲空间更快,需要GC停顿时间更好预测
  • 不希望牺牲大量的吞吐性能
  • 不需要更大的Java Heap
  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间
  • G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址
  • 参考下图,理解G1的垃圾回收方式 : G1回收从整体来看,是标记整理算法收集器; 但从局部两个Region间则是复制算法实现.所以不会产生内存碎片并且回收速度很快
  • 会直接把存活对象复制到另一个Region,并且清除当前Region

动态年龄阈值

Survivor空间剩余相同年龄对象大小总和大于Surivivor空间一半则大于等于该年龄(取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值)的对象可以直接进入老年代

分配担保机制

  1. 当MinorGC时,survivor空间不足以放下所有剩余对象,则会将对象通过分配担保机制放入老年代,当老年代空间够则会直接进行MinorGC,否则会进行FullGC为担保腾出来空间
  2. 担保机制原理 : 老年代最大剩余连续空间大于新生代大小/历代回收晋升大小平均值则进行MinorGC,否则进行MajorGC(JDK6+)

跨代引用检测

当新生代对象持有老年代对象引用或者老年代对象持有新生代对象引用的情况成为跨代引用
垃圾标记时不考虑该种情况则会误删有引用的对象

老年代GC时

CMS在Remark阶段前增加可中断的并发预清理阶段,默认当Enden超过2M启动.该阶段等到MinorGC则会在Remark时扫描对象减少.等待时间默认5S,CMSMaxAbortablePrecleanTime参数可设置
可设置CMSScavengeBeforeRemark参数,用来保证Remark前强制进行一次Minor GC.

年轻代GC时

老年代持有新生代引用情况不多,引入卡表Card Table实现引用标记.卡表的具体策略是将老年代的空间分成大小为512B的若干张卡,卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值.Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用

相关参数

-XX:+PrintGCDetails: GC时打印GC日志
-XX:MaxTenuringThreshold: 设置晋升老年代阈值,默认15
Xms和Xmx: 初始堆大小和最小堆大小
-XX:+UseG1GC(JDK9为默认垃圾收集器)
-XX:G1HeapRegionSize

优化思路

对于虚拟机来说,复制对象的成本要远高于扫描成本,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小。因此如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加.所以适当增加Eden区大小保证吞吐量,对于单次GC来说影响不大,但对缩短响应延时会有所提升

  1. 默认新生代老年代容量比例为1:2,是因为新生代对象周期短,创建和回收频繁,复制算法时间快;老年代对象周期长,使用标记-整理/清除算法回收时间慢.
  2. 当新生代短期大对象多时,为了防止动态对象年龄判定造成老年代暴增,导致频繁的MinorGC和MajorGC.应增大新生代大小,减少GC同时,也不会增加很多的GC时长
  3. 当长期对象多时,应扩充下老年代大小,防止频繁MajorGC

学习参考

JVM应用的吞吐量(throughout)是什么意思?暂停时间是什么意思?
CMS垃圾回收机制
美团技术团队-从实际案例聊聊Java应用的GC优化
美团技术团队-Java Hotspot G1 GC的一些关键技术
BILIBILI学习网之JVM原理

特别鸣谢

BILIBILI学习网之马士兵公开课,图片大部分来源于该视频网站(二刷)

#学习路径#
全部评论
感谢参与【创作者计划3期·技术干货场】!欢迎更多牛油来写干货,瓜分总计20000元奖励!!技术干货场活动链接:https://www.nowcoder.com/link/czz3jsgh3(参与奖马克杯将于每周五结算,敬请期待~)
1 回复
分享
发布于 2021-05-14 10:30

相关推荐

17 160 评论
分享
牛客网
牛客企业服务