本文内容来自于周志明的《深入理解 Java 虚拟机》,仅供学习交流使用。
常见的 Java 的垃圾回收(Garbage Collection,GC)算法主要分四种。
第一种是标记-清除算法。它先把可回收的对象一个个地从存储区域中标记出来,再把这些对象一个个地删除。它的缺点是,垃圾回收后,空闲的物理数据区域不是连续的。
第二种是复制算法,它每次执行时,都会把存活的对象从一个区域存入另一个区域。这样,既能一次性清除前一个区域,又能保证后一个区域的连续。但是它又会额外消耗一倍的存储空间。
第三种则是标记-整理算法。它在标记-清除算法上做了改进,它先标记出可回收对象,再对存活的对象进行整理,最后清空边界外的对象(有点像在一个数组里删掉好几个元素后,先把后面的对象前移,再把数组尾部“多余”的对象清理掉)。
第四种算法是分代收集算法。它一般根据 Java 对象的存活周期将内存分为新生代和老年代两块。在新生代中,一般存储存活周期较短的对象;在老年代中,一般存储存活周期较长的对象。根据新生代和老年代的区别,这两块存储区采用不同的垃圾收集算法。新生代采用复制算法,老年代采用标记-清除或标记-整理算法。
Minor GC & Major GC
新生代中采用的是 Minor GC 算法,即复制算法。它将存储区域按照 8:1:1 的大小划分为一个 Eden 区域(以下简称 E 区)和两个 Survivor 区域(以下简称 S 区) 。执行 Minor GC 算法时,E 区域和一个 S 区域中的对象会被存入另一个 S 区中。当对象从 E 区进入 S 区时,对象的年龄会变成 1。当对象从一个 S 区复制到另一 S 区时,对象的年龄就加 1。当对象的年龄满足一定条件时,这个对象就从新生代存入老年代。
老年代中采用的是 Major GC 算法,又称为 Full GC 算法,一般也就是标记-清除或标记-整理算法。Major GC 它一般会比 Minor GC 慢 10 倍以上,因此,Major GC 也会比 Minor GC 调用次数更少。
Java 的内存分配机制与垃圾回收算法紧密相关。
新建的对象一般都存储在新生代中。但是,当新建了一个很大的对象时(对象的大小大于虚拟机参数 PretenureSizeThreshold 时),大对象会直接存入老年代。
当新生代中的存储空间不足时,会触发 Minor GC 算法清理新生代的存储区。此时,可能出现对象衰老或分配担保的情况。
有两种情况会引起对象衰老,即对象从新生代进入老年代。
第一:如果被复制的对象的年龄太大(对象的年龄大于虚拟机参数 MaxTenuringThreshold 时),该对象会直接存入老年代。
第二:如果在 S 区中相同年龄的所有对象的总大小超过了 S 区的一半,年龄大于或等于该年龄的对象就进入老年代。(无视 MaxTenuringThreshold,适应性更高)
分配担保。即新生代没有足够的存储空间时,需要“借用”老年代的空间。也就是说,老年代担保了新生代的分配空间。同样,也有两种情况会产生分配担保。
第一,对象大小大于 S 区。从 E 区存入 S 区的对象太大,S 区存不下,这个对象就会被直接存入老年代。
第二,对象数量太多。有许多存活的小对象需要保存,S 区存不下,因此,也会有一些对象被存入老年代。(但是并不知道是具体哪些对象复制到 S 区,哪些对象存入老年代)
须知,市面上常用的垃圾收集器有许多种,它们并没有统一的规则和参数,不同的收集器会有不同的内存回收行为。因此,以上只是一种可行的回收方案,并不能代表所有方案。