java 暂停_Java垃圾回收(一)

bb53dc7c5c6ad1edc832730e4bd9c0bd.png
你算什么垃圾?这可能是最近比较火的话题了. 随着垃圾分类的不断推进,相信城市里的环境会更美好. 今天蹭个热度, 聊聊 Java里的垃圾回收.

Java虚拟机自动内存管理,将原本需要由开发人员手动回收的内存, 交给垃圾回收器来自动回收.既然是自动机制, 肯定没有手动回收那样的精准高效, 而且还会带来不少与垃圾回收实现相关的问题.

引用计数法与可达性分析

垃圾回收,是将已经分配出去的内存,但却不再使用的内存回收回来, 以便能够再次分配.在Java虚拟机中, 垃圾指的是死亡的对象所占据的堆空间.那么问题来了,如何判断一个对象已经死亡了?

我们先说一种古老的辨别方法: 引用计数方法(reference counting). 它的做法是为每个对象都添加一个引用计数器,用来统计指向该对象的引用个数.一旦某个对象引用计数是0,则说明该对象已经死亡,便可以被回收了.具体实现是这样子的:

  1. 如果有一个引用,被赋值为某一对象, 那么将该对象的引用计数器 + 1.
  2. 如果一个指向某一个对象的引用,被赋值为其他值,那么将该对象的引用计数器 -1.
  3. 我们需要获取所有的引用更新操作,并且相应地的增加或者减少目标对象的引用计数器.

引用计数器需要额外的空间来存储计数器, 更新操作比较繁琐,更致命的是无法处理循环引用对象.

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

fcab8feb9ec8e44e5b1214af3b377fbe.png
循环引用

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

这里有个问题,什么是GC Roots呢?

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

  • Java方法栈帧中的局部变量.
  • 已加载类的静态变量.
  • JNI Handles.
  • 已经启动且未停止的Java线程.

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

尽管可达性分析算法本身很简明,但是在实践当中还是有其它问题的需要解决的.

在多线程环境下, 其它线程可能会更新已经访问过的对象中的引用,从而造成误报「将引用设置为null」或者漏报「将引用设置为未被访问过的对象」. 如果误报的话并没有什么问题,Java虚拟机至多损失了部分垃圾回收的机会.如果漏报则比较严重了, 因为垃圾回收器可能回收事实上仍被引用的对象内存.一旦从原引用访问已经被回收了的对象,则很有可能会直接导致Java虚拟机崩溃.

怎么解决这样的问题呢?

在Java虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式--「Stop The World」, 停止其他非垃圾回收线程的工作,直到完成垃圾回收.这就造成了垃圾回收的暂停时间「GC Pause」

Stop The World及安全点

「Stop the World」是通过安全点(safepoint)机制来实现的.当虚拟机收到「Stop The World」请求,它便会等待所有的线程都到达安全点,才允许请求「Stop The World」的线程进行独占的操作.

安全点的初始目的并不是让其它线程停下,而是找到一个稳定的执行状态.在这个执行状态下, Java虚拟机堆栈不会发生变化.这样,垃圾回收器便能够安全的执行可达性分析.

举例说明, 当java程序通过JNI执行本地代码时,如果这段代码不访问Java对象、调用java方法、或者返回至原Java方法,那么Java虚拟机的堆栈不会发生变化,也就代表着这段本地代码可以作为同一个安全点.

只要不离开这个安全点,Java虚拟机便能够在垃圾回收的同时,继续运行这段本地代码.

由于本地代码需要通过JNI的API来完成上述操作, 因此Java虚拟机仅需要在API的入口处进行安全点检测(safepoint poll), 测试是否有其它线程请求停留在安全点里,便可以在必要的时候挂起当前线程.

除了执行JNI本地代码外, Java线程还有其它几种状态:

  • 解释执行字节码
  • 执行即时编译器生成的机器码
  • 线程阻塞

阻塞的线程由于处于Java虚拟机线程调度器的掌握之下,因此属于安全点.

其它几种上状态则是运行状态,需要虚拟机保证在可预见的时间内进入安全点.否则, 垃圾回收线程可能长期处于等待所有线程进入安全点的状态,从而变相地提高了垃圾回收的暂停时间.

对于解释执行来说,字节码与字节码之间皆可作为安全点.当有安全点请求是, 执行一条字节码便进行一次安全点检测.

执行即时编译生成的机器码则比较复杂, 由于这些代码直接运行在底层硬件之上,不受Java虚拟机掌控,因此在生成机器码时, 即时编译需要插入安全点检测, 以避免机器码长时间没有安全点检测的情况,. HotSpot虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环边「back-edge」插入安全点检测.

由于安全点检测本身也有一定的开销和即时编译器生成的机器码打乱了原本栈帧上的对象分布情况这两个原因, 所以不能在每一条机器码或者每一个机器码基本块处插入安全点检测.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值