深入理解java虚拟机之垃圾回收算法?CMS垃圾回收的基本流程?对象引用类型?
摘要:本文讲解讲解常见的垃圾收集算法,首先思考三个问题:1、哪些内存需要回收?2、什么时候回收?3、如何回收? 然后讲解内存回收的具体实现-垃圾收集器 ,最后对Java中 对象引用类型及具体使用场景 做了探讨,jvm的自动垃圾回收策略使得程序员摆脱了编程中繁杂的内存管理,可以把精力专注于系统业务。
思考三个问题:1、哪些内存需要回收?2、什么时候回收?3、如何回收?
1、哪些内存需要回收?
- 程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡,无需考虑回收问题。
- JVM中的方法区和堆需要进行内存回收
2、什么时候回收?
1)判断对象是否死亡
-
1、引用计数算法 (废弃)
对象被引用就+1,难以解决循环引用问题 -
2、可达性算法(栈、方法区的引用对象)
1)概念:通过一系列称为 “GC roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC roots没有任何引用链相连时,证明此对象不可用。2)GC roots的对象包括:
1、虚拟机栈中引用的对象
2、本地方法栈 native引用的对象
3、方法区中 类静态属性 引用的对象
4、方法区中常量引用的对象3)引用的分类:
强引用(new),软引用(soft),弱引用(weak),虚引用(Phantom)4)当一个对象不可达GC Roots时,这个对象并不会立马被回收,被真正的回收需要经历两次标记:
- 如果没有GC roots相连接的引用链,他将第一次标记并进行筛选,看是否有必要执行finalize方法;
- 如果有必要执行finalize方法,对象将被放置在F-Queue队列中,虚拟机会触发一个Finalize()线程去执行,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize中拯救自己,只要重新引用链上的任何一个对象建立关联。
2)java垃圾回收时间
- 自动:不需要显示释放对象内存,虚拟机自行执行;
- GC时间:在虚拟机空闲、堆内存不足时触发,低优先级垃圾回收线程;
- GC对象:没任何引用的对象(可达性算法)
3、如何回收?垃圾回收算法 方法论
1)标记-清除算法 直接回收不存活的(老年代)
- 分为标记和清除两个过程,首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 缺点:效率问题:标记和清除两个过程的效率都不高;
空间问题:标记清除后会产生大量不连续的内存碎片
2)复制算法 (新生代)
- 把Eden:From Survivor:To Survivor空间大小设成8:1:1,对象总是在Eden区出生,若Eden区满,触发minor GC,若GC后,存活的对象太多,to survivor内存不够时,通过分配担保机制复制到老年代
3)标记 - 整理算法(老年代)
-
和标记-清除算法类似,在清除对象的时候先将可回收对象移动到一端,然后清除掉端边界以外的对象
优点:1、解决大量内存碎片问题;2、当对象存活率较高时,效率也很好
4)分代收集算法 根据对象存活周期的不同,将内存空间划分为几块
- 1、新生代(复制算法)
- 2、老年代(标记-清除算法,标记-整理算法)
5)内存分配与回收策略 对象的内存分配
-
1、大多数情况下,对象在新生代eden区分配,eden区没有足够的空间进行分配时,虚拟机将发起一次minor gc
-
2、大对象(需要大量连续内存空间的java对象,超出eden区大小)直接进入老年代
-
3、长期存活的对象将进入老年代:(存活轮次多:15)
- jvm采用分代收集的思想管理内存,给每个对象定义了一个年龄计数器。如果对象在eden出生后并经过第一次minor GC后仍然存活,将移动到survivor中,age++,对象在survivor区每经过一次minorGC,age++,age=15时,升到老年代。
-
4、动态对象年龄判断(防止survivor满)
- 如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到maxTenutingThreshold中要求的年龄。
-
5、空间分配担保机制
- 在y gc之前,jvm会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,大于表示安全;如果小于:检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
4、垃圾收集器:
1)垃圾收集器就是内存回收的具体实现
2)HotSpot内存回收算法
-
准确式GC 快速完成GC Roots引用链枚举
引用链枚举时,应用OopMap数据结构,快速完成GC Roots引用链枚举,
OopMap:在类加载完成时,存储 :寄存器和栈的 <偏移量,数据及数据类型>;<k,v> -
安全点检测:程序有长时间执行特征 — 方法调用、循环跳转、异常跳转时
1、仅需在安全点记录OopMap信息;若每条指令生成OopMap,则空间成本太大;
2、程序执行时仅在安全点停下来GC -
多线程的主动式中断,使得各个线程都跑到安全点再停顿
1、在安全点、创建的对象分配内存时,设置一个标志
2、各个线程执行时主动轮询该标志,若为真,则中断挂起 -
安全区域检测:代码中,引用关系不发生变化
1、线程没有分配CPU时间,无法跑到安全点
2、GC时可忽略该标识自己处于安全区域的线程
3、要离开安全区域,需要收到系统已完成引用链枚举的信号 -
HotSpot虚拟机的垃圾回收器
年轻代:serial收集器 parNew parallel scavenge G1
老年代:parallel old serial oid CMS G1
3)垃圾收集器 3类
- 1、serial收集器 (新生代默认收集器)
单线程,在进行垃圾回收时,必须暂停其他所有的工作线程,直至他收集完成
新生代 | 复制算法 |
---|---|
老年代 | 标记-整理算法 |
- 2、parNew收集器: 新生代
是serial收集器的多线程版本,只有它和serial能配合CMS收集器工作,默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数
新生代 | 复制算法 |
---|---|
老年代 | 标记-整理算法 |
-
3、ParNew Scanvenge收集器
达到一个可控制的吞吐量。停顿时间和吞吐量不可能同时调优。
吞吐量:指CPU用于运行用户代码的时间占总时间的比值 -
4、CMS收集器: 老年代
一款以获取 最短回收停顿时间 为目标的收集器,是基于“标记-清除”算法实现的,重视服务的响应速速,希望系统停顿时间最短。
4个步骤 | 特点 |
---|---|
初始标记 | gc-roots能直接关联的对象 stop the world |
并发标记 | 进行gc tracing的过程 |
重新标记 | 修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录 stop the world |
并发清除 | 基于标定结果,直接清除对象 |
缺点 | 详情及解决方案 |
---|---|
对cpu资源非常敏感 | |
吞吐量低 | 低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。 |
cms收集器无法处理浮动垃圾 | 可能出现concurrent mode failure 而导致另一次full gc的产生,解决方法:虚拟机启动后备预案,临时启用serial old收集器来重新进行老年代的垃圾回收 |
cms基于“标记-清除”,会产生大量内存碎片 | 解决方案:开启内存碎片的合并整理过程 |
- 5、G1收集器: /region/
面向服务端应用的垃圾收集器,以获取最短回收停顿时间为目标的收集器 两次停顿
G1的特点 | 详情 |
---|---|
1、并发与并行 | G1能充分利用cpu、多核的硬件优势,使用多个cpu来缩短stop-the-world停顿的时间,部分其他收集器原本需要停顿java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。 |
2、空间整合 | cms基于“标记-清除”,会产生大量内存碎片,G1是基于标记-整理的算法(两个region的数据是基于复制的),不会产生内存空间碎片 |
3、可预测的停顿 | g1能建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集的时间不超过N毫秒 |
实现思路:
- 使用G1收集器时,将堆划分为相等的region,并且能和整个堆中任意的对象发生引用关系,优先回收价值大的region。每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
分为4个步骤 | 详情 |
---|---|
初始标记 | gc-roots能直接关联的对象 stop the world 耗时短 |
并发标记 | 进行gc tracing的过程 |
最终标记 | 修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录 stop the world |
筛选回收(也能并发) | 对region的回收价值和成本进行排序,根据用锁期望的GC停顿时间来制订回收计划 |
- 整体上看是“标记-整理”,局部看是“复制”,不会产生内存碎片
- 总结:
需求 | 使用的收集器 |
---|---|
1.吞吐量优先 | Parallel Scavenge 新生代 复制算法 / parallel Old 老年代 标记-整理 |
2、重视服务响应速度,最短回收停顿时间 | Parallel Scavenge 新生代 复制算法 / CMS 老年代 并发的标记-清除 |
3、面向服务器端应用 | G1收集器 |
5、常用的GC策略,什么时候会触发YGC,什么时候触发FGC? ****
-
GC回收算法:复制回收,标记清除,引用计数
-
谁会被GC,什么时候 GC?
(1) 超出了作用域或引用计数为空的对象;从gc root开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象。
(2) 程序员不能具体控制时间,系统在不可预测的时间调用System.gc()函数的时候;当然可以通过调优,用NewRatio控制新生代和老年代的比例,用MaxTenuringThreshold 控制进入老年代的次数,使得老年代存储空间延迟达到full gc,从而使得计时器引发gc时间延迟OOM,以延长对象生存期。 -
yGC触发条件:
1、新对象生成,且eden空间申请失败,会触发yGC
2、full gc时会伴随yGC -
FullGC触发条件:对整个堆进行整理
1、老年代被写满(尽量避免创建大的对象以及数组)1、通过-XMN虚拟机参数调大新生代的大小
2、通过-xx:maxTenuringThredhold调大对象那个进入老年代的年龄2、system.gc()被显示调用,但是虚拟机不一定真正去执行
3、空间分配担保失败**1、用复制算法的Minor GC需要老年代的内存空间作担保
2、MinorGC触发前,比较老年代的剩余空间和新生代所有对象大小,老年代小,且不允许冒险,则fullgc
3、允许冒险,继续比较对象历次与本次进入老年代的平均大小,若本次大,则FullGC为什么要担保: 每次minorgc,新生代年龄满15的会进入老年代,如果新生代全部对象都一起满15会导致老年代不够放
4、永久代空间不足(JDK 1.7及以前)
在JDK1.7及以前,HotSpot虚拟机中的方法区是用永久代实现的,永久代中存放的为一些Class的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么虚拟机会抛出java.lang.OutOfMemoryError。jdk8之后,由Meta space代替了perm永久代,用于防止永久代溢出,使用了自扩容机制。
5、Concurrent Mode Failure
执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是GC过程中浮动垃圾过多导致暂时性的空间不足),便会报Concurrent Mode Failure错误,并触发Full GC
6、Java中对象引用类型都有哪些/具体使用场景? **
Java中对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用(父类java.lang.ref.Reference),Java的对象是位于heap中的,heap中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚
-
1、强引用
new一个对象就是属于强引用,当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM)使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题
String abc=new String("abc"); //"abc"强引用 SoftReference<String> softRef=new SoftReference<String>(abc);//2 "abc"强引用 WeakReference<String> weakRef = new WeakReference<String>(abc);//3"abc"强引用 abc=null; //4 "abc"软引用 softRef.clear();//5"abc"弱引用
-
2、软引用
如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它
使用场景:软引用可用来实现内存敏感的高速缓存。(有空余内存,就保留缓存,内存不足时会清理掉)
gc回收软引用的过程:1、首先将softRef的referent(abc)设置为null;
2、将heap中的new String(“abc”)对象设置为可结束的
3、当heap中的new String(“abc”)对象的 finalize()方法 被运行而且该对象占用的内存被释放,softRef被添加到它的 ReferenceQueue (引用队列 如果有的话)中。 -
3、弱引用
用于构建一种没有特定约束的关系。如果一个对象只具有弱引用,那该类就是可有可无的对象(使用场景:它同样是很多缓存实现的选择)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期(一旦gc,马上回收)
适用场景2:在静态内部类中,经常会使用弱引用。例如,一个类发送网络请求,承担callback的静态内部类,则常以虚引用的方式来保存外部类(宿主类)的引用,当外部类需要被JVM回收时,不会因为网络请求没有及时回来,导致外部类不能被回收,引起内存泄漏;
使用场景3:ThreadLocal(ThreadLocalMap里面的key为弱引用,使用过后,应该remove,否则容易出现OOM,因为key可能被回收了,但值还在)(千万要注意) -
4、虚引用
应用场景:虚引用主要用来跟踪对象被垃圾回收的活动(对象被finalize或cleaner后,做一些垃圾清理的工作),虚引用必须和引用队列(ReferenceQueue)联合使用(因为get方法只返回null,若不指定引用队列,就没意义了)
引用队列使用的例子:利用引用队列,我们可以在对象处于相应状态时,虚可达,执行后期处理逻辑Object counter = new Object(); ReferenceQueue refQueue = new ReferenceQueue<>();//引用队列 PhantomReference<Object> p = new PhantomReference<>(counter, refQueue); counter = null; System.gc(); try{ //Remove是一个阻塞方法,可以指定timeout,或者选择一直阻塞 Reference<Object> ref = refQueue.remove(1000L); if(ref != null){ //do something } }catch(InterruptedException e) { //Handle it }
-
5、总结
善于利用软引用和弱引用可以有效避免OOM(虚引用也是,使用 java.lang.ref.cleaner 代替 finalize 方法)
例子:假如有一个应用需要读取大量本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存中,有可能造成内存溢出,此时可以使用软引用解决这个问题。(Android中的Glide图片加载框架)
设计思路:用一个HashMap来保存图片的路径和相应图片对象关联的软应用之间的映射关系,内存不足时,JVM会自动回收这些缓存,图片对象所占用的空间,从而有效避免了OOM问题。 -
6、对象可达性状态流转分析
对象生命周期和不同可达性状态,以及不同状态可能的改变关系
对象创建-->队形初始化-->强引用状态<-->软引用(接下来也可指向弱引用和finalize) <-->弱引用(可以指向finalize) -->虚引用-->Unreachable -->finalize 软引用和弱引用都可以通过get方法获取原有对象(这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用)
-
7、诊断JVM引用情况
如果你怀疑应用存在引用(或finalize)导致的回收问题,可以有很多工具或者选项可供选择,比如 HotSpot JVM 自身便提供了明确的选项(PrintReferenceGC)去获取相关信息 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC //可以查看各类引用数量
-
8、Reachability Fence
通过底层API来达到强引用的效果,这就是所谓的设置reachability fence(有些时候,对象本身并没有强引用,但是也许它的部分属性还在被使用,这样就导致诡异的问题),在JDK源码中,reachabilityFence大多使用在Executors或者类似新的HTTP/2客户端代码中,大部分都是异步调用的情况。编程中,可以按照上面这个例子,将需要reachability保障的代码段利用try-filenally包围起来,在finally里明确声明对象强可达finally里明确声明对象强可达(Reference.reachabilityFence(this);) ?
一、有时候生活轻不轻松,就看你选择了走什么样的路!
二、拥有资源的多少并不重要,如果你不懂得利用,永远都是不够的。
三、向你伸出手的人,不一定都真心想救你!
四、你永远无法满足所有人!
五、别放弃,再坚持一下就到成功彼岸!
六、生活要懂得苦中作乐!
七、方向不对,越努力越窘迫!
八、不要墨守成规,敢于创新才能打败对手!
九、也许有一天,你发觉日子特别的艰难,那可能是这次的收获特别的巨大!