Java虚拟机垃圾收集机制

垃圾收集机制

在虚拟机内存模型中:

  1. 程序计数器,消耗内存可以忽略不计
  2. 虚拟机栈,在编译期可知需要分配多少内存空间,栈帧入栈分配空间,出栈回收内存。
  3. 本地方法栈,与虚拟机栈基本一致。
  4. Java堆,最大的内存区域,对象几乎在运行时才分配内存,创建频繁甚至需要同步分配,所以堆内存自动回收机制特别重要。
  5. 方法区,同样需要内存回收。

本章了解整个垃圾收集机制

  1. 了解垃圾收集流程
  2. 重点掌握虚拟机垃圾收集算法:
    • 对象存活判定算法
    • 垃圾收集算法
  3. 虚拟机GC算法实现
  4. 垃圾收集器
  5. 对象在GC时如何分配

对象存活判定算法

什么时候发生GC

当分配Java对象内存时,Java堆内存空间不够,且无法扩展时,进行一次GC

GC处理的是Java堆中的对象,哪些对象是需要回收的呢

这就需要对象存活判定算法,目前有两种对象存活判定算法

  1. 引用计数法
  2. 可达性分析算法

引用计数法是什么

对象中设置一个引用计数器,每当有一个地方引用它时,计数器值加1。引用失效时,计数器值减1。

计数器值为0的对象就是可被回收的对象。

引用计数法有什么优缺点

  • 优点:实现简单,判定效率高
  • 缺点:无法解决对象直接相互循环引用的问题

什么是对象间的相互循环引用

例如:对象A和对象B都有字段ref, 现在让A.ref=B, B.ref=A

然后手动执行GC, 虽然这两个对象不可能再被访问,但他们之间的相互引用让引用计数器都不等于0,无法被回收。

java虚拟机使用的是引用计数法吗

不是,因为对象直接相互循环引用的问题,java虚拟机使用的是可达性分析算法

什么是可达性分析算法

通过一系列的GC Roots的对象作为起始点,从这些起始点开始向下搜索,搜索走过的路径被称为引用链

当一个对象到GC Roots没有任何应用链相连,则证明对象是不可用的。

也可以说,从GC Roots到对象不可达时,则对象可回收。

一系列的GC Roots对象,GC Roots对象有哪些

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

当GC Roots到对象不可达时,对象就一定可以被回收吗

当GC Roots到对象不可达时,对象不一定会被回收,需要经历两次标记。

  1. 第一次GC Roots 到对象不可达时,对象被标记1次。
  2. 判断对象是否覆盖了finalize()方法
    • 对象覆盖了finalize()方法,判断虚拟机是否执行了finalize方法
      • 虚拟机没有执行finalize方法,将对象放入F-Queue队列中,待回收
        • 稍后虚拟机自动建立低优先级的Finalizer线程执行F-Queue队列中对象的finalize方法。
          • finalize方法中对象与引用链上链接建立关联,即GC Roots到对象可达
          • finalize方法中对象没有雨引用链上链接建立关联
        • 稍后GC对F-Queue队列中对象进行第二次标记
          • 与引用链建立链接的对象,移除待回收集合
          • 没有与引用链建立链接的对象,对象可回收
      • 虚拟机已经执行了finalize方法,对象可回收
    • 对象没有覆盖finalize()方法,对象可回收

虚拟机自动建立的低优先级的Finalizer线程执行的时间很长怎么办

Finalizer线程中对象finalize方法可能并不会等待方法执行结束,因为finalize方法可能出现死循环等异常情况,导致整个内存回收机制崩溃。

所以只要执行了finalize方法的对象,且没有与引用链建立关联,对象就是可回收的。虽然可能方法没有结束。

对象的finalize方法有点像C++的析构函数呀

是的,finalize方法就是Java对C++做出的妥协。
尽量不要使用这个方法,try-catch-finally可以做的更好。

方法区或永久代也可能被GC吗

方法区或永久代也会被GC, 虽然效率会很低。

方法区或永久代会回收两部分内容:

  1. 废弃常量
  2. 无用的类

这里的常量是指什么

这里是指运行时常量池中常量:字面量和符号引用

常量如何判断可回收

  1. 堆中没有对象引用该字面量或符号引用
  2. 其他地方也没有引用该字面量或符号引用

jdk1.8移除了永生代,会有什么变化吗

常量被移到堆中,即常量的回收与对象的回收一致了。
所以jdk1.8只负责类回收。

类如何判断可回收

条件苛刻,下面3个条件同时满足才可以回收

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

而且是否对类进行回收,虚拟机还提供参数进行控制:-Xnoclassgc, 是否开启类回收。

总结

对象存活判定算法

  1. 引用计数法
  • 原理:对象中设置一个引用计数器,当有地方引用它时,计数器+1,引用失效时,计数器-1,当计数器==0时,对象已死。
  • 优点:实现简单,判定效率高
  • 缺点:无法解决对象间直接相互循环引用问题。
  1. 可达性分析算法
  • 原理:一系列的GC Roots对象作为起始点,向下搜索,走过的路径被称为引用链。当一个对象到GC Roots没有一个引用链相连,或者说GC Roots到对象不可达时,对象已死
  • 优点:解决对象间直接相互循环引用的问题
  • GC Roots对象:
    1. 虚拟机栈中引用的对象,即栈帧中本地变量表的引用对象
    2. 本地方法栈中JNI,即Native方法中对象
    3. 方法区中静态变量引用的对象
    4. 方法区中常量引用的对象

对象两次标记判定算法

  • 原理:对象的fianlize方法可以第一次标记已死的对象重新存活。
  • 步骤:
    1. 判断对象是否覆盖finalize方法或者判断虚拟机是否已经执行了finalize方法
    2. 如果没有覆盖finalize方法或已经执行了finalize方法,则对象必死
    3. 如果对象覆盖了finalize方法且虚拟机没有执行finalize方法,将对象放到F-Queue队列中
    4. 稍后虚拟机自动建立低优先级的线程,执行F-Queue队列中对象的finalize方法,因虚拟机资源和效率,不一定会等待所有对象的finalize方法执行完成。
    5. 稍候GC对F-Queue队列中对象进行第二次标记,如果对象在finalize方法中与引用链重新建立链接,即第二次标记存活的对象,移出待回收集合。如果第二次标记还是死亡,则对象必死。

方法区回收判定算法

  1. 常量判定
    • 常量内容:字面量和符号引用
    • 判定算法:
      • 堆中没有对象引用该字面量或符号引用
      • 其他地方没有引用该字面量或符号引用
  2. 类判定
    • 虚拟机启用类回收卸载
      • 使用参数:-Xnoclassgc,启用类回收卸载
    • 类信息同时满足3个条件判定可卸载
      • 堆中没有该类的任何实例对象
      • 该类的ClassLoader被卸载
      • 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射机制引用到该类

垃圾回收算法

垃圾回收算法有哪几种

  1. 标记清除
  2. 复制算法
  3. 标记整理
  4. 分代收集

什么是标记清除算法

  1. 标记:标记出所有待回收的对象。
  2. 清除:标记完成,统一回收被标记对象的内存。

标记清除算法有什么优缺点

  • 优点:实现简单
  • 缺点:
    1. 标记和清除的效率都不高。
    2. 一次GC后会产生大量的不连续的内存碎片,当分配大对象时,可能会无法分配内存而重新发起一次GC

什么是复制算法

  1. 将堆内存分为大小相等两块区域,每次只用其中一块
  2. 当使用的那一块内存使用完毕,触发一次GC, 将存活的对象复制到另外一块,已使用过的内存空间一次清理。
  3. 复制时只移动堆顶指针,按顺序分配内存,不会有大量内存碎片出现

复制算法有什么优缺点

  • 优点:解决了标记清除算法的效率问题和内存碎片问题
  • 缺点:一次只能用一半的内存空间,空间消耗太大

复制算法如何优化内存空间消耗太大的问题

IBM的研究表明: 98%的对象朝生夕死,熬过一次GC的对象极少,所以并不需要按1:1等比例来划分内存空间。

内存划分:

  1. Eden空间:占内存80%,数量1
  2. Survivor空间:占内存10%,数量2
    • From空间
    • To空间

每次使用Eden空间和Survivor空间中一个,即使用90%的空间,剩余10%的空间。
毕竟98%只是一般理论数据,10%的空间足够存放理论上2%的存活对象。

发生GC时,将存活对象(理论上2%)复制到Survivor的另一块空闲空间,Eden空间和已使用的一块Survivor空间回收内存。

98%毕竟是理论值,如果超过10%的对象熬过GC, 特别是大对象,那该怎么办

98%的对象朝生夕死,根据对象生存周期的不同可以将java堆内存分为两种:新生代和老年代

开始内存都在新生代中分配,每熬过一次GC, 对象年龄+1,当对象年龄到15岁时,移到老年代。

很显然,新生代的对象适合用复制算法。但如果10%的空间不够,会用老年代的空间进行担保,进入老年代的空间。

能熬过15次GC的老年代中对象,存活率肯定比较高,用复制算法的10%空间根本不够吧

是的,老年代中对象存活率很高,不适合使用复制算法,所以使用标记整理算法。

什么是标记整理算法

  1. 标记:对所有可回收的对象进行标记
  2. 整理:标记完成后,所有存活的对象向一端移动,然后直接清理存活对象边界之外的内存空间。

标记整理算法有什么优缺点

  • 优点:解决了标记整理的大量内存碎片的问题,适合老年代。老年代对象存活率高,没有额外空间对它进行分配担保。
  • 缺点:不适合新生代。新生代对象存活率低,没有复制算法效率高。

那新生代就用复制算法,老年代就用标记整理算法就好了

是的,这就是分代算法

  1. 新生代对象存活率低,使用复制算法,只需复制少量对象就完成收集。
  2. 老年代存活率高,且没有额外空间担保,必须用标记整理算法回收空间。

总结

堆内存分代

  • 原因:98%的对象朝生夕死
  • 分代:
  1. 新生代
    • 存放对象:对象起始都在新生代分配,每熬过一次GC,对象年龄+1,对象到15岁,移到老年代。
    • 复制算法空间划分:使用复制算法对新生代空间划分
      • Eden空间:占用新生代80%,1个,使用的空间。
      • Survivor空间:占用新生代20%,2个,使用其中之一。
        • From空间,占用新生代10%
        • To空间,占用新生代10%
  2. 老年代

垃圾收集算法

  1. 标记清除
    • 原理:标记所有待回收的对象,标记完成,清理所有待回收对象内存空间。
    • 优点:实现简单
    • 缺点:标记和清理效率不高,而且产生大量内存碎片。
  2. 复制算法
    • 原理:
      • 将新生代分为两部分空间,一块Eden空间(占用80%空间)和两块Survivor空间(分别占用10%空间),每次只使用Eden空间和一块Survivor空间,即使用90%空间。
      • 发生GC时,将存活的对象复制到另一块空闲的Survivor空间,Eden空间和已使用的Survivor空间一次清理。
    • 优点:适合收集新生代朝生夕死的对象,只需复制少量的存活对象完成收集,且不会产生大量内存碎片。
    • 缺点:存活对象超过10%时,需要担保进入老年代。
  3. 标记整理
    • 原理:标记所有待回收对象,标记完成,将存活对象移到一端,端边界之外的内存空间一次清理。
    • 优点:适合对象存活率高,且无法担保的老年代。也不会产生大量内存碎片。
    • 缺点:标记和整理效率不高。
  4. 分代算法
    • 原理:在新生代使用复制算法,只需复制少量对象即可完成收集。在老年代使用标记整理算法,老年代对象存活率高,且无法担保。

想共同学习jvm的可以加我微信:1832162841,或者进QQ群:982523529

你可能感兴趣的:(Java虚拟机垃圾收集机制)