JVM 的垃圾收集算法主要包括 4 种:标记-清除算法,标记-整理算法,复制算法,分代收集算法,相比而言,分代收集算法是最常用的,也相对复杂一点,所以在此整理记录一下,加深记忆。
垃圾收集的是哪里?
首先抛出一个问题,垃圾收集,收集的到底是哪里?
我们来看一下 Java 的内存区(又叫做运行时数据区),如下图所示:
可以看出,按照线程共享和线程私有可以分为两大块,各自的组成如下:
线程共享:
堆,方法区(HotSpot 的永久代);
线程私有:
程序计数器,Java 虚拟机栈,本地方法栈;
在内存结构中,论大小,堆可以称得上是 JVM 中所管理的最大的一块内存,所以它就是垃圾收集器管理的主要目标了。
为什么要分代?
一般来说,我们将堆分为新生代和老年代,这样就可以根据各个年代的特点选择合适的垃圾收集算法。
在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成垃圾收集的过程。
而老年代的对象存活率是比较高的,且没有额外的空间对它进行分配,所以我们必须选择 “标记-清除” 或 “标记-整理” 算法进行垃圾收集。
回收是如何进行的?
从下图中可以看出,堆被分为新生代和老年代,它们之间的空间大小比例为 8:1:1,其中的新生代又被划分为 3 个部分,分别为 Eden 区,From Survivor 区和 To Survivor 区。
回收过程如下:
- 当新创建一个对象后,它会被默认分配到新生代的 Eden 区域;
- 当 Eden 区满了之后,会把 Eden 和 From Survivor 区域的存活对象统一移动到 To Survivor 区,并且给移动对象的年龄 + 1;
- 之后会回收死掉的对象,并把原来的 From Survivor 区改成 To Survivor 区,To Survivor 区改为 From Survivor 区;
- 当 To Survivor 区满了后,就会将所有的对象移动到老年区;
其中提及到了一个概念叫移动对象的年龄,它又是指什么呢?
实际上,虚拟机给每个对象都设置了一个对象年龄(Age)计数器,如果对象在 Eden 区域出生并经过新生代垃圾回收(Minor GC) 后仍然存活,且能被 Survivor 区域所容纳的话,就会被移动到 Survivor 空间中,并将对象年龄设为 1。
之后,对象在 Survivor 中每熬过一次新生代垃圾回收(Minor GC),年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中(对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置)。
此时突然有了一个新的问题,为什么 GC 的分代年龄要设置为 15,16 不可以么?
查阅了网上的资料后了解到,对象的 GC 年龄是保存在对象的头信息里的,头信息又分为两个部分:Mark Word 和 Klass Pointer。其中的 Mark Word 主要用于存储对象自身的运行时数据,我们的 GC 分代年龄就存储 Mark Word 里面。
看到一篇博客对这个问题分析的很到位,附上链接,大家可以自行查看。
https://javap.blog.csdn.net/article/details/103721326
摘录部分内容:
我们可以通过实际操作,来验证 GC 分代年龄是如何进行存储的:OpenJDK 提供了一个工具包,可以输出对象的结构布局信息。
引入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
编写一段测试代码如下:
/**
* @Author: pch
* @Date: 2019/12/23 09:08
* @Description: 查看对象的头信息
*/
public class HeaderTest {
public static void main(String[] args) throws InterruptedException {
Person p = new Person();
//不调用 hashCode() 不会记录哈希码
int hashCode = p.hashCode();
//转16进制输出,与头信息中的hashCode进行比较
String hex = Integer.toHexString(hashCode);
System.out.println("HashCode十六进制:"+hex);
print(p);
}
static void print(Person p){
System.err.println(ClassLayout.parseInstance(p).toPrintable());
}
}
class Person{
private boolean flag;
}
控制台输出如下:
从图片中可以看到,因为 Object Header 采用 4 个 bit 位来保存年龄,而 4 个 bit 位能表示的最大数就是 15,所以分代年龄最大值只能是 15,16 的话就存不下了。
到此,年龄最大值的问题就搞明白了,我们分析了半天,都是在说堆的回收,那么方法区没人管了么?这就要说到回收的类型了。
回收分为哪些类型?
垃圾回收分为部分收集 (Partial GC) 和整堆收集 (Full GC),我们刚刚提到的方法区,就是通过整堆收集来进行垃圾回收的。
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC): 收集整个 Java 堆和方法区。
整理到这里,分代垃圾回收的过程基本就清晰了,保持学习,坚持记录,本篇博客参考了以下资料,在此表达感谢:
https://blog.csdn.net/xiaozhaoshigedasb/article/details/85568596
https://javap.blog.csdn.net/article/details/103721326