BiBi - JVM -3- 垃圾收集算法

From:深入理解Java虚拟机

  • 目录
    BiBi - JVM -0- 开篇
    BiBi - JVM -1- Java内存区域
    BiBi - JVM -2- 对象
    BiBi - JVM -3- 垃圾收集算法
    BiBi - JVM -4- HotSpot JVM
    BiBi - JVM -5- 垃圾回收器
    BiBi - JVM -6- 回收策略
    BiBi - JVM -7- Java类文件结构
    BiBi - JVM -8- 类加载机制
    BiBi - JVM -9- 类加载器
    BiBi - JVM -10- 虚拟机字节码
    BiBi - JVM -11- 编译期优化
    BiBi - JVM -12- 运行期优化
    BiBi - JVM -13- 并发

程序计数器、虚拟机栈、本地方法栈这3部分区域随线程而生而灭,每一个栈桢中分配多少内存基本是在编译期间就可知的,他们的分配和回收具有确定性,不需要考虑回收的问题。而Java堆和方法区不一样,一个方法中的多个分支需要的内存是不一样的,需要程序处于运行期间才能知道要创建哪些对象,这部分内存的分配和回收是动态的。

  • GC需要完成的3个件事情:
    1)哪些内存需要回收?
    2)什么时候回收?
    3)如何回收?

1. 判断对象“存活”的方法

  • 引用计数算法【不好】

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是没有被引用,死亡的对象。
问题:难以解决对象之间互相循环引用
例子:对象a、b已经不能再被访问了,但是他们因为互相引用这对方,导致他们的引用计数都不为0。所以,主流的Java虚拟机没有采用这种方法进行内存管理。

  public static void main(String[] args) {
    A a = new A();
    B b = new B();
    a.obj = b;
    b.obj = a;
    a = null;
    b = null;
    System.gc();
  }
  • 可达性分析算法【主流程序语言采用的方法】

通过一系列【GC Roots】对象作为起始点,从这些节点向下搜索,所走过的路径称为【引用链】,当一个对象到GC Roots没有任何引用链相连时,则此对象不可用。

  • Java中可以作为GC Roots的对象包括:
    1)虚拟机栈中引用的对象
    2)本地方法栈中JNI引用的对象
    3)方法区中类静态属性引用的对象
    4)方法区中常量引用的对象

2. finalize()方法

宣告一个对象真正死亡,至少要经历两次标记过程:
第一次:对象进行可达性分析后发现没有与GC Root相连的引用链,进行第一次标记,并且还要进行一次筛选,筛选得条件是该对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过了,虚拟机将这两种情况视为【没有必要执行】
第二次:对象有必要执行finalize()方法,将这个对象放置在F-Queue队列中,并由一个低优先级的Finalizer线程去执行它。finalize()方法是对象逃离死亡命运的最后机会,GC会对F-Queue队列中的对象进行第二次小规模的标记

注意:任何一个对象的finalize()方法只会被系统自动调用一次,所以在finalize()方法中逃脱的对象,只能够逃脱一次。
F-Queue队列的执行,无法保证各个对象的调用顺序。所以,最好不要在finalize()中做任何事情。

3. 回收方法区

永久代的垃圾回收主要分两部分:【废弃的常量】和【无用的类】。回收废弃的常量跟Java堆中对象的回收类似。

  • 无用类的判断条件:
    1)Java堆中不存在该类的任何实例
    2)加载该类的ClassLoader已经被回收
    3)该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类

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

4. 垃圾收集算法

  • 1)标记 - 清除算法

分两个阶段,先标记出需要回收的对象,在标记完成后再统一回收所有标记的对象。
缺陷:标记和清除两个过程效率不高;标记清除后会产生大量的不连续的内存碎片,当程序运行过程中需要分配大对象时,由于无法找到连续的内存空间,而不得不提前触发另一次垃圾回收动作。

  • 2)复制算法【JVM新生代使用的算法】

将内存按容量分为大小相等的两块,每次只使用其中一块。当这块内存用完,就将还存活的对象复制到另外一块上面,然后再把已使用过的这块内存空间一次全部清理掉
优点:实现简单、效率高、没有锁片。
缺陷:内存空闲一半,浪费。

因为【新生代】中的对象98%都是“朝生夕死”【存活率不高】,适合用复制算法来回收,但并不一定按照1:1来划分内存空间。

HotSpot划分比例为Eden:Survivor1:Survivor2 = 8:1:1,每次使用Eden和其中一个Survivor,当回收时,将Eden和Survivor1中存活着的对象一次性复制到Survivor2中,最后清理掉Eden和Survivor1空间。这样新生代只浪费了【10%】的内存空间。
当新生代中回收的空间大于10%,即Survivor2空间不够用时,多的对象将直接通过【分配担保机制】进入到【老年代】。

  • 3)标记 - 整理【JVM老年代使用的算法】

与标记-清除算法一样,只是不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

老年代中对象【存活率高】【没有额外的空间进行担保】,所以必须使用:标记-清理/整理算法,不能使用复制算法进行担保。

你可能感兴趣的:(BiBi - JVM -3- 垃圾收集算法)