JVM系列 从一到掌握JVM系列之垃圾回收算法

愿君学长松,慎勿作桃李。这篇文章主要讲述JVM系列 从一到掌握JVM系列之垃圾回收算法相关的知识,希望能为你提供帮助。


【JVM系列 从一到掌握JVM系列之垃圾回收算法】
标记阶段:引用计数算法垃圾标记阶段:对象存活判断

  • 在堆里存放着几乎所有的 java 对象实例,在 GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
  • 那么在 JVM 中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
  • 判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
方式一:引用计数算法
  • 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
  • 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
  • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点:
    • 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
      每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
    • 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
循环引用
当p的指针断开的时候,内部的引用形成一个循环,这就是循环引用,从而造成内存泄漏。

举例
/**
* -XX:+PrintGCDetails
* 证明:java使用的不是引用计数算法
* @author shkstart
* @create 2020 下午 2:38
*/
public class RefCountGC
//这个成员属性唯一的作用就是占用一点内存
private byte[] bigSize = new byte[5 * 1024 * 1024]; //5MB

Object reference = null;

public static void main(String[] args)
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();

obj1.reference = obj2;
obj2.reference = obj1;

obj1 = null;
obj2 = null;
//显式的执行垃圾回收行为
//这里发生GC,obj1和obj2能否被回收?
System.gc();

try
Thread.sleep(1000000);
catch (InterruptedException e)
e.printStackTrace();



通过 ??-XX:+PrintGCDetails?? 输出详细信息
[GC (System.gc()) [PSYoungGen: 15497K-> 696K(76288K)] 15497K-> 704K(251392K), 0.0013045 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 696K-> 0K(76288K)] [ParOldGen: 8K-> 624K(175104K)] 704K-> 624K(251392K), [Metaspace: 3274K-> 3274K(1056768K)], 0.0043467 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
Heap
PSYoungGentotal 76288K, used 655K [0x000000076ad80000, 0x0000000770280000, 0x00000007c0000000)
eden space 65536K, 1% used [0x000000076ad80000,0x000000076ae23ee8,0x000000076ed80000)
from space 10752K, 0% used [0x000000076ed80000,0x000000076ed80000,0x000000076f800000)
tospace 10752K, 0% used [0x000000076f800000,0x000000076f800000,0x0000000770280000)
ParOldGentotal 175104K, used 624K [0x00000006c0800000, 0x00000006cb300000, 0x000000076ad80000)
object space 175104K, 0% used [0x00000006c0800000,0x00000006c089c178,0x00000006cb300000)
Metaspaceused 3281K, capacity 4496K, committed 4864K, reserved 1056768K
class spaceused 359K, capacity 388K, committed 512K, reserved 1048576K

我们能够看到,上述进行了GC收集的行为,将上述的新生代中的两个对象都进行回收了
PSYoungGen: 15490K-> 808K(76288K)] 15490K-> 816K(251392K)

如果使用引用计数算法,那么这两个对象将会无法回收。而现在两个对象被回收了,说明Java使用的不是引用计数算法来进行标记的。

引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的 python,它更是同时支持引用计数和垃圾收集机制。
具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
Java 并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
Python 如何解决循环引用?
手动解除:很好理解,就是在合适的时机,解除引用关系。
使用弱引用 weakref,weakref 是 Python 提供的标准库,旨在解决循环引用。






    推荐阅读