手记

简单聊聊JVM的垃圾回收

生常谈,先回顾下基础知识。

既然要垃圾回收,就好比现实生活中我们去回收垃圾一样。第一步,先判断是不是垃圾(ps:单位里阿姨每天都会来帮我们清理办公环境,但她肯定不会动我桌上的东西,尽管桌上的瓶子已经空了好几天了,阿姨还是会认为不是垃圾;在垃圾桶里的,即使是还没喝的可乐,阿姨也会认为他是垃圾把他倒掉),第二步,回收清理垃圾。

第一步,JVM中如何判断这个对象是不是垃圾呢,换句话说,如何判断是否已经死亡了呢?

两种判断对象是否死亡的方法

1.引用计数法

它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。

它的具体实现是这样子的:如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器 +1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器 -1。也就是说,我们需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。

除了需要额外的空间来存储计数器,以及繁琐的更新操作,引用计数法还有一个重大的漏洞,那便是无法处理循环引用对象。

举个例子,假设对象 a 与 b 相互引用,除此之外没有其他引用指向 a 或者 b。在这种情况下,a 和 b 实际上已经死了,但由于它们的引用计数器皆不为 0,在引用计数法的心中,这两个对象还活着。因此,这些循环引用对象所占据的空间将不可回收,从而造成了内存泄露。

2.GC Roots可达性分析法

目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。

那么什么是 GC Roots 呢?我们可以暂时理解为由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)如下几种:

Java 方法栈桢中的局部变量;

已加载类的静态变量;

JNI handles;

已启动且未停止的 Java 线程。

可达性分析可以解决引用计数法所不能解决的循环引用问题。举例来说,即便对象 a 和 b 相互引用,只要从 GC Roots 出发无法到达 a 或者 b,那么可达性分析便不会将它们加入存活对象合集之中。

虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。

比如说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。

误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。

Stop-the-world 以及安全点

为了防止回收还正被引用的对象,我们需要一个安全的环境即Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。

先不说具体的安全点的概念,先认为JVM有自己关于安全点的定义。

当JVM收到stop-the-world的时候,他便会等待所有的线程都到达安全点,然后才开始回收。

垃圾回收的三种方式

1.清除

即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。

缺点很明显,会造成内存碎片。JVM内部是要求内存连续的,所以就会出现总的空闲内存还比较充足但是无法分配对象的情况。

2.压缩

即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。

3.复制

即把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。




作者:江南京城
链接:https://www.jianshu.com/p/bd2f099d37f2


0人推荐
随时随地看视频
慕课网APP