Java Garbage Collection

垃圾收集机制是Java的核心功能,解放了程序猿手动分配、释放内存的压力,极大的提高了开发效率,垃圾回收最早起源于LISP语言(Lisp还是一种函数式语言, 开创了许多先驱概念, 如动态类型、高阶函数等, 独具一格的是表达式, 现代语言越发展越像Lisp), 之后Smalltalk、python、java等都引入了垃圾收集机制。

Java中常见的垃圾收集器:

引用计数法和可达性分析

垃圾收集,顾名思义,就是将已分配出去的不再使用的内存,再回收,以便再次分配,Java是面向对象的语言,垃圾内存便是指不再使用的对象,那这里的关键就在于:如何分辨一个对象是否存活?

引用计数法: 最开始人们使用引用计数法, 就是为该对象维护一个引用计数器, 来维护该对象的引用次数, 若该对象的引用计数大于0, 说明该对象是存活的, 若等于0, 说明该对象已经死亡, 可以回收了; 当该对象被引用时, 引用计数+1, 如果引用指向其他对象, 则引用计数-1; 但是引用计数法无法解决循环引用的问题, 如a对象和b对象相互引用, 则他们的引用计数都不为0, 但他们和外界的对象都没有引用关系, 应该属于垃圾对象, 此时引用计数法就无法回收这样的对象;

a对象和b对象循环引用

可达性分析法: 为了解决引用计数法的弊端, 现代主流回收器采用的是可达性分析法, 其实质在于将一系列GC Roots作为初始的存活对象合集(live set), 然后从该集合出发, 找出所有能被该集合引用的对象, 并加入到该集合中, 这个过程就称为”标记”(mark), 最后, 未被加入到集合的对象就是可以回收的;

哪些对象可以作为GC Roots呢? 我们可以近似理解为由堆外指向堆内的引用, 包括以下几种:

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

可达性分析法简洁明了, 但是多线程环境下, 判断对象是否可达并非那么容易, 一旦误判存活的对象为死亡对象, 导致回收了仍被引用的对象, 则会导致虚拟机崩溃

Stop-the-world 以及安全点

传统的解决方式采用简单粗暴的方法, 这便是stop-the-world, 停止其他非垃圾回收线程, 等待直到垃圾回收完成, 但不是任何时候都可以进入stop-the-world状态, Java虚拟机中是通过安全点机制来实现的, 当JVM收到stop-the-world请求时, 便会等待所有的线程达到安全点, 才允许stop-the-world线程进入独占工作。

安全点是为了找到一个稳定的执行状态, 在这个执行状态下, Java虚拟机的堆栈不会发生变化, 以便能够安全的执行可达性分析。

垃圾回收算法

  1. copy算法: 即把内存分为两等分, 分别用from和to指针维护, 并且只是用from指针指向的内存区域来分配内存, 当发生垃圾回收时, 便把存活的对象复制到to指针指向的内存区域, 并且交换from和to中的内容, copy算法可以很好的内存碎片问题, 但缺点是堆空间的使用效率低下;

  1. mark-and-sweep算法: 把死亡的对象所占据的内存标记为空闲内存, 并记录在一个空闲列表中, 当新建对象时, 便会从空闲列表中寻找空闲内存, 分配给新的对象, 缺点是容易造成内存碎片;

  1. mark-and-compact算法: 把存活的对象聚集到内存区域的起始位置, 从而留下一段连续的内存, 这可以很好的解决内存碎片的问题, 代价是压缩算法的性能开销;

当然, 实践中会综合使用上述的几种算法, 以规避他们缺点。

0%