垃圾收集器与内存分配策略
栈的内存随着方法的结束和线程结束自动回收,因此Java堆和方法区是垃圾收集器所关注的内存
判断对象是否可以回收
1、 引用计数法:给对象中添加一个引用计数器,当有一个地方被引用时加1,引用失效减1,计数器为0的就是可以回收的,但是会有互相引用的情况
2、可达性分析法 对象到一系列称为GC Roots的对象有没有引用链相连
即使在可达性分析法中不可达的对象,也至少要经历两次标记过程
第一次标记:可达性分析后无与GC Roots相连的引用链
第二次标记:第一次标记后筛选(finalize()方法没有被JVM调用过)后放置在F-Queue队列中,仍无引用链和GC Roots相连则进行第二次标记
方法区的收集:废弃常量和无用的类
废弃的常量:如常量池中的字符串常量“abc”,没有String对象引用常量池的这个“abc”常量,那么abc就是废弃常量可以移除常量池
无用的类:
1、该类的实例都被回收
2、加载该类的ClassLoader已被回收
3、该类的Class对象没有在任何地方被引用,也就是无法通过反射访问该类的方法
一、垃圾收集
垃圾收集算法
1、复制
将内存划分为大小相等的两块,每次只使用其中一块,当其中的一块用完了将其上面存活的对象复制到另一块上面,然后把使用过的内存空间一次清理掉。缺点是将可用内存缩小为了原来的一半,对象存活率较高时不适合使用。
新生代中的对象98%都是朝生夕死的,因此新生代按照8:1:1的比例分为了eden,survivor from 和survivor to空间,每次回收将eden和survivor from中存活的对象复制到survivor to中,不够的话再放到old中,然后将eden,survivor from一次清除掉。
2、标记-清除
首先标记需要回收的对象,在标记完成后统一回收 问题1、效率问题:标记和清除效率都不高 2、空间问题:清除后会产生大量内存碎片,过多的话会导致以后分配大对象如数组找不到一块连续的内存而提前触发一次GC
3、标记-整理
首先标记需要回收的对象,然后将所有存活的对象向一侧移动与将要回收的对象分隔开,然后将要回收的对象一次清理掉,适合用再老年代上。
垃圾手机策略
分代收集
新生代每次垃圾回收都有大量的对象死去少量存活,只需付出少量对象的复制成本即可完成收集。采用复制算法
老年代对象存活率高,没有额外的空间做担保, 只能采用标记-清除或者标记-整理算法
垃圾收集器
新生代垃圾收集器:Serial、ParNew、ParallelScavenge、G1
老年代垃圾收集器:CMS、Serial Old(MSC)、Parallel Old、G1
垃圾收集器的发展,使用户线程的停顿时间在不断缩短,但是仍没办法完全消除,因此寻找更优秀的垃圾收集器仍在继续!
Serial收集器:单线程,采用复制算法,而且进行垃圾收集时,必须暂停JVM其他所有的工作进程,直到它收集结束。仍是Client模式下虚拟机新生代默认收集器
ParNew收集器:Serial的多线程版本,采用复制算法,其他基本相同。是运行在server模式下的虚拟机首选的新生代收集器
Parallel Scavenge收集器:与其他收集器关注点在缩短用户线程停顿时间不同,它关注点是达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行代码时间+垃圾收集时间)如:JVM总运行100分钟,
垃圾收集1分钟,那吞吐量=99%,如果新生代采用了此收集器,那老年代只能使用Serial Old收集器
Serial Old收集器:Serial收集器的老年代版本,同样单线程,采用标记-整理算法,存在意义是给Client客户端JVM使用
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,采用多线程和标记-整理算法
CMS收集器(Concurrent Mark Sweep):一种以获取最短回收停顿时间为目标的收集器,基于标记- 清除算法实现。(只会收集老年代和永久代,1.8后改为元空间(需要设置 CMSClassUnloadingEnabled)),不会收集年轻代
1、初始标记 标记GCRoots直接关联的对象
2、并发标记 往下跟踪标记所有与GCRoots有引用链可达的对象 (可与用户线程同时工作),就是进行GCROOTS Tracing的过程
3、重新标记 修正并发标记期间因用户线程运行而导致的标记变动的一部分对象
4、并发清除 清除未标记的对象 (可与用户线程同时工作)
缺点:
1、虽然在并发阶段可与用户线程同时工作,但是会占用CPU资源,导致应用程序变慢,总吞吐量会降低
2、无法处理浮动垃圾,即在并发清除阶段新产生的垃圾,只有留待下一次GC时再清理掉
3、使用标记-清除算法,会有大量内存碎片产生
G1收集器(Garbage-First):特点:
1、并行与并发:充分利用多CPU,多核环境的硬件优势,来缩短停顿时间,在GC期间可通过并发的方式让Java程序继续执行
2、分代收集:采用不同的算法去收集刚创建的对象,存活了一段时间的对象和熬过多次GC的对象,以获取更好的收集效果
3、空间整合:整体基于标记-整理算法,内部region之间采用复制算法,都不会产生内存空间碎片
4、可预测的停顿:除了追求短时间停顿外,还建立了可预测停顿模型,使在M毫秒内,在垃圾收集上的时间不超过N毫秒
G1逻辑上将整个Java堆划分为多个大小相等的独立区域(region)。仍保留新生代和老年代的概念,但它们之间不是物理隔离了,新生代和老年代都是一部分region的集合了。G1之所以
能建立可预测停顿模型,因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的最优),在后台维护一个优先列表,
每次根据允许的收集时间,优先回收价值最大的region,这也是Garbage-First的由来。这种使用region划分内存空间以及有优先级的区域回收方式保证了G1在有限的时间内可获取尽可能高的收集效率。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,
虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象
引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remember Set即可保
证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
1、初始标记
2、并发标记
3、最终标记
4、筛选回收
二、内存分配与回收策略
Minor GC 新生代GC:一般比较频繁,回收速度也比较快
Major GC/Full GC 老年代GC:调用System.gc() 强制执行GC为Full GC
(Full GC停顿时间比Minor GC高几个量级,一般为50倍以上)
对象优先在Eden区上分配,Eden区上没有足够的空间分配时,触发一次Minor GC(新生代GC),将存活的对象复制进Survivor to区,若Survivor to区没有足够的空间存放,则通过分配担保机制将对象转移到老年代中。
同时,经过一次Minor GC进入到survivor to区的对象,年龄计数器设为1,在Survivor from区的对象每经过一次Minor GC,年龄加1,当年龄增加到 -XX:MaxTenuringThreshold 设定的阀值(默认15)或者在Survivor区中有相
同年龄的所有对象大小总和大于Survivor区大小的一半,那么大于这个年龄的对象,将会被移动到老年代中。
每进行Minor GC之前,在允许担保失败的情况下,JVM将查看老年代中最大可用连续空间是否大于历次minor GC晋升到老年代的对象的平均大小,如果大于,将进行一次Minor GC;如果minor GC后老年代空间不足,
则紧接着触发Full GC,如果小于,则直接触发Full GC。(新生代和老年代的比例默认是1:2)
(HandlePromotionFailure设置是否允许担保失败(默认允许),如果不允许担保失败,那么每次Minor GC前JVM查看老年代中最大的连续空间是否大于新生代所有对象的大小总和,如果小于,则直接触发Full GC)
大的对象(成员变量很多)可能直接进入老年代,避免在Eden区和两个Survivor区之间发生大量的内存复制。典型的大对象是那种很长的字符串对象或者数组。超过 -XX:PretenureSizeThreshold参数配置的大小的对象直接在老年代分配内存。