JVM内存回收

目录


  • 引用计数法

  • 可达性分析算法

    • 可达性分析算法实现方式

  • 堆对象回收

  • 方法区回收

  • 分代收集理论

  • 标记-复制算法

  • 标记-清除算法

  • 标记-整理算法

  • Serial收集器

  • Parallel Scavenge 收集器

  • ParNew收集器

  • CMS收集器(老年代)


判断对象为可回收状态的方法

引用计数法

引用计数法实现简单,并且效率高,原理是在对象中添加一个引用计数器啊,当有对象引用到它时,计数器加1,当不引用时,计数器减1。在GC时,只需要判断引用计数器是否为0,就可以判断该对象是否能被回收。

引用计数法并不被主流虚拟机使用,原因是很难解决对象循环引用的问题,两个对象互相引用导致引用计数器一直不为0,使得GC无法回收这些对象。

可达性分析算法

GC Roots对象作为起点,从这些节点向下查找引用对象,这些对象都标记为非垃圾对象,其余非标记对象即为垃圾对象。

可以作为GC Roots对象:

  • 线程栈局部变量表的对象
  • 静态变量
  • 本地方法栈变量

可达性分析算法实现方式

作为GC Roots的对象会很多,如果在GC前进行扫描会消耗很多时间。JVM采用了OopMap数据结构保存GC Roots对象,当需要GC时,直接从OopMap中获取即可。

当线程运行到safe point时,会保存GC Roots对象到OopMap中。

当需要GC时,GC线程会等待所有用户线程到达safe point后,再进行回收,这样才能保证OopMap是最新的。

如何让用户线程到达safe point?

  • 抢断中断:先暂停所有用户线程,如果发现线程没有到达safe point,则重新运行直到safe point为止
  • 主动中断:设置一个标志位,线程运行时轮询直到为真,就表示运行到safe point(主流使用)

如果GC时线程处于wait或者block状态,则无法主动中断。JVM引入了safe region(安全区)。安全区是指代码片段中,引用关系是不变的,在这个区域任何位置GC都是安全的。当用户线程执行到safe region的代码时,会标注自己进入了安全区,如果线程要离开安全区,则需要等待GC完成。

通过暂停用户线程、安全点和安全区机制,保证了JVM在GC时,GC Roots引用关系不变,可以获取最新的OopMap。

垃圾回收过程

堆对象回收

可达性分析算法会经历两次标记过程,第一次标记垃圾对象会判断对象是否执行finalize方法,如果没有覆盖,对象直接回收。

如果覆盖了finalize方法,则可以在finalize方法中与GC Roots对象建立引用链逃脱回收。

方法区回收

方法区回收主要回收两个部分:

  • 无用的类
  • 无用的常量,例如如果一个字符串在常量池中,但没有String对象引用常量池中的该字符串,则会被GC回收

如何判断一个类是无用的类?

  • 类的所有实例已经被回收
  • 加载类的ClassLoader被回收
  • 类对应的Class对象没有被引用(即无法通过反射创建类)

GC算法

分代收集理论

一般把Java堆分为新生代和老年代,根据两代的特点选择不同的GC算法进行回收。

  • 新生代,每次GC会有大量的对象被回收,所以可以选择复制算法,效率高
  • 老年代,对象存活几率高,使用标记-清除或者标记-整理算法等进行回收

标记-复制算法

将内存分为大小相同的两块,每次只使用其中的一块,当GC时,将存活的对象移动到另外一块可用内存,清除掉使用的这块内存。

  • 优点:快
  • 缺点:浪费一半内存

图示:

JVM内存回收

标记-清除算法

标记-清除算法分为两个步骤:

  • 标记:标记存活的对象
  • 清除:统一回收未被标记的对象

这种算法有两个明显的问题:

  • 效率问题,需要标记的对象太多,效率低
  • 空间问题,清除后产生不连续内存碎片

图示:

JVM内存回收

标记-整理算法

根据老年代特点的标记算法,分为三个步骤:

  • 标记,标记存活对象
  • 整理,所有存活对象挪到一边
  • 清除,清除掉边界外的内存

图示:

JVM内存回收

垃圾收集器

Serial收集器

JVM参数:-XX:+UseSerialGC -XX:+UseSerialOldGC

Serial收集器是单线程收集器,收集过程中会STW。

分代算法使用:

  • 新生代:复制算法
  • 老年代:标记-整理算法

图示:

JVM内存回收

Parallel Scavenge 收集器

JVM参数:-XX:+UseParallelGC -XX:+UseParallelOldGC

Parallel收集器是Serial收集器的多线程版本,使用多线程进行垃圾回收。默认的收集线程数和CPU核数相同(可以通过-XX:ParallelGCThreads修改)。

分代算法使用:

  • 新生代:复制算法
  • 老年代:标记-整理算法

图示:

JVM内存回收

ParNew收集器

JVM参数:-XX:+UseParNewGC

与Parallel Scavenge收集器原理一样,除了Serial收集器外,只有ParNew收集器可与CMS收集器配合使用。

CMS收集器(老年代)

JVM参数:-XX:+UseConcMarkSweepGC(old)

CMS(Concurrent Mark Sweep)收集器是并发的标记清楚算法实现,可以基本让用户线程和GC线程并发工作,产生较短的STW,注重用户体验。

分为四个步骤:

  • 初始标记:STW并记录GC Roots直接引用的对象
  • 并发标记:三色标记法遍历GC Roots,耗时长,但不会STW(由于用户线程同时运行,可能会导致对象状态发生改变)
  • 重启标记:STW并记录并发标记过程中产生的对象状态变更,比初始阶段耗时长,但远远低于并发标记时间
  • 并发清理:并发清理内存空间,这个阶段新增对象不做任何处理
  • 并发重置:重置GC过程中的标记数据

图示:

JVM内存回收

CMS收集器特点:

  • 并发收集
  • 低停顿

CMS收集器缺点:

  • 会和用户线程抢CPU资源
  • 无法处理浮动垃圾(并发标记和并发清理阶段产生的垃圾),浮动垃圾等待下次GC清理
  • 产生内存碎片,可以使用-XX:+UseCMSCompactAtFullCollection参数使用整理算法
  • 执行过程不确定,有可能一次GC还没做完,又触发了GC(并发标记和并发清理阶段出现),触发concurrent mode failure错误,此时STW并使用Serial Old收集器

参数:

-XX:+UseConcMarkSweepGC:启用cms-XX:ConcGCThreads:并发的GC线程数-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次Young GC,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

参考资料

  • CMS垃圾收集器
  • Java JVM的引用计数和可达性分析垃圾收集算法
  • JVM源码分析之安全点safepoint
  • JVM可达性分析算法是怎么实现的?


原文始发于微信公众号(erpang coding):JVM内存回收

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/37325.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!