JVM 的自动内存管理,让原本应该是开发人员去做的事情,变成了垃圾回收器来做的事情
既然是别人帮忙做的事情,那么可能就不是自己想要的,所以就需要我们了解一下垃圾回收相关的内容
引用计数法与可达性分析
垃圾回收,垃圾回收,那就是有的内存分配给了一些对象,但是这些对象已经用完了,那么它所占用的内存也就应该该释放掉了,却还没有释放
那么,这里就有个问题:该如何确定一个对象用完了呢?
其中一种方法就是引用计数法
引用计数法就是给每个对象添加一个引用计数器,来统计指向该对象的引用个数
比如:如果有一个引用,被赋值为某一个对象,那么这个对象的引用计数器就 +1 ,如果一个指向这个对象的引用,被赋值为了其他的值,那么这个对象的引用计数器就 -1 ,这样如果这个对象的引用计数器为 0 ,我们就可以认为这个对象已经使用完毕,它所占用的内存空间可以回收掉了
这种方案听上去无懈可击,但是有一个致命的漏洞,就是没办法处理循环引用的问题
比如说: A 和 B 互相引用,除此之外也没有其他的引用指向 A 或者 B ,在这种情况下,其实 A 和 B 所占用的内存就可以释放掉了,但是因为它们互相都有引用,所以此时的引用计数器并不为 0 ,在这种情况下,就不能对它们进行回收
现在只是两个对象,如果再来两个,再来两个,这样循环引用的对象多了之后,就会造成内存泄露
基于引用计数法的弊端,当前 JVM 主流的垃圾回收器采取的是可达性分析算法
这个算法本质就是将一系列的 GC Roots 作为初始的存活对象合集( live set ),然后从这个合集出发,探索所有能够被该集合引用到的对象,并把这些对象加入到集合中来,这个过程就叫做标记( mark ),遍历到最后,没有被探索到的对象就是可以回收的对象
那么什么是 GC Roots 嘞?一般包括(但不限于)以下几种:
刚才说因为引用计数法存在循环引用的问题,所以目前主流垃圾回收器选用的都是可达性分析法,也就是说,它解决了循环引用问题,其实这一点也比较好理解,虽然 A 和 B 相互引用,但是这个时候从 GC Roots 开始出发,是没有办法到达 A 和 B 的,那么就不会把它们放到存活对象合集之中,自然也就会被回收掉
但是在实际中还是会有问题的,比如:在多线程环境下,就会有其他线程更新已经访问过的对象中的引用,但是是多线程并行的嘛,这个时候可达性分析法已经把这个引用设置成了 null ,或者这个对象还在使用,但可达性分析法把它标记为了没有被访问过的对象,被回收掉了,这种情况可能直接导致 JVM 崩溃掉
Stop-the-world & safepoint
既然可达性分析法也有自己的一些缺陷,总得有解决方案吧?比较暴力的一种方法就是 Stop-the-world ,估计听名字也能知道,就是让全世界都停下来,也就是说,在进行垃圾回收的时候,其他所有非垃圾回收线程的工作都需要停下来,先让垃圾回收器工作完毕再说。这就是所谓的暂停时间( GC pause )
Stop-the-world 是通过安全点( safepoint )机制来实现的。啥意思嘞?咱先想个场景,现在你敲代码敲的特别开心,又有思路,状态又好,美滋滋的正在工作,突然毫无缘由的就让你现在不准敲代码,你会不会不开心?好不容易思路来了对吧,就一点儿理由都不给的就让我停下,不合理吧?
同样的场景,一个线程现在跑的特别 happy ,而且再有一秒钟就完成了任务,这个时候 JVM 收到了 Stop-the-world 请求,二话不说就把所有的线程给停掉,不太好吧?那么这个时候安全点( safepoint )机制就登场了。有了安全点机制,当 JVM 收到 Stop-the-world 请求的时候,它就会等待所有的线程都达到安全点,才允许请求 Stop-the-world 的线程进行独占的工作
那么,什么时候是安全点呢?举个例子来说:当 Java 程序通过 JNI 执行本地代码时,如果这段代码不访问 Java 对象,不调用 Java 方法,不返回到原 Java 方法,那么 Java 虚拟机的堆栈就不会发生改变,那这段本地代码就可以作为一个安全点。只要不离开这个安全点, JVM 就可以在垃圾回收的同时,继续运行这段本地代码
因为本地代码需要通过 JNI 的 API 来完成上述三个操作,因此 JVM 只需要在 API 的入口处进行安全点检测( safepoint poll ),看看有没有其他线程请求停留在安全点这里,就可以在必要的时候挂起当前线程
垃圾回收的三种方式
当标记好存活的对象之后,就可以进行垃圾回收了
主流的垃圾回收方式,可以分为三种:清除( sweep ),压缩( compact ),复制( copy )
清除,就是把死亡对象所占据的内存标记成空闲内存,并把它记录在一个空闲列表( free list )中,当需要新建对象的时候,就直接在空闲列表中寻找空闲内存,划分给新建的对象就完了
但是这里会产生一个问题,因为死亡的对象所占据的内存可能是随机的,回收完毕之后,内存就是碎片化的,如果此时有对象申请一块连续的内存空间,尽管碎片化的内存空间是够用的,也没办法进行分配