JVM笔记 -- 垃圾回收机制

 

 

目录

对象存活判定算法

引用计数算法

可达性分析算法

方法区的回收

垃圾收集算法(GC算法)

分代收集算法(Generational Collection)

标记 - 清除算法

复制算法

标记 - 整理算法

HotSpot的算法实现

对象引用


随着程序的运行,内存中存在的实例对象、变量等信息占据的内存越来越多,如果不及时进行内存回收,势必会带来程序性能的下降及因为内存不存导致的不必要的系统异常。

在之前介绍的jvm内存结构的五个区中,程序计数器、JVM栈、本地方法栈是不需要进行GC(垃圾回收 Garbage Collection)的。因为它们的生命周期和线程同步,随着线程的销毁,它们占用的内存会自动释放。所有只有方法区和堆需要进行GC。那么什么时候需要GC呢,那么就需要了解JVM中的对象存活判定算法了。

对象存活判定算法

在堆里面存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象哪些还存活着,哪些已经死去。

引用计数算法

给对象中添加一个引用计数器,每当一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器为0的的对象就是不可能再被使用的。

引用计数算法的实现简单,判定效率也高,但难以解决对象间的循环引用问题。如:对象A中含有B的引用,对象B中含有A的引用,这样即使程序中不存在对象A、B的引用了,计数器人不为1,即无法进行垃圾回收。

可达性分析算法

主流商用程序语言的主流判定对象存活的算法实现。这个算法的基本思路就是通过一系列的称"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径车位引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图,对象object5、object6、object7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们被判断为可回收。

在java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

方法区的回收

之前提到过,需要进行GC的区只有堆和方法区,堆的回收是依靠对象存活判定算法的,那么方法区的GC又是由什么决定呢?

我们都知道,方法区是用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。其中,方法区的GC主要为两部分内容: 废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串‘abc’已经加入了常量池中,但是没有任何String对象引用常量池中的‘abc’常量,也没有其他地方引用这个字母量,如果这时发生内存回收,而且必要的话,这个‘abc'常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判断一个常量是否是“废弃常量”比较简单,而判定一个类是否是“无用的类”的条件则相对苛刻许多。类需同时满足下面3个条件才能算是无用的类:

  • 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

只有同时满足上面三个条件的类才可以被虚拟机回收,而是否会被回收,由相关参数所控制。

垃圾收集算法(GC算法)

分代收集算法(Generational Collection)

当前的商用虚拟机都采用分代收集算法, 这种算法的思想是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记整理算法来进行回收。

标记 - 清除算法

标记-清除算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后同一回收所有标记的对象。

它主要不足有两个:

  • 效率问题,标记和清除两个过程效率都不高;
  • 空间问题,标记清除后会产生大量不连续的内存碎片(可能导致分配较大对象时找不到连续内存而不得不提前触发另一个垃圾收集动作)

复制算法

将可用内存分为大小相等的两块,每次只用其中一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

复制算法使得每次都是对整个半区进行内存回收,内存分配时再也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。代价是将内存缩小为原来的一半,代价过高。

现在的商业虚拟机都采用这种收集算法来回收新生代。因为新生代中的对象98%是"朝生夕死"的,所以不需要1:1分配。只需分为一块较大的Eden空间和两块较小的Survivor空间即可(复制过程参考上文中Eden空间和Survivor空间的描述)。当然,当Survivor空间不够时,需要依赖老年代进行分配担保

标记 - 整理算法

标记过程与"标记 - 清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象移向一端,然后直接清理掉端边界以外的内存。该算法用于老年代。

 

HotSpot的算法实现

HotSpot实现上述对象存活判定算法和垃圾收集算法时需要严格的考量,才能保证虚拟机的高效运行。在此,不展开说明,仅列举一些要点

枚举根节点:在安全点使用一组OopMap的数据结构记录哪些位置是对象的引用。

安全点:在安全点才能停顿进行GC,安全点是以"是否具有让程序长时间执行的特征"为标准选取的。如:方法调用、循环跳转、异常跳转等。

在GC发生时需要让所有线程都跑到最近的安全点上再停顿下来,有两种方案可供选择

  • 抢先式中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机采用这种方案。
  • 主动式中断:不对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点的地方是重合的。

安全区域:指在一段代码片中,引用关系不会发生变化,在这个区域中任意地方开始CG都是安全的。可看作被拓展的Safepoint,线程执行到Safe Region时,会标识自己进入Safe Region,这样,GC发生时,虚拟机就不用管为自己标识了Safe Region状态的线程了。

对象引用

在前面对象存活判定中,对象是否存活都与引用有关。那么引用到底是什么?

JDK 1.2之后,Java将引用分为强引用 (Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种。

  • 强引用:类似"Object obj = new Object()"这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:用来描述一些还有用但并非必须的对象(缓存等)。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
  • 弱引用:也是用来描述非必需对象,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前
  • 虚引用:也被称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

 

参考:

深入理解Java虚拟机

Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值