JVM 是怎么把“送”出去的内存又“要”回来的 - 图2

上一篇我们知道了内存回收是指那些在堆或方法区中不在引用或使用的对象被回收掉,以保证后面的对象能够顺利的被分配。但是随之出现一个问题,就是虚拟机是怎么知道某个对象不使用了呢?今天就一起来看看虚拟机是怎么来判断一个对象是否存活的。

目录:

  1. 垃圾收集算法分类
  2. 分代收集的概念
  3. 主流 JVM 中的回收算法

垃圾收集

垃圾收集算法可以从如何判断对象消亡分为两类

  1. 引用计数式垃圾收集(Reference Counting GC)
  2. 追踪式垃圾收集(Tracing GC)

在主流的 JVM 中,使用的是第二种 追踪式垃圾收集 算法。

更多内容推荐阅读Richard Jones撰写的《垃圾回收算法手册》的第2~4章的相关内容。

多学一点:大多 JVM 不使用第一种方式的原因是因为引用计数的办法会导致两个对象在互相引用时,计数器的值均不为零,从而导致内存回收出现问题,进而导致内存泄漏。

分代收集

因为大部分对象都是短暂存活后就死亡了,所以根据每次 GC 对对象进行年龄的划分来区别对待。

根据理论和实践数据,“垃圾们” 被分为了两个年龄阶段

  • 新生代
  • 老年代

对于不同区域的内存回收(部分收集 Partial GC), GC 也因此分为了

  • MinorGC(Young GC)新生代的垃圾收集。
  • Major GC(Old GC CMS独有)老年代的垃圾收集。
  • Mixed GC(G1中独有)收集整个新生代以及部分老年代的垃圾收集。
  • Full GC(收集整个Java堆和方法区的垃圾收集。)

这里要知道,分代收集的思想不是凭空出现,而是根据理论是实践之后的数据得出

对于理论和实践的内容补充,理论这一块,最早的时候,人们认为,大部分对象存在很短的一段时间就会消失,这就是弱分代假说。而与之对应的强分代假说就是如果一个对象经过多次 GC 之后仍然存活,那就认为其很难消亡。事实上这一块不只一方拿出来实际的数据,这里包括个人或组织,其中业内具有影响力的 IBM 曾经公布一个数据,98% 的新对象会在第一次 GC 被回收掉。

多学一点:在强弱分代假说的基础上,引申出一个新的假说,就是跨代引用假说。比如一个新生代对象被老年代所引用,那么你便不得不去在每次新生代 GC (minor gc)时去扫描全部的老年代对象。基于这个问题提出了记忆集与卡表的概念,这个数据结构专门用来表示老年代的数据区域,在(minor gc)时会只会扫描某个数据区域内的老年代对象即可。(关于这部分内容因为涉及垃圾回收器的实现细节,所以本篇暂不过多讨论,后续会专门整理一篇关于虚拟机垃圾回收实现细节的内容,欢迎关注催更)

回收算法

上面我们知道虚拟机对内存进行的不同的区域划分,于是针对不同的区域也拥有了不同的处理方法。
“标记-清除算法”“标记-复制算法”“标记-整理算法”。

标记-清除算法

分为两步,首先通过可达性分析将需要清理的对象标记出来,之后将其清除。

不过这种方式的缺点很明显,有两个:

  1. 当对象过多时需要遍历的量变多。
  2. 这种方式会产生大量碎片空间。

图片来自周志明《深入理解 Java 虚拟机(第三版)》3.3.2 标记-清除算法

JVM 是怎么把“送”出去的内存又“要”回来的 - 图3

标记-复制算法 (新生代)

图片来自周志明《深入理解 Java 虚拟机(第三版)》3.3.3 标记-复制算法

这种算法将新生代直接一分为二,每次只使用一半,一面用完以后,将存活的对象复制到另外一半空间上去,将当前空间全部清除

JVM 是怎么把“送”出去的内存又“要”回来的 - 图4

不过这种方式导致空间利用率低下,并且上面我们也提过有实践证明大部分对象在第一次 GC 会被回收掉,数据验证大部分对象会在新生代被回收,这个数值由 IBM 量化,98%的对象会被回收, 所以 hotspot 采用了使用一个 Eden 和两块 Survivor ,默认比 8:1,这样即只有 10% 的空间留着复制备用,大大提高了 标记-复制 算法的可用性。这种划分方式成为 Appel 式回收

多学一点:Appel 式回收还有一个分配担保,即复制到备用的 Survivor 上时空间不够,此时内存将在 老年代 完成分配。

标记-整理算法(老年代)

关于这个算法的名字有的叫标记-复制,有的叫标记-整理,这个不要纠结,只是个名字,明白其中的思想就可以了

在新生代因为存活的对象很少,即需要复制的对象较小,使用 标记-复制 算法可以很高效的解决垃圾收集这件事,不过在老年代因为对象大多存活时间较长,同时这种降低空间利用率的方法在这里便没那么好用了。

标记-整理 算法在 标记-清除 算法的基础上加入了移动操作,标记步骤相同,之后 标记-整理 算法会将存活对象向一端移动,直到没有存活对象为止,之后在分界处将内存回收掉。

图片来自周志明《深入理解 Java 虚拟机(第三版)》3.3.4 标记-整理算法

JVM 是怎么把“送”出去的内存又“要”回来的 - 图5

对于 标记-整理 算法中的移动操作,有需要取舍的地方

如果移动

程序停顿与更新对象引用影响程序的吞吐量

内存移动操作需要暂停用户线程,设计者称其为 “ Stop The World ” ,并且移动存活对象后,对于的对象引用地址需要进行更新(这里的关联知识点就是对象的访问方式,会影响到通过 直接访问 这种方式。使用 句柄 访问的方式不受对象内存位置移动影响)

在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。—— 郑雨迪极客时间专栏《深入拆解Java虚拟机》11|垃圾回收(上)

(如果你玩过守望先锋的话)

JVM 是怎么把“送”出去的内存又“要”回来的 - 图6

不移动

链式内存存储影响程序的延迟

在大量的内存碎片产生后,导致内存通过链式存储的方式来保存对象。这将加大对象的存储和访问开销。

怎么办?

抉择

基于以上两种影响,不同的垃圾回收器选择了不同的实现方法。

hotspot 虚拟机中关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法

多学一点:CMS 虽然使用了 标记-清除算法 但在其碎片化程度较高时(影响对象分配内存)会进行一次 标记-整理。而且 CMS 还有一个特点,不知道你还记得不,在之前我们一起学的对象咋创建的那篇文章,提到了指针碰撞(加法)这种内存分配方式,而这种方式只能在堆空间规整的前提下才能使用,显然 CMS 的标记-清除策略不能够直接使用,所以 CMS 在实现细节上,他在空闲列表上申请内存时,会申请一块较大的空间,然后在这块‘属于自己’的内存空间上在进行造作(指针碰撞)。

另外,还有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。前面提到的基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。 — 周志明《深入理解 Java 虚拟机(第三版)》3.3.4 标记-整理算法

小结

简单总结一下前面几篇文章的主要内容:

开篇的《JVM 你知道不?一起来学啊》介绍了一下我们为什么要学习虚拟机,同时对虚拟机内容大纲有所了解。

接着我们通过《你创建的 Java 对象搁哪了》了解了 Java 程序的运行时数据区域内容。知道了具体的内存划分之后,我们急不可耐的想知道《JVM 中对象咋创建啊,又怎么访问啊》。

当我们知道对象怎么创建的之后,就想着法的想“弄死“它,把分给它的地方要回来,于是我们一起翻开了秘籍《JVM 是怎么把“送”出去的内存又“要”回来的》。

今天我们又细细的品了品JVM 把内存”收“回来用的是什么法器,至此我们已经知道了虚拟机内存管理的大部分内容了,包括内存区域、内存分配、内存回收以及具体的回收法器。接下来的内容就是关于具体的垃圾收集器了。

(正文完)


如果觉得写的还不错,欢迎关注催更收藏、点赞、转发推荐给更多的人,如果想一起系统学习的话,非常欢迎加群一起讨论学习,“我们都是菜鸡,等你来互啄”,让虚拟机知识从此和枯燥乏味说拜拜。