垃圾回收器以及内存分配
一.垃圾回收器
1.串行回收器
它分为新生代串行回收器和老年代串行回收器
1.1新生代串行回收器
它有两个特点:单线程,独占式。在实时性要求高的系统,这个是不可接收的.
新生代串行处理器默认使用复制算法。在cpu处理器等硬件不是特别优越的场合,它的性能比并行的好。
1.2老年代串行回收器
它默认使用的是标记压缩算法.它可以和多种新生代回收器配合使用,同时也可以作为cms回收器的备用回收器
若要启用老年代串行回收器,可以下面的参数
-XX:+UseSerialGC:新老年代都用串行回收器
-XX:+UseParNewGC:新生代用ParNew,老年代用串行的.
-XX:+UseParallelGC:新生代用Parallel回收器,老年代用串行的.
2.并行回收器
2.1新生代ParNew回收器
它知识简单的将串行回收器多线程化,回收策略,算法,参数和串行的回收器一样。
开启的参数:
-XX:+UseParNewGC:新生代用ParNew,老年代用串行的.
-XX:ParallelGCThreads:指定parnew回收器工作时使用的线程数量,一般最好和cpu数量相同.默认情况,cpu<8时.线程数量等于cpu数量.cpu>8时,线程数量等于3+((5*cpu)/8)
-XX:+UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS.
2.2新生代ParallelGC回收器
它也是复制算法的回收器.它和Parnew的区别在于,它很关注系统的吞吐量.
启用的参数如下:
-XX:+UseParallelGC:新生代用ParallelGC回收器,老年代用串行的.
-XX:+UseParallelOldGC:新生代用ParallelGC,老年代用ParallelGCOld
它提供了两个重要的参数控制吞吐量:
-XX:MaxGCPauseMillis:设置最大垃圾手机停顿时间.它会调整堆大小或者其他一些参数,如果把值设置的很小,虚拟机可能会使用一个小堆,不过这会导致回收频繁,增加回收总时间,降低吞吐.
-XX:GCTimeRatio:设置吞吐量大小(0-100).如果设置为n,那么系统在垃圾收集的时间不会超过(1/1+n)%。默认n=99;
除此之外,它还支持一种自适应的gc调节策略.使用下面的参数打开.
-XX:+UseAdaptiveSizePolicy
在手工调优比较困难的场合,可以用这个方式,让虚拟机自己调优。
2.3老年代ParallelOldGC回收器
和ParallelGC类似,不过它作用于老年代,使用标记压缩算法,在jdk1.6中才可以使用.也可以使用-XX:ParallelGCThreads
3.CMS回收器
Cms主要关注系统停顿时间。CMS(Concurrent Mark Sweep),意为并发标记清除,是使用标记清除算法,也是一个使用多线程并行回收的垃圾回收器.
3.1 CMS主要工作步骤
其中,初始标记和重新标记是独占系统资源的。默认情况下,并发标记之后会有一个预清理(-XX:-CMSPrecleaningEnabled:不进行预清理).
如果新生代GC和重新标记接连触发,会导致停顿时间太长.预清除可以根据历史数据预测下一次新生代GC的时间,然后让重新标记发生在当前时间和预测时间的中间节点.
3.2 CMS主要参数
-XX:+UseConcMarkSweepGC:启动CMS回收器
-XX:ConcGCThreads -XX:ParallelCMSThreads:手工设定并发线程数量。默认并发线程数量是ParallelGCThreads+3 /4.ParallelGCThreads表示GC并行时使用的线程数量。
CPU资源紧张时,收到CMS回收器线程的影响,应用系统的性能可能很糟糕.
(PS:并发是指收集器和应用线程交替执行,并行是应用程序停止,多个线程一起GC。并行的回收器不是并发的.)
它不会等到堆能存饱和时候才进行垃圾回收,而是达到一个阀值的时候就开始回收,以确保CMS工作过程中,依然有足够的空间支持应用程序运行.
-XX:CMSInitiatingOccupancyFrction:设置阀值,默认68.当老年代的空间使用率达到68%时,就会执行一次CMS回收。如果应用的内存增长率很快,执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,虚拟机机会启用串行收集器进行垃圾回收。
因为它使用的是标记清除算法,所以会产生垃圾碎片.这个对于系统性能是相当不利的.下面几个参数可以解决这个问题.
-XX:+UseCMSCompactAtFullCollection开关可以使CMS在垃圾手机完成后,进行一次碎片整理,碎片整理不是并发进行的.
-XX:CMSFullGCsBeforeCompaction:可以用于设定进行多少次CMS回收后,进行一次内存压缩.
3.3 CMS日志分析
如果在日志中发现(concurrent mode failure),说明CMS回收器并发收集失败.可能是中老年代空间不足.如果频繁出现这个提示,就应该预留一个较大的老年代空间.
3.4Class的回收
如果需要回收Perm区,默认情况下,要触发一次Full GC.如果希望CMS回收Perm,需要打开-XX:+CMSClassUnloadingEnabled.
4.G1回收器
G1回收器是在jdk1.7中使用的全新垃圾回收器.从分代上看,G1依然属于分代垃圾回收器,它会区分年轻和老年代,依然有eden和survivor,但是从堆的结构上来看,它不要求整个eden,年轻代老年代 都连续.
它有如下特点:
并行:多个GC线程同时工作.
并发:可以和应用程序同时执行.
分代GC:它可以同时用于年轻代和老年代.
空间整理:它每次回收都会进行有效的复制.
可以预见性:由于分区的原因,可以每次只选取部分区域进行内存回收,这样缩小了回收的范围.
4.1 G1内存划分和收集过程.
G1把堆内存分区,每次只收集几个分区.
G1收集过程有4个阶段 :
新生代GC;并发标记周期;混合收集;如果需要会进行FULLGC
4.2 G1的新生代GC
新生代GC主要回收eden和survivor.一旦eden区被占满,新生代GC就会启动,
下图所示,E,S,O分别代表eden,survivor,老年代.
G1的并发阶段和CMS有点类似.
初始标记:标志根结点可达对象,这个过程会伴随一次新生代GC产生全局停顿。
根区域扫描:不能被GC打断.
并发标记:扫描整个堆空间,并做好标记.
重新标记:对上一次的标记结果进行修正,G1在这个过程中使用SATB算法,类似快照,加速标记过程.
独占清理:这个过程会引起停顿.
并发清理:清理需要清理的区域.
4.4混合回收
在并发标记周期中,回收的比例不会高.但是,G1已经知道那些区域有比较多的垃圾,混合回收阶段就会专门针对这些区域回收。
混合GC会执行多次,直到回收了足够多的内存,然后会触发一次新生代GC。之后又可能会发生一次并发标记周期的处理,最后又会引起混合GC.
在某些特别繁忙的场合会出现在回收过程中内存不足的情况.遇到这种情况,就会发生FullGC
二 .system.gc对于虚拟机参数的影响
-XX:+DisableExplicitGC:禁用System.gc
System.gc默认会忽略CMS和G1,加上-XX:ExplicitGCInvokesConcurrent可以改变这个默认行为.
使用串行回收器调用system.gc,只会发生full gc.
并行GC会先触发新生代GC,再发生full gc;这样做是避免所有回收工作同时交给一次Full GC进行,缩短停顿时间.
三. 对象内存分配
1.对象何时进入老年代
-Xms50m -Xmx50m -XX:+PrintGCDetails
public static void main(String[] args) {
for(int i =0 ;i< 5*1024 ;i++){
//一次分配1kb,总共分配5m
byte[] b = new byte[1024];
}
}
可以观察到只有eden的空间被使用了.那么新生代对象普通情况下什么时候会进入到老年代呢.新生代对象每经历一次GC,年龄就+1,MaxTenuringThreshold这个参数用来设置新生代对象的最大年龄. 默认是15.但是也有可能在小于15次GC的时候晋升到老年代,这个取决于survivor区GC后的使用率,默认50.
如果优先达到这个使用率,那么就自动晋升,如果一直没有达到这个使用率,那么在age等于15的时候晋升.
使用参数:
-XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC
-Xms100M -Xmx100M -Xmn24m -XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=6 -XX:TargetSurvivorRatio=99
如果不使用TargetSurvivorRatio,那么有可能在age=2的时候就被晋升到老年代.这里分配了5m不会被回收的空间,新生代为24m,ratio=2,所有eden=12M,from=to=6M,足够存放5m的空间,使用率5/6=83.3%<99%.所以肯定会触发MaxTenuringThreshold这个条件.有兴趣的同学可以自己去调整TargetSurvivorRatio这个参数测试.
static Map<Integer,byte[]> map = new HashMap<Integer,byte[]>();
public static void main(String[] args) {
//分配5m内存
for(int i =0 ;i< 5*1024 ;i++){
map.put(i, newbyte[1024]);
}
//分配100 m空间
for(int j = 0 ;j< 100 ;j++){
byte[] b = newbyte[1024*1024];
}
}
2.大对象进入老年代
如果对象的体积很大,survivor无法容纳,那么会直接晋升到老年代.
-XX:PretenureSizeThreshold可以设置这个默认值(单位字节).默认为0.这参数只对串行和parnew有效,对ParallelGC无效.
-XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=500 -Xms32M -Xmx32M
static Map<Integer,byte[]> map = new HashMap<Integer,byte[]>();
public static void main(String[] args) {
//分配5m内存
for(int i =0 ;i< 5*1024 ;i++){
map.put(i, newbyte[1024]);
}
}
理论上所有的字节都应该被分配到老年代,但是实际情况却是如下所示.
Heap def new generation total 9792K, used 6474K [0x00000007be000000, 0x00000007beaa0000, 0x00000007beaa0000) eden space 8704K, 74% used [0x00000007be000000, 0x00000007be6528d0, 0x00000007be880000) from space 1088K, 0% used [0x00000007be880000, 0x00000007be880000, 0x00000007be990000) to space 1088K, 0% used [0x00000007be990000, 0x00000007be990000, 0x00000007beaa0000) tenured generation total 21888K, used 16K [0x00000007beaa0000, 0x00000007c0000000, 0x00000007c0000000) the space 21888K, 0% used [0x00000007beaa0000, 0x00000007beaa4010, 0x00000007beaa4200, 0x00000007c0000000) Metaspace used 2652K, capacity 4486K, committed 4864K, reserved 1056768K class space used 286K, capacity 386K, committed 512K, reserved 1048576K
这是因为虚拟机为线程分配空间时,会优先使用TLAB区域,又可能会在TLAB优先分配失去了晋升老年代的机会.
加上-XX:-UseTLAB就可以了
3.TLAB上分配对象
TLAB全场Thread local allocation buffer,即线程本地分配缓存.这是一个线程专用的内存分配区域.它是为了加速对象分配而生的.TLAB本身占用eden空间.默认会为每一个线程分配一块TLAB空间.
分别测试开启与关闭的情况:禁用逃逸分析,防止栈上分配,禁止后台编译
分别用下面两组参数去执行,观察执行时间,你会发现,开启Tlab,的时间会大大优于不开启.
-XX:+UseTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis
-XX:-UseTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis
public static void main(String[] args) {
long begin = System.currentTimeMillis();
for(int i =0 ;i< 1000000000 ;i++){
byte[] by = new byte[2];
by[0] = '1';
}
long end = System.currentTimeMillis();
System.out.println("Direct:"+(end-begin));
}
Tlab空间不会太大,如果tlab的空间已经使用了X KB,再分配一个 Y KB的对象时已经放不下了.这时候虚拟机有两种选择:
1.废弃当前tlab2.直接在堆上分配
虚拟机内部会维护一个refill_waste的值 ,当请求对象大于它时,在堆上分配.这个阀值可以使用TLABRefillWasteFraction来调整,它表示比例,默认64,代表用1/64的tlab空间大小作为refill_waste。
默认情况,tlab和refill_waste都会在运行时不断调整.如果想要禁用自动调整-XX:-ResizeTLAB禁用自动调整,-XX:TALBSize:指定tlab大小,如果想要观察tlab打开跟踪参数:-XX:+PrintTLAB
下图是一个简单对象的分配流程
四.finalize()方法对垃圾回收的影响
finalize()是FinalizerThread线程处理的,每一个即将被回收的并且包含有finalize方法的对象都会在正式回收前加入执行队列.该队列为java.lang.ref.ReferenceQueue引用队列,内部实现为链表结构.
FinalReference中Finalizer封装了实际的回收对象,相当于一个强引用,这意味对象又变成一个可达对象.一旦出现性能问题,将导致这些垃圾对象长时间堆积在内存中,可能导致oom.
下面的程序就会导致oom.
public class LongFinalize {
private static class FinalizeF{
private byte[] b = new byte[512];
@Override
protected void finalize() throws Throwable {
try {
System.out.println(Thread.currentThread().getId());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for(int i = 0 ;i < 40000;i++){
FinalizeF f = new FinalizeF();
}
}
}
finailzeThread执行流程:
使用参数-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/zcf1/Downloads/test.dump 把具体的信息生成到dump文件中.然后通过eclipse mat ->open dump file 打开
可以看到Finalizer占用了8.7M.使用系统自带的“Finalizer overview”可以更好的查看Finalizer.
ps:垃圾回收器和新生代老年代的关系