回收堆区
对象已死? (不再被任何途径引用的对象,不需要继续存活,可被回收)
1,引用计数法 (缺点:很难解决对象之间相互循环引用的问题)
2,可达性分析算法 作为GCroot的对象 可以是VMStack(栈帧中本地变量表)中引用的对象,方法区静态属性引用的 对象,方法区 常量引用的对象,本地方法栈中JNI引用对象
关于引用:强引用,软引用,弱引用,虚引用phantom Reference
任何对象的finalize()只能被系统自动调用一次,如果对象面临下一次回收,他的这个方法不会再次执行(系统自动给他 finalize()
回收方法区
主要回收:
1,废弃常量,(常量池中得类,方法,字段的符号引用也是如此,这些常量没地方再引用了,可以被清理)
2,无用的类(a,该类已无任何实例(均被回收);b,加载该类的ClassLoader已被回收;c,该类对应的java.lang.Class对 象没有在任何地方被引用,无法通过反射访问该类)
在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都 需要虚拟机具备类卸载功能,以保证永久代不会溢出
java堆示意图
垃圾回收算法
1,标记-清除算法(Mark-Sweep) 效率不高,另,控件碎片太多导致在程序运行过程中需要分配较大对象时,无法找到足 够的连续内存而不得不提前触发另一次GC
2,复制算法: 整个内存分半,GC后将存活的对象复制到另一块儿。简单,高效。缺点:内存缩小了一半
现在商业VM用这种算法回收新生代:因为新生古代对象98%都是“朝生夕死”,所以no need 按1:1划分内存空间,而 是将内存分为一快较大的Eden空间和两个较小的Survivor空间,每次使用Eden和其中一个Suervivor。
回收时,Eden和Survivor中存活的对象一次性复制到另一个Survivor空间上,再清空Eden和刚刚的Survivor空间。
Hotspot虚拟机默认Eden和Survivor比例是8:1。当survivor空间不够时,需要依赖其他内存(老年代)进行分配担 保(Handle Promotion)
缺点:对象存活率较高时,复制操作多,效率低。还需要分配担保。所以老年代不适用这种算法
3,标记-整理算法(Mark-compact):主要针对对象存活率高,没有格外空间担保,可以用此算法。标记-清除一样,只是 让所有存活对象都向一端移动,然后清理掉端边界以外的内存。老年代用此算法
4,分代收集算法(Generational Collection)。就是将不同代的内存空间采取不同算法。
Hotspot的算法
枚举根节点:
why:1,作为GC Root的节点,主要在 全局性的引用(常量或者类静态属性)与执行上下文(栈帧中得本地变量表) 中。 现在的应用仅仅方法区就有数百兆,逐个检查引用,会消耗很多时间。
2,整个分析期间整个执行系统看起来就像冻结在某个时间点,不能分析的时候对象引用关系还在不停地变化。这就 导致GC进行时必须丁顿所有的执行线程。即使在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也 必须停顿。
how:HotSpot中,使用一组称为OopMap的数据结构来达到这个目的(不用遍历,直接得知哪些地方存放对象引 用)。类加载完时,就把对象内什么偏移量上是什么类型的数据计算出来。在JIT编译过程中,在特定位置记 录下栈和寄存器中那些位置是引用。(此处,建议看《深入JAVA 虚拟机》第73页 Oop Ordinary Object pointer,73页有具体的例子)
result: 在OopMap的协助下,HotSpot可以快速且准确的完成GC Root枚举。但是问题也来了,OopMap内容变化的指 令非常多,可能导致引用关系变化。若每一条指令都生成对应的OopMap,name将会需要大量的额外空间。
安全点: 事实上,前面提到,只在特定位置上记录这些信息,这些位置就是“安全点”(Safepoint),即,程序执行时并非在 所有地方都能停顿下来开始GC,只有达到安全点是才能暂停。所以“安全点”的选定标准是:是否可以让程序长时 间执行。
那么,GC时如何让所有线程都“跑”到最近的安全点上再停顿?策略有二:
抢先式中断(Preemptive)
主动式中断(Voluntary Suspension)
安全区域:是指在一段代码片段中,引用关系不发生变化。在这个Region中任何地方开始GC都是安全的。
垃圾收集器:
1,Serial收集器
它进行GC时,必须暂停其他所有工作线程。到如今,它依然是运行在Client模式下默认新生代收集器
优点,简单高效,对于限定单个CPU环境来说,它没有线程交互的开销。
2,ParNew收集器
其实是Serial收集器的多线程版本,是运行在Server模式下的虚拟机中首选的新生代收集器
目前只有它能与CMS收集器配合工作
3,Parallel Scavenge收集器 (又称吞吐量优先收集器)
是一个新生代收集器,使用复制算法,并行多线程收集器。
CMS等收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间。
Parallel Scavenge收集器目标则是达到一个可控制的吞吐量(Throughput):用户代码运行时间a\(a+GC时间b)
停顿时间越短,越适合需要与用户交互的程序
吞吐量则可以高效利用CPU时间,适合在后台运算而不需要交互的任务
通过两个参数来精确控制吞吐量
1,-XX:MaxGCPauseMillis参数 最大垃圾收集停顿时间 收集器尽可能保证内存回收时间不超过设定值
2,-XX:GCTimeRatio参数 吞吐量大小的参数
(并不是把PauseMillis设置的越小,就可以使得垃圾收集速度更快{值必须大于0的毫秒数},GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快,这将导致垃圾收集发生的更频繁一些。原来10秒收集一次,每次停顿100毫秒。现在变成5秒收集一次,每次停顿70毫秒,停顿时间的确在下降,但是吞吐量也降下来了)
(GCTimeRatio参数值应当大于0,小于100的整数。比如设置为19。那么允许的GC时间就占总时间的5%(1/(1+19)),默认值是99,就是允许1%(1/(1+99)))
4,Serial Old收集器
Serial收集器的老年代版本,单线程收集器,“标记-整理”算法
意义:a,给Client模式下虚拟机使用
b,作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
5,Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理“算法
之前如果新生代用了Parallel Scavenge收集器,老年代只有Serial Old收集器与他配合。而Serial Old收集器在服务端的性能上比较“拖累”,因其无法充分利用服务器的多CPU处理能力。直到Parallel Old出现。所以:
在注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scavenge + Parallel Old 组合
6,CMS收集器
针对老年代;
基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
以获取最短回收停顿时间为目标;
并发收集、低停顿;
需要更多的内存(看后面的缺点);
是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
四个步骤:
A)、初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象;
速度很快;
但需要"Stop The World";
(B)、并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;
(C)、重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
(D)、并发清除(CMS concurrent sweep)
回收所有的垃圾对象;
缺点有三:
(A)、对CPU资源非常敏感:(占用一部分线程会导致程序变慢,总吞吐量降低。CMS默认启动的回收线程总数为CPU数 量+3/4.当cpu数量大于4是,并发GC线程不少于25%的CPU资源,且随着CPU增加而下降,但是如果不足4个,比如 2个,就要分出一半的运算能力(1个cpu)执行收集器线程。)
(B)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"而导致另一次Full GC 由于CMS并发清理阶段,用户线 程还在运行并产生新的垃圾,CMS当次无法处理,只能等下次GC再清理。这部分就成为浮动垃圾。另 CMS收集器不 能像其他收集器那样等到老年代几乎完全填满了再收集,它必须预留一部分空间提供给并发收集时的程序运作使用。 若预留的不够,就会出现Concurrent Mode Failure,此时启动后预案:临时启用Serial Old收集器重新进行老年代垃圾 收集,这样停顿时间就长了。
(C)、产生大量内存碎片
解决方法:
(1)、"-XX:+UseCMSCompactAtFullCollection"
使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
但合并整理过程无法并发,停顿时间会变长;
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
(2)、"-XX:+CMSFullGCsBeforeCompaction"
设置执行多少次不压缩的Full GC后,来一次压缩整理;
为减少合并整理过程的停顿时间;
默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
内存分配与回收策略
内存管理主要解决俩问题:一:给对象分配内存;二:回收分配给对象的内存。
总结:内存分配主要是在堆上分配(也可能经过JIT编译后备拆散为标量类型并间接地栈上分配)。对象主要分配在新生代Eden区,若启动本地线程分配缓冲,将按线程有现在TLAB上分配。少数情况下直接分配在老年代。分配规则不固定,取决于使用的是哪一种垃圾收集器组合,还有虚拟机于内存相关参数设置。
对象优先在新生代Eden区分配
当Eden区没有足够空间分配时,虚拟即将发起一次MinorGC。GC后把存活的对象存到Survivor中。如果Survivor 中内存不够,则通过分配担保机制转移到老年代去。(复制过去,内存复制过程)
新生代GC(MinorGC),发生在新生代的来及收集动作。会比较频繁,因这里的对象大多朝生夕死。所以一般回 收速度也较快。
老年代GC(MajiorGC/Full GC), 发生在老年代的GC,出现了MajorGC,经常会伴随至少一次MinorGC(不 绝对)。另MajorGC一般比MinorGc慢10倍以上
(此处,建议看《深入JAVA 虚拟机》第92页)
大对象直接进入老年代
大对象:是指大量连续内存空间的java对象,典型的就是那种很长的字符串以及数组。
长期存活的对象将进入老年代
若对象在Eden去出生,并经过一次MinorGC后仍村卓,并被Survivor容纳的话,将被移动到Survivor空间中,对象年龄计数器加1,对象年龄设置为1。 在Survivor中每熬过一次MinorGC年龄就加1,当增加到一定程度时(默认是15岁,通过-XX:MaxTenuringThreshold 年龄阈值 来设置),将会被晋升到老年代中。
空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFail 设置不允许冒险,那这时也要改为进行一次Full GC。
下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC 后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Suvivor 无法容纳的对象直接进人老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC(Stop the World) 来让老年代腾出更多空间。取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC 存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure )。如果出现了HandlePromotionFailure 失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免FullGC过于频繁。