JVM学习笔记(四):GC算法与四种引用
1 来源
- 来源:《Java虚拟机 JVM故障诊断与性能优化》——葛一鸣
- 章节:第四章
本文是第四章的一些笔记整理。
2 GC
算法
常见的GC
算法包括:
- 引用计数法
- 标记清除法
- 复制算法
- 标记压缩法
- 分代算法
- 分区算法
2.1 引用计数法
实现原理:
- 对于一个对象A
- 只要有任何一个对象引用A,A的引用计数器就加1
- 引用失效时,引用计数器就减1
- 只要对象A的引用计数器的值为0,对象A就不会再被使用,等待被回收
缺点:
- 无法处理循环引用,比如A引用B,B引用A,但是并没有其他对象引用两者,此时A和B的引用计数都不为0,这样就无法被回收
- 引用计数器要求每次引用产生和消除的时候,伴随一个加法操作和一个减法操作,对系统性能会有一定的影响
由于引用计数法上述的缺点,Java
中并未采用作为GC
算法。
2.2 标记清除法
标记清除法将垃圾回收分为两个阶段:
- 标记阶段:通过根节点标记所有从根节点开始的可达对象,未被标记的就是垃圾对象
- 清除阶段:清除所有未被标记的对象
标记阶段:
清除后:
缺点很明显,就是回收后的空间是不连续的,工作效率会低于连续的内存空间。
2.3 复制算法
核心思想:
- 内存空间分为相等的两块
- 每次只使用其中一块
- 回收的时候将存活对象移到另一块中,然后清除正在使用的内存块中所有对象
- 交换两个内存块的角色
优点是回收后的内存空间是没有碎片的,而缺点是如果存在大量的对象,需要花费大量的时间复制,并且内存只有原来的一半。
比如下图中的A、B两块相同的内存空间,A在垃圾回收的时候,将存活对象复制到B中,B在复制后保持连续:
复制完成后,A会被清空,并将B设置为当前使用的空间。
在Java
的新生代串行垃圾回收器中,使用了复制算法,新生代分为eden
区、from
区以及to
区。其中from
和to
区是两块内存相同的空间,也叫survivor
区,也就是幸存者空间。在垃圾回收的时候,eden
区以及from
区存活的对象会被复制到to
区,然后清空from
区与eden
区,接着from
和to
区的角色将会交换,也就是下一次垃圾回收的时候,会从原来的to
区(新的from
区)复制到原来的from
区(新的to
区)。
2.4 标记压缩法
标记压缩法是一种老年代算法,在标记清除法的基础上做了一些优化,和标记清除法一样,首先也需要从根节点开始,对所有可达对象做一次标记,然后将所有存活对象压缩到内存的一端,接着清理边界外的所有空间,图示如下:
标记压缩法的优点是可以避免碎片的产生,又不需要两块相同的内存空间。
2.5 分代算法
分代算法并不是一种具体的垃圾回收算法,分代算法其实是一种根据每块内存空间的特点使用不同回收算法以提高效率的算法。比如:
- 在新生代中:会有大量的新建对象很快被回收,因此新生代比较适合使用复制算法
- 在老年代中:采用标记压缩法或标记清除法
2.6 分区算法
分区算法将整个堆空间划分成连续的不同小区间,每个小区间都独立使用,独立回收,如图所示:
3 四种引用
Java
里面提供了4个级别的引用:
- 强引用
- 软引用
- 弱引用
- 虚引用
下面分别来看一下。
3.1 强引用
强引用就是代码中一般使用的引用类型,强引用的对象是可触及的,不会被回收,比如:
StringBuffer str = new StringBuffer("a");
如果上面的代码运行在方法体内,那么局部变量str
会被分配在栈上,而对象StringBuffer
实例会被分配在堆上,str
指向的是StringBuffer
实例所在的堆空间,通过str
可以操作该实例,str
就是StringBuffer
实例的强引用。
又比如执行了以下代码:
StringBuffer str1 = str;
那么str1
也会指向str
指向的对象,也就是它们都指向同一个StringBuffer
实例,此时str1==str
的值为真,因为两个指向的是同一个堆空间地址。
强引用的特点如下:
- 可以直接访问目标对象
- 强引用指向的对象不会被系统回收,
JVM
宁愿抛出OOM
也不会回收强引用指向的对象 - 强引用可能会导致内存泄漏
3.2 软引用
软引用是被强引用弱一点的引用类型,如果一个对象只持有软引用,那么当堆空间不足的时候,就会被回收,软引用可以使用SoftReference
类实现,比如下面的代码:
public static void main(String[] args){
Byte[] b = new Byte[1024*1024*8];
SoftReference<Byte[]> softReference = new SoftReference<>(b);
b = null;
System.out.println(softReference.get());
System.gc();
System.out.println("After GC");
System.out.println(softReference.get());
b = new Byte[1024*1024*8];
System.gc();
System.out.println(softReference.get());
}
在OpenJDK 11.0.10
上,加上-Xmx40m
的输出如下:
[Ljava.lang.Byte;@1fbc7afb
After GC
[Ljava.lang.Byte;@1fbc7afb
null
可以看到,当垃圾回收的时候,未必会回收软引用对象,但当内存紧张时,会回收软引用对象。
3.3 弱引用
弱引用是比软引用弱的引用类型,在垃圾回收的时候,只要发现弱引用,不管系统空间使用情况如何,都会将对象进行回收。但是由于垃圾回收器的线程通常优先级不高,并不一定能很快发现弱引用对象,这种情况下弱引用对象可以存在较长时间。弱引用例子如下:
public static void main(String[] args){
Byte[] b = new Byte[1024*1024*8];
WeakReference<Byte[]> softReference = new WeakReference<>(b);
b = null;
System.out.println(softReference.get());
System.gc();
System.out.println("After GC");
System.out.println(softReference.get());
}
输出(-Xmx40m
):
[Ljava.lang.Byte;@1fbc7afb
After GC
null
可以看到在GC
后,弱引用对象会被立即回收。
软引用、弱引用的一个常见使用场景是保存可有可无的缓存数据,当系统内存不足时,这些内存数据会被回收,不会导致OOM
,而内存充足时,这些缓存数据又可以存在相当长的时间,从而起到让系统加速的作用。
3.4 虚引用
虚引用是所有引用类型中最弱的一个,一个持有虚引用的对象和没有引用几乎是一样的,随时都可能被垃圾回收器回收。另外,试图使用虚引用的get()
方法获取强引用的时候,总是会失败,并且虚引用需要和引用队列一起使用,作用在与跟踪垃圾回收过程。
public static void main(String[] args) throws Exception {
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> reference = new PhantomReference<>(new String("test"),queue);
System.out.println(reference.get());
}
输出结果:
null