|
|
|
|
公众号矩阵

来说说垃圾回收怎么样~

JVM 的自动内存管理,让原本应该是开发人员去做的事情,变成了垃圾回收器来做的事情,既然是别人帮忙做的事情,那么可能就不是自己想要的,所以就需要我们了解一下垃圾回收相关的内容。

作者: 鸭血粉丝 来源:Java极客技术|2020-11-27 07:45

本文转载自微信公众号「Java极客技术」,作者鸭血粉丝 。转载本文请联系Java极客技术公众号。 

JVM 的自动内存管理,让原本应该是开发人员去做的事情,变成了垃圾回收器来做的事情

既然是别人帮忙做的事情,那么可能就不是自己想要的,所以就需要我们了解一下垃圾回收相关的内容

引用计数法与可达性分析

垃圾回收,垃圾回收,那就是有的内存分配给了一些对象,但是这些对象已经用完了,那么它所占用的内存也就应该该释放掉了,却还没有释放

那么,这里就有个问题:该如何确定一个对象用完了呢?

其中一种方法就是引用计数法

引用计数法就是给每个对象添加一个引用计数器,来统计指向该对象的引用个数

比如:如果有一个引用,被赋值为某一个对象,那么这个对象的引用计数器就 +1 ,如果一个指向这个对象的引用,被赋值为了其他的值,那么这个对象的引用计数器就 -1 ,这样如果这个对象的引用计数器为 0 ,我们就可以认为这个对象已经使用完毕,它所占用的内存空间可以回收掉了

这种方案听上去无懈可击,但是有一个致命的漏洞,就是没办法处理循环引用的问题

比如说: A 和 B 互相引用,除此之外也没有其他的引用指向 A 或者 B ,在这种情况下,其实 A 和 B 所占用的内存就可以释放掉了,但是因为它们互相都有引用,所以此时的引用计数器并不为 0 ,在这种情况下,就不能对它们进行回收

现在只是两个对象,如果再来两个,再来两个,这样循环引用的对象多了之后,就会造成内存泄露

基于引用计数法的弊端,当前 JVM 主流的垃圾回收器采取的是可达性分析算法

这个算法本质就是将一系列的 GC Roots 作为初始的存活对象合集( live set ),然后从这个合集出发,探索所有能够被该集合引用到的对象,并把这些对象加入到集合中来,这个过程就叫做标记( mark ),遍历到最后,没有被探索到的对象就是可以回收的对象

那么什么是 GC Roots 嘞?一般包括(但不限于)以下几种:

  • Java 方法栈桢中的局部变量
  • 已加载类的静态变量
  • JNI handles
  • 已启动并且没有停止的 Java 线程

刚才说因为引用计数法存在循环引用的问题,所以目前主流垃圾回收器选用的都是可达性分析法,也就是说,它解决了循环引用问题,其实这一点也比较好理解,虽然 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 )中,当需要新建对象的时候,就直接在空闲列表中寻找空闲内存,划分给新建的对象就完了

但是这里会产生一个问题,因为死亡的对象所占据的内存可能是随机的,回收完毕之后,内存就是碎片化的,如果此时有对象申请一块连续的内存空间,尽管碎片化的内存空间是够用的,也没办法进行分配

压缩,就是把存活的对象聚集到内存区域的起始位置,这样就可以留下一段连续的内存空间。这样去做的话,可以解决内存碎片化的问题,代价就是压缩算法带来的性能开销

复制,就是把内存区域分成两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当进行垃圾回收时,就把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。

复制这种方式也可以解决内存碎片化的问题,但是它的缺点也是比较明显的,因为把内存区域分成了两等分嘛,那利用率就比较低咯,最高也是 50% 了,不能再高了

垃圾回收在 JVM 中的应用

上面说的三种垃圾回收方式是理论上的,那么在 JVM 中是如何应用的呢?

这就先要来了解下 JVM 的堆划分,大概就是这样子:

JVM 将堆划分为新生代和老年代,在新生代中又划分为 Eden 区,还有两个大小相同的 Survivor 区

当程序调用 new 指令时,会在 Eden 区中划出一块作为存储对象的内存,但是因为堆空间是线程共享的,所以在这里面划分空间的话就需要同步,要不然出现了两个对象共用一段内存,那不就该打架了嘛

JVM 为了避免两个对象打架的事情发生,就让每个线程向 JVM 申请一段连续的内存,来作为线程私有的 TLAB ( Thread Local Allocation Buffer ,对应虚拟机参数 -XX:+UseTLAB ,默认开启的)

Eden 区一直进行分配,总有空间分配完毕的时候,该怎么办?此时 JVM 就会触发一次 Minor GC ,来收集新生代的垃圾,存活下来的对象就会被送到 Survivor 区

在图中可以看到, Survivor 区有两个,一个是 from ,一个是 to ,其中 to 指向的 Survivor 区是空的

当发生 Minor GC 时, Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区,然后交换 from 和 to 指针,这样就保证了下一次 Minor GC 时, to 指向的 Survivor 区还是空的

同时 JVM 会记录 Survivor 区的对象一共被来回复制了几次,如果一个对象被复制的次数为 15 (对应虚拟机参数 -XX:+MaxTenuringThreshold ),这个对象就会被晋升( promote )到老年代

那么在发生 Minor GC 时,采用哪种垃圾回收方式会比较好一些呢?采用复制方式,也就是 标记-复制 算法会好一些。为什么呢?因为在新生代中,大部分的 Java 对象只存活一小段时间,那么我们就可以采用耗时比较短的垃圾回收算法,让大部分的垃圾都能在新生代被回收掉。使用 标记-复制 算法的话,理想情况下就是 Eden 区中的对象基本都死亡了,那么需要复制的数据非常少,此时这种算法的优势就被极大的体现了出来

【编辑推荐】

  1. jvm类加载器,类加载机制详解,看这一篇就够了
  2. JVM(Java Virtual Machine)源码分析-类加载场景实例分析
  3. 调优 | 别再说你不会 JVM 性能监控和调优了
  4. JVM真香系列:Java文件到.Class文件
  5. 10个经典又容易被人疏忽的JVM面试题
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

30人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

194人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微