垃圾收集机制
在虚拟机内存模型中:
- 程序计数器,消耗内存可以忽略不计
- 虚拟机栈,在编译期可知需要分配多少内存空间,栈帧入栈分配空间,出栈回收内存。
- 本地方法栈,与虚拟机栈基本一致。
- Java堆,最大的内存区域,对象几乎在运行时才分配内存,创建频繁甚至需要同步分配,所以堆内存自动回收机制特别重要。
- 方法区,同样需要内存回收。
本章了解整个垃圾收集机制
- 了解垃圾收集流程
- 重点掌握虚拟机垃圾收集算法:
- 对象存活判定算法
- 垃圾收集算法
- 虚拟机GC算法实现
- 垃圾收集器
- 对象在GC时如何分配
对象存活判定算法
什么时候发生GC
当分配Java对象内存时,Java堆内存空间不够,且无法扩展时,进行一次GC
GC处理的是Java堆中的对象,哪些对象是需要回收的呢
这就需要对象存活判定算法,目前有两种对象存活判定算法
- 引用计数法
- 可达性分析算法
引用计数法是什么
对象中设置一个引用计数器,每当有一个地方引用它时,计数器值加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对象有哪些
- 虚拟机栈中引用的对象,即栈帧中本地变量表中对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI,即Native方法中引用的对象。
当GC Roots到对象不可达时,对象就一定可以被回收吗
当GC Roots到对象不可达时,对象不一定会被回收,需要经历两次标记。
- 第一次GC Roots 到对象不可达时,对象被标记1次。
- 判断对象是否覆盖了finalize()方法
- 对象覆盖了finalize()方法,判断虚拟机是否执行了finalize方法
- 虚拟机没有执行finalize方法,将对象放入F-Queue队列中,待回收
- 稍后虚拟机自动建立低优先级的Finalizer线程执行F-Queue队列中对象的finalize方法。
- finalize方法中对象与引用链上链接建立关联,即GC Roots到对象可达
- finalize方法中对象没有雨引用链上链接建立关联
- 稍后GC对F-Queue队列中对象进行第二次标记
- 与引用链建立链接的对象,移除待回收集合
- 没有与引用链建立链接的对象,对象可回收
- 稍后虚拟机自动建立低优先级的Finalizer线程执行F-Queue队列中对象的finalize方法。
- 虚拟机已经执行了finalize方法,对象可回收
- 虚拟机没有执行finalize方法,将对象放入F-Queue队列中,待回收
- 对象没有覆盖finalize()方法,对象可回收
- 对象覆盖了finalize()方法,判断虚拟机是否执行了finalize方法
虚拟机自动建立的低优先级的Finalizer线程执行的时间很长怎么办
Finalizer线程中对象finalize方法可能并不会等待方法执行结束,因为finalize方法可能出现死循环等异常情况,导致整个内存回收机制崩溃。
所以只要执行了finalize方法的对象,且没有与引用链建立关联,对象就是可回收的。虽然可能方法没有结束。
对象的finalize方法有点像C++的析构函数呀
是的,finalize方法就是Java对C++做出的妥协。
尽量不要使用这个方法,try-catch-finally可以做的更好。
方法区或永久代也可能被GC吗
方法区或永久代也会被GC, 虽然效率会很低。
方法区或永久代会回收两部分内容:
- 废弃常量
- 无用的类
这里的常量是指什么
这里是指运行时常量池中常量:字面量和符号引用
常量如何判断可回收
- 堆中没有对象引用该字面量或符号引用
- 其他地方也没有引用该字面量或符号引用
jdk1.8移除了永生代,会有什么变化吗
常量被移到堆中,即常量的回收与对象的回收一致了。
所以jdk1.8只负责类回收。
类如何判断可回收
条件苛刻,下面3个条件同时满足才可以回收
- 该类所有实例被回收,即java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
而且是否对类进行回收,虚拟机还提供参数进行控制:-Xnoclassgc, 是否开启类回收。
总结
对象存活判定算法
- 引用计数法
- 原理:对象中设置一个引用计数器,当有地方引用它时,计数器+1,引用失效时,计数器-1,当计数器==0时,对象已死。
- 优点:实现简单,判定效率高
- 缺点:无法解决对象间直接相互循环引用问题。
- 可达性分析算法
- 原理:一系列的
GC Roots
对象作为起始点,向下搜索,走过的路径被称为引用链。当一个对象到GC Roots没有一个引用链相连,或者说GC Roots到对象不可达时,对象已死 - 优点:解决对象间直接相互循环引用的问题
- GC Roots对象:
- 虚拟机栈中引用的对象,即栈帧中本地变量表的引用对象
- 本地方法栈中JNI,即Native方法中对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
对象两次标记判定算法
- 原理:对象的fianlize方法可以第一次标记已死的对象重新存活。
- 步骤:
- 判断对象是否覆盖finalize方法或者判断虚拟机是否已经执行了finalize方法
- 如果没有覆盖finalize方法或已经执行了finalize方法,则对象必死
- 如果对象覆盖了finalize方法且虚拟机没有执行finalize方法,将对象放到F-Queue队列中
- 稍后虚拟机自动建立低优先级的线程,执行F-Queue队列中对象的finalize方法,因虚拟机资源和效率,不一定会等待所有对象的finalize方法执行完成。
- 稍候GC对F-Queue队列中对象进行第二次标记,如果对象在finalize方法中与引用链重新建立链接,即第二次标记存活的对象,移出待回收集合。如果第二次标记还是死亡,则对象必死。
方法区回收判定算法
- 常量判定
- 常量内容:字面量和符号引用
- 判定算法:
- 堆中没有对象引用该字面量或符号引用
- 其他地方没有引用该字面量或符号引用
- 类判定
- 虚拟机启用类回收卸载
- 使用参数:-Xnoclassgc,启用类回收卸载
- 类信息同时满足3个条件判定可卸载
- 堆中没有该类的任何实例对象
- 该类的ClassLoader被卸载
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射机制引用到该类
- 虚拟机启用类回收卸载
垃圾回收算法
垃圾回收算法有哪几种
- 标记清除
- 复制算法
- 标记整理
- 分代收集
什么是标记清除算法
- 标记:标记出所有待回收的对象。
- 清除:标记完成,统一回收被标记对象的内存。
标记清除算法有什么优缺点
- 优点:实现简单
- 缺点:
- 标记和清除的效率都不高。
- 一次GC后会产生大量的不连续的内存碎片,当分配大对象时,可能会无法分配内存而重新发起一次GC
什么是复制算法
- 将堆内存分为大小相等两块区域,每次只用其中一块
- 当使用的那一块内存使用完毕,触发一次GC, 将存活的对象复制到另外一块,已使用过的内存空间一次清理。
- 复制时只移动堆顶指针,按顺序分配内存,不会有大量内存碎片出现
复制算法有什么优缺点
- 优点:解决了标记清除算法的效率问题和内存碎片问题
- 缺点:一次只能用一半的内存空间,空间消耗太大
复制算法如何优化内存空间消耗太大的问题
IBM的研究表明: 98%的对象朝生夕死,熬过一次GC的对象极少,所以并不需要按1:1等比例来划分内存空间。
内存划分:
- Eden空间:占内存80%,数量1
- 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%空间根本不够吧
是的,老年代中对象存活率很高,不适合使用复制算法,所以使用标记整理算法。
什么是标记整理算法
- 标记:对所有可回收的对象进行标记
- 整理:标记完成后,所有存活的对象向一端移动,然后直接清理存活对象边界之外的内存空间。
标记整理算法有什么优缺点
- 优点:解决了标记整理的大量内存碎片的问题,适合老年代。老年代对象存活率高,没有额外空间对它进行分配担保。
- 缺点:不适合新生代。新生代对象存活率低,没有复制算法效率高。
那新生代就用复制算法,老年代就用标记整理算法就好了
是的,这就是分代算法
- 新生代对象存活率低,使用复制算法,只需复制少量对象就完成收集。
- 老年代存活率高,且没有额外空间担保,必须用标记整理算法回收空间。
总结
堆内存分代
- 原因:98%的对象朝生夕死
- 分代:
- 新生代
- 存放对象:对象起始都在新生代分配,每熬过一次GC,对象年龄+1,对象到15岁,移到老年代。
- 复制算法空间划分:使用复制算法对新生代空间划分
- Eden空间:占用新生代80%,1个,使用的空间。
- Survivor空间:占用新生代20%,2个,使用其中之一。
- From空间,占用新生代10%
- To空间,占用新生代10%
- 老年代
垃圾收集算法
- 标记清除
- 原理:标记所有待回收的对象,标记完成,清理所有待回收对象内存空间。
- 优点:实现简单
- 缺点:标记和清理效率不高,而且产生大量内存碎片。
- 复制算法
- 原理:
- 将新生代分为两部分空间,一块Eden空间(占用80%空间)和两块Survivor空间(分别占用10%空间),每次只使用Eden空间和一块Survivor空间,即使用90%空间。
- 发生GC时,将存活的对象复制到另一块空闲的Survivor空间,Eden空间和已使用的Survivor空间一次清理。
- 优点:适合收集新生代朝生夕死的对象,只需复制少量的存活对象完成收集,且不会产生大量内存碎片。
- 缺点:存活对象超过10%时,需要担保进入老年代。
- 原理:
- 标记整理
- 原理:标记所有待回收对象,标记完成,将存活对象移到一端,端边界之外的内存空间一次清理。
- 优点:适合对象存活率高,且无法担保的老年代。也不会产生大量内存碎片。
- 缺点:标记和整理效率不高。
- 分代算法
- 原理:在新生代使用复制算法,只需复制少量对象即可完成收集。在老年代使用标记整理算法,老年代对象存活率高,且无法担保。
想共同学习jvm的可以加我微信:1832162841,或者进QQ群:982523529