第四篇 垃圾回收器以及内存分配

垃圾回收器以及内存分配

 

 

一.垃圾回收器

 

 

       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,老年代.

 

 

       4.3G1的并发标记周期

         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.废弃当前tlab
      2.直接在堆上分配

     虚拟机内部会维护一个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:垃圾回收器和新生代老年代的关系



 

 

猜你喜欢

转载自zcf9916.iteye.com/blog/2407848
今日推荐