JVM二.垃圾收集理论知识

博主最近复习深入理解JVM一书,整理归纳,以形成系统认识和方便日后复习。
本文主要介绍

  1. 引用
  2. JVM垃圾回收算法

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人想出来。

一. 概述

垃圾回收意义

电脑的内存是有限的,如果一段程序申请了一块内存空间并执行完计算之后,会导致这块内存被占用,可用内存就变少。如果不释放内存、回收垃圾,电脑内存迟早耗尽。
C语言中有malloc、free等于内存分配以及内存释放的函数。而Java中使用垃圾收集机制来整理内存空间。

垃圾回收怎么做

垃圾收集(Garbage Collection,GC)需要完成的三件事情:

  1. what
    哪些内存需要回收?
  2. when
    什么时候回收?
  3. how
    如何回收?

当要排查各种内存溢出、内存泄漏问题时,当垃圾收集称为系统达到更高并发量的瓶颈时,我们就需要对这些”自动化”的技术实施必要的监控和调节。

发生GC的内存区域---where

  1. 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;每一个栈帧中分配多少内存基本上在类结构确定下来的时候就已知。因此这几个区域的内存分配和回收都具有确定性,不需过多考虑回收问题,方法结束或者线程结束时,内存自然就随之回收了。
  2. Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存!

二. 对象已死吗?---what

垃圾回收器在对堆进行回收前,首要确定的事情就是这些对象之间哪些还存活着,哪些已经死去?
分析方法:引用计数法、可达性分析法

引用计数算法(Reference Counting)

定义

给对象添加一个引用计数器

  • 当一个地方引用它时,计数器值就+1
  • 当引用失效时,计数器值就-1
  • 任何时刻计数器为0的对象就是不可能被再使用的

优点

实现简单,判定效率高

缺点

由于其很难解决对象之间相互循环引用的问题,故主流Java虚拟机里面都没有选用Refrence Couting算法来管理内存

public class ReferenceCountingGC {
     public Object instance = null;
     private static final int _1MB = 1024 * 1024;
     // 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
     private byte[] bigSize = new byte[2 * _1MB];
     public static void testGC() {
         ReferenceCountingGC objA = new ReferenceCountingGC();
         ReferenceCountingGC objB = new ReferenceCountingGC();
         objA.instance = objB;
         objB.instance = objA;
         objA = null;
         objB = null;
         // 假设在这行发生GC,objA和objB是否能被回收?
         System.gc();
     }
}

实际上这两个对象已经不可能再被访问,但是因为它们互相引用着对方,导致它们的引用计数值都不为0,引用计数算法无法通知GC收集器回收它们。

可达性分析算法(Reachability Analysis)

定义

判断对象存活的基本思路:

  1. 通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)
  2. 当一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达)时,则证明此对象是不可用的
GC Roots判断对象存活

GC Roots对象

Java语言中,可作为GC Roots对象包括:

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

引用的类型

无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。
JDk1.2之后,Java对引用概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,4种强度一次逐渐减弱。

1. 强引用(Strong Reference

是指在程序代码之中普遍存在的

  • 只要强引用存在,对象就不会发生GC

  • 例子:

Object obj = new Object();

2. 软引用(Soft Reference

是用来描述一些还有用但并非必须的对象。

  • 对于软引用关联着的对象,在系统将要发生OOM异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出OOM异常。
  • 软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。
  • 例子:使用 SoftReference 类来实现软引用。
Object obj = new Object();
SoftReference sf = new SoftReference(obj);
 
 

3. 弱引用(Weak Reference

  • 是用来描述非必须对象的,强度比软引用更弱,被弱引用关联的对象只能生存到下一次GC发生之前。
  • 当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 例子:使用 WeakReference 类来实现弱引用。
Object obj = new Object();
WeakReference wf = new WeakReference(obj);
 
 

4. 虚引用(Phantom Reference

  • 也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
  • 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
  • 例子:使用 PhantomReference 来实现虚引用。
Object obj = new Object();
PhantomReference pf = new PhantomReference(obj);
 
 

回收方法区

效率

在方法区中进行垃圾收集的性价比一般比较低;而在Heap中,尤其是在新生代,常规应用进行一次垃圾收集一般回收70%~95%的空间,而永久代的垃圾收集效率远低于此

回收什么

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

  1. 回收废弃常量与回收Java堆中的对象类似
  2. 判定一个类是否是无用的类条件相对苛刻:
    2.1 该类所有实例都已被回收,即Java堆中不存在该类的任何实例
    2.2 加载该类的ClassLoader已经被回收
    2.3 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该方法

并且满足了也不一定会被卸载

针对场景

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

finalize()

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。

三. 垃圾收集算法---how

只介绍内存回收的方法论(算法思想及发展过程),不讨论具体算法实现。

1. 标记-清除算法(Mak-Sweep)

定义

MS算法分标记和清除两个阶段:

  1. 标记出所有需要回收的对象
  2. 标记完成后,统一回收所有被标记的对象
Mak-Sweep

不足

  1. 效率问题
    标记和清除两个过程的效率都不高
  2. 空间问题
    标记清除之后会产生大量不连续的内存碎片,空间碎片太多后导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发一次垃圾收集动作

2. 复制算法(Coping)

过程

  1. 将可用内存按容量划分为大小相等的两块,每次使用其中一块
  2. 当这一块的内存用完了,就将还存活的对象复制到另一块上面
  3. 然后再把已使用的内存清理掉
Coping

优缺点

  • 优点
    每次对整个半区进行回收,内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
  • 不足
    提高效率的代价是将内存缩小到原来的一半

对缺点的改进

现代商业虚拟机都采用 复制算法(Coping) 来回收新生代

1.空间分配

  • 新生代中的对象一般98%是朝生夕死,无需按照1:1比例来划分内存空间,而是将内存分为1块较大的Eden(伊甸园)空间和2块较小的Survivor(幸存者)空间,每次使用Eden和其中1块Survivor
  • HotSpot VM默认Eden和Survivor的比例是8:1:1,即只浪费10%的内存

2.具体过程

回收时,将Eden和Survivor中还存活的对象一次性复制到另外一个Survivor空间中,最后清理掉Eden和刚才用过的Survivor空间。

3.分配担保(Handle Promotion)

98%的对象可回收只是一般场景下的数据,无法保证每次回收都只有不多于10%的对象存活,所以当Survivor空间不足时,需要依赖其他内存(老年代)进行`分配担保(Handle Promotion),让对象进入老年代。

3. 标记-整理算法(Mark-Compact)

copying的不足

复制算法在对象存活率较高时复制操作较多,效率会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以对应被使用内存中的所有对象都100%存活的极端情况,所以老年代一般不直接选用这种算法。

定义

根据老年代的特点,提出标记-整理(Mark-Compact)算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理调用端边界以外的内存。

Mark-Compact

对比

标记-清除vs 标记-整理

对比

4. 分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,根据对象存活周期的不同将内存分为几块。

一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法

分代 特点 算法
新生代 每次垃圾回收时都发现有大批对象死去,只有少量对象存活 复制算法,以少量对象复制的成本
老年代 对象存活率高、没有额外空间对其进行分配担保 标记-清理或标记-整理算法来

参考文章
周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社
JVM学习笔记(三)垃圾收集器与内存分配策略
【深入理解JVM】:垃圾收集(GC)概述

你可能感兴趣的:(JVM二.垃圾收集理论知识)