JVM--垃圾回收
哪些内存应该被回收?
JVM垃圾回收主要针对堆区和方法区。在Java堆中,我们不再需要的对象就可以被回收了。例如:在一个方法中新生成了一个Integer对象作为局部变量,当方法结束后,该对象将不会再被使用,是被回收的对象。
如何确定对象可被回收?
引用计数法
当没有引用指向某个对象时,可以认为该对象可被回收。
在引用计数法中,每个对象都记录了指向它的引用的个数,当引用为0时即可被回收;但是引用计数法有个明显的缺陷就是它无法解决循环应用的问题。
可达性分析
可达性分析方法从一系列称为“GC Roots”的对象出发,向下搜索,搜索的路径形成引用链,最终构成一个有向连通图,如果某对象不在该图中,则认为该对象不可达,可以被回收。
通常作为GC Roots的对象:
- 虚拟机栈本地变量表中引用的对象
- 方法区中类静态变量所引用的对象
- 方法区中常量所引用的对象
- JNI本地栈中引用的对象
引用分级
对象是否可被回收,都是通过引用关系来确定的。在某些场景下,单一的引用定义不能很好的利用资源:对于某些既可以回收也可以不回收(非必需,如某些缓存对象)的对象,在内存充足的情况下,可以不回收,在内存不足的情况下,希望回收这些对象。为了充分利用资源,Java定义了四种引用级别:
- 强引用:直接赋值引用均为强引用
- 软引用:用于描述非必需的对象,在内存充足时不回收,在将要OOM之前回收,对应SoftReference类。
- 弱引用:用于描述非必需的对象,一旦指向该对象的强引用不存在了,GC发生时即被回收,对应WeakReference类。
- 虚引用:不影响对象的生命周期,仅用于被回收时能收到系统通知。
方法区中类的回收
满足以下条件的类可以被回收:
- 堆中没有该类的实例
- 加载该类的ClassLoader已被回收
- 该类对应的Class对象未被引用
如何回收?
Mark-Sweep
- 思路:先标记,再清理。标记可以通过可达性分析做到,清理时是原地清理。
- 优缺点:原地清理后磁盘碎片多,生成大对象时容易再次触发GC
Copying
- 思路:使用内存时,每次仅使用一半,GC将存活的对象直接复制到另外一半,然后将原来的一半空间清除。
- 优缺点:整体清除磁盘碎片少,简单高效;但是每次仅用一半内存,资源利用率低
Mark-Compact
- 思路:先标记,再整理。标记阶段与标记-清理算法一样,整理阶段时将存活的对象集中在一块,然后将剩下的内存清理。
- 优缺点:整理阶段相对标记清理算法的清理阶段耗时
比较项 | mark-sweep | copying | mark-compact |
---|---|---|---|
速度 | 中等 | 最快 | 最慢 |
空间 | 少,会积累碎片 | 需要存活对象的两倍大小空间 | 少,不积累碎片 |
时间 | mark正比存活对象,sweep正比堆大小 | 正比存活对象 | mark和compact都正比存活对象 |
移动对象 | 否 | 是 | 是 |
Generational Collection
分代
主要是依据对象的存活周期将内存划分为几个部分,然后根据各个部分的对象存活特点针对性的使用GC算法进行GC。
hotspot将Java堆划分为年轻代和老年代:
年代 | 特点 | 回收算法 | 空间担保 |
---|---|---|---|
年轻代 | 朝生夕死,存活率低 | Copying | 老年代 |
老年代 | 存活率高 | mark-sweep/mark-compact | 无 |
由于复制算法需要额外的内存空间,在年轻代中分了Eden和survivor空间(From与To),分代布局图如下:
晋升
1、给对象分配内存时,分配空间顺序为:栈、TLAB、Eden
2、大对象将直接在老年代中分配内存
3、在Young GC后,存活的对象年龄+1,同时它们和survivor0(survivor1)区的对象被复制到survivor1(survivor0)区,在以下三种情况下,将发生晋升:
- survivor1(survivo0)区空间不足
- 对象的年龄到了一定程度(默认15岁)
- survivor空间中同年龄的对象占到了survivor空间的50%
空间担保
在发生Young GC之前,虚拟机会检查老年代的最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,最坏的情况下,年轻代全部晋升老年代,内存空间也够用。如果不成立,虚拟机在允许担保失败的情况下检查老年代空间是否大于历次新生代晋升时所需的平均空间,如果大于则先进行一次Young GC。如果不允许担保失败或者已经担保失败,将进行Full GC。
hotspot GC实现
定位存活对象
hotspot定位存活对象基于可达性分析的结果。我们以虚拟机栈为GC Roots出发说明hotspot的可达性分析实现原理。
在虚拟机栈上不仅有对象引用,还有各类操作符、方法返回信息等,从栈上出发寻找引用,首先要知道栈上哪些位置是对象引用,然后从这些引用出发,找到堆中的对象再向下搜索的时候,要知道对象数据中在哪些位置是字段引用,才能继续向下展开搜索。
这里关键问题是,栈上的引用位置信息和对象内部的引用位置信息如何获取?
一般可以通过引用自己打上tag的方式或者额外辅助信息的方式,hotspot采用了后者,引入了OopMap:在栈上,通过OopMap可以直接获知哪些位置上是引用,在对象中,通过OopMap可以直接获知哪些位置是字段引用。可以理解OopMap将内存地址与引用类型构建成了键值对的关系,通过读取OopMap就可以直接知道哪些内存地址上是引用,进而进行引用的下一步搜索。
那么OopMap是谁生成的呢?
在堆中的对象自己的OopMap,需要类加载器生成,类加载器持有对象的类型信息,可以获知对象在什么位置上是字段引用,从而为对象生成OopMap;在栈上的OopMap,需要解释器和编译器的支持,程序执行的时候,解释器和编译器知道在栈上或者寄存器里什么位置是引用类型。
在OopMap的支持下,可达性分析的效率就大大提高了,到这一步就是引用图的遍历了。
然而如果为栈上每条指令都生成OopMap的话,将耗费大量的空间和时间,虚拟机选定了一些特定的位置生成OopMap(基本上是以是否具备让程序长时间运行为基准选定的,对象图比较稳定),这些位置被称为safepoint:
- 循环结束
- 异常抛出点
- 方法调用/返回处
也就是说程序需要执行到safepoint,才能进行GC Roots枚举。
在多线程环境下,GC与其他线程之前的配合关系是:
- 对于发起GC时没有跑到safepoint的线程,GC需要等待。
- 对于处于WAITING或者BLOCKED状态的线程,需要safe region(某个区域引用关系都不会变化,拓展版safepoint)的支持:进入safe region后才能GC,GC完成其需要的工作后线程才能出safe region。
定位对象中的一致性问题
垃圾回收必须与其他程序对某一时刻上的对象图保持一致性。
举个例子:如果有对象A、B、C,A独立,B引用了C,垃圾回收器由A扫描到B,此时其他程序将C赋给了A,那么垃圾回收器会认为C不可达,而实际上在其他程序的对象图里,C是可达的。
垃圾回收在确认对象图这一阶段,需要暂停其他程序执行,即Stop The World。
解决了存活对象定位的问题,接下来就是如何回收的问题了。hotspot基于分代回收策略,针对不同的年代实现了不同的垃圾回收器。
hotspot垃圾回收器
hotspot垃圾回收器概览:
hotspot垃圾回收器的演进过程可以说是由单线程到多线程、由多线程到可并发、由不可控到可控的发展过程,垃圾回收系统效率越来越高,越来越可靠,也越来越精细。
CMS
在CMS之前的垃圾回收器,都无法提供与其他程序并发运行,CMS带来了更低的停顿(STW)时间和并发执行的实现。
那么,CMS是如何实现的呢?
分析一下mark-sweep的过程,其中sweep的过程在确定了存活对象的情况下,可以独立运行完成,那这一部分的工作就可以拆分出来并发执行;另外,CMS是针对老年代的,老年代有什么特点?空间大、对象存活率高、一般程序执行过程中变动较小!那么在mark过程中如果直接一步到位将会比较慢,因为需要遍历整个老年代对象图。因此,CMS再次做了拆分,将标记阶段拆分为三个阶段:initial making,concurrent making,remaking。
首先initial making阶段需要STW,然后标记GC Roots能直接关联的老年代对象,这个阶段很快就能完成(毕竟只有少数对象);然后进入concurrent making,这个阶段并发执行,即此时仅在initial making获取的结果的基础上继续标记,其他程序继续执行;在concurrent making阶段,垃圾回收对象图和其他程序对象图会出现不一致的情况(比如有新的新生代的对象引用了老年代的对象、有对象晋升到了老年代等),此时需要再来一次STW,对对象图产生变动的地方进行重新标记—即remking。
由此来看,CMS将mark-sweep过程做了更细化的拆分,尽可能将不需要在STW期间做的工作移出STW而并发执行,减少了STW的时间,提高了响应速度。
CMS的缺点:
- CPU敏感:CMS的响应速度来源于并发,如果CPU只有少量的核心,将无法提供真正的并发性,反而会因为更多的线程切换而降低效率
- 无法处理浮动垃圾:在remking以后就已经确定了本次Old GC会处理的垃圾,sweep阶段是并发执行的,可能会有新的对象晋升老年代,如果预留空间不足将触发SerialOld FullGC,将带来较长的停顿时间。对策是合理设置CMS启动阈值。
- 碎片处理:CMS基于mark-sweep,随着运行时间的增加,碎片将越来越多,最后不得不触发FullGC进行整理。可以根据应用特点调整碎片整理的频率和时机。
G1
相比于CMS在算法执行过程中做拆分,G1在堆内存的布局上做拆分:将原来的堆内存不再严格分为大块的Eden、survivor和Tenured,而是将整个堆拆分为一个个小的Region,如下图:
大致上,在G1下Young GC的工作为将存活的对象复制到另外的Region上并释放原空间;Old GC与CMS类似大致分为initial making,concurrent making,remaing,copying/cleanup几个阶段。
G1收集器将堆拆分为Region来管理,不仅提供了并发、低停顿,而且:
- 利于在Young/Old GC时使用copying算法,不会产生内存碎片
- 利于评估各个垃圾块的价值,在后台维护一个优先列表,根据用户允许的停顿时间,优先收集价值最大的垃圾块,使停顿可控。
以下为Oracle官网上的G1垃圾回收逻辑图:
在G1下Young GC的逻辑图如下:
Old GC 逻辑图:
注:G1的初识标记阶段会伴随着一次Young GC。
注:并发标记阶段如果发现有Region中没有存活对象,则直接将该Region清理掉
http://rednaxelafx.iteye.com/blog/1044951
http://hllvm.group.iteye.com/group/topic/38223#post-248757
《深入理解Java虚拟机》
《hotspot》实战
https://www.jianshu.com/p/2a1b2f17d3e4
http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
https://blog.****.net/qqqqq1993qqqqq/article/details/71882733
https://blog.****.net/fei33423/article/details/70941939