java jvm内存管理/gc策略/参数设置

1. JVM内存管理:深刻垃圾收集器与内存分配策略html

http://www.iteye.com/topic/802638java

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

概述:

  提及垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项技术当作Java语言的伴生产物。事实上GC的历史远远比Java来得久远,在1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期,人们就在思考GC须要完成的3件事情:哪些内存须要回收?何时回收?怎么样回收?

  通过半个世纪的发展,目前的内存分配策略与垃圾回收技术已经至关成熟,一切看起来都进入“自动化”的时代,那为何咱们还要去了解GC和内存分配?答案很简单:当须要排查各类内存溢出、泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,咱们就须要对这些“自动化”的技术有必要的监控、调节手段。

  把时间从1960年拨回如今,回到咱们熟悉的Java语言。本文第一章中介绍了Java内存运行时区域的各个部分,其中程序计数器、VM栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的帧随着方法进入、退出而有条不紊的进行着出栈入栈操做;每个帧中分配多少内存基本上是在Class文件生成时就已知的(可能会由JIT动态晚期编译进行一些优化,但大致上能够认为是编译期可知的),所以这几个区域的内存分配和回收具有很高的肯定性,所以在这几个区域不须要过多考虑回收的问题。而Java堆和方法区(包括运行时常量池)则不同,咱们必须等到程序实际运行期间才能知道会建立哪些对象,这部份内存的分配和回收都是动态的,咱们本文后续讨论中的“内存”分配与回收仅仅指这一部份内存。

对象已死?

  在堆里面存放着Java世界中几乎全部的对象,在回收前首先要肯定这些对象之中哪些还在存活,哪些已经“死去”了,即不可能再被任何途径使用的对象。

引用计数算法(Reference Counting)

  最初的想法,也是不少教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任什么时候刻计数器为0的对象就是不可能再被使用的。

  客观的说,引用计数算法实现简单,断定效率很高,在大部分状况下它都是一个不错的算法,但引用计数算法没法解决对象循环引用的问题。举个简单的例子:对象A和B分别有字段b、a,令A.b=B和B.a=A,除此以外这2个对象再无任何引用,那实际上这2个对象已经不可能再被访问,可是引用计数算法却没法回收他们。

根搜索算法(GC Roots Tracing)

  在实际生产的语言中(Java、C#、甚至包括前面提到的Lisp),都是使用根搜索算法断定对象是否存活。算法基本思路就是经过一系列的称为“GC Roots”的点做为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证实此对象是不可用的。在Java语言中,GC Roots包括:

  1.在VM栈(帧中的本地变量)中的引用
  2.方法区中的静态引用
  3.JNI(即通常说的Native方法)中的引用

生存仍是死亡?

  断定一个对象死亡,至少经历两次标记过程:若是对象在进行根搜索后,发现没有与GC Roots相链接的引用链,那它将会被第一次标记,并在稍后执行他的finalize()方法(若是它有的话)。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这点是必须的,不然一个对象在finalize()方法执行缓慢,甚至有死循环什么的将会很容易致使整个系统崩溃。finalize()方法是对象最后一次逃脱死亡命运的机会,稍后GC将进行第二次规模稍小的标记,若是在finalize()中对象成功拯救本身(只要从新创建到GC Roots的链接便可,譬如把本身赋值到某个引用上),那在第二次标记时它将被移除出“即将回收”的集合,若是对象这时候尚未逃脱,那基本上它就真的离死不远了。

  须要特别说明的是,这里对finalize()方法的描述可能带点悲情的艺术加工,并不表明笔者鼓励你们去使用这个方法来拯救对象。相反,笔者建议你们尽可能避免使用它,这个不是C/C++里面的析构函数,它运行代价高昂,不肯定性大,没法保证各个对象的调用顺序。须要关闭外部资源之类的事情,基本上它能作的使用try-finally能够作的更好。

关于方法区

  方法区即后文提到的永久代,不少人认为永久代是没有GC的,《Java虚拟机规范》中确实说过能够不要求虚拟机在这区实现GC,并且这区GC的“性价比”通常比较低:在堆中,尤为是在新生代,常规应用进行一次GC能够通常能够回收70%~95%的空间,而永久代的GC效率远小于此。虽然VM Spec不要求,但当前生产中的商业JVM都有实现永久代的GC,主要回收两部份内容:废弃常量与无用类。这两点回收思想与Java堆中的对象回收很相似,都是搜索是否存在引用,常量的相对很简单,与对象相似的断定便可。而类的回收则比较苛刻,须要知足下面3个条件:

  1.该类全部的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
  2.加载该类的ClassLoader已经被GC。
  3.该类对应的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方经过反射访问该类的方法。

  是否对类进行回收可以使用-XX:+ClassUnloading参数进行控制,还可使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载、卸载信息。

  在大量使用反射、动态代理、CGLib等bytecode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都须要JVM具有类卸载的支持以保证永久代不会溢出。

垃圾收集算法

  在这节里不打算大量讨论算法实现,只是简单的介绍一下基本思想以及发展过程。最基础的搜集算法是“标记-清除算法”(Mark-Sweep),如它的名字同样,算法分层“标记”和“清除”两个阶段,首先标记出全部须要回收的对象,而后回收全部须要回收的对象,整个过程其实前一节讲对象标记断定的时候已经基本介绍完了。说它是最基础的收集算法缘由是后续的收集算法都是基于这种思路并优化其缺点获得的。它的主要缺点有两个,一是效率问题,标记和清理两个过程效率都不高,二是空间问题,标记清理以后会产生大量不连续的内存碎片,空间碎片太多可能会致使后续使用中没法找到足够的连续内存而提早触发另外一次的垃圾搜集动做。

  为了解决效率问题,一种称为“复制”(Copying)的搜集算法出现,它将可用内存划分为两块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另一块上面,而后就把原来整块内存空间一次过清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存就能够了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免过高了一点。

  如今的商业虚拟机中都是用了这一种收集算法来回收新生代,IBM有专门研究代表新生代中的对象98%是朝生夕死的,因此并不须要按照1:1的比例来划份内存空间,而是将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次过拷贝到另一块survivor空间上,而后清理掉eden和用过的survivor。Sun Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。固然,98%的对象可回收只是通常场景下的数据,咱们没有办法保证每次回收都只有10%之内的对象存活,当survivor空间不够用时,须要依赖其余内存(譬如老年代)进行分配担保(Handle Promotion)。

  复制收集算法在对象存活率高的时候,效率有所降低。更关键的是,若是不想浪费50%的空间,就须要有额外的空间进行分配担保用于应付半区内存中全部对象都100%存活的极端状况,因此在老年代通常不能直接选用这种算法。所以人们提出另一种“标记-整理”(Mark-Compact)算法,标记过程仍然同样,但后续步骤不是进行直接清理,而是令全部存活的对象一端移动,而后直接清理掉这端边界之外的内存。

  当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,这种算法并无什么新的思想出现,只是根据对象不一样的存活周期将内存划分为几块。通常是把Java堆分做新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少许存活,那就选用复制算法只须要付出少许存活对象的复制成本就能够完成收集。

垃圾收集器

  垃圾收集器就是收集算法的具体实现,不一样的虚拟机会提供不一样的垃圾收集器。而且提供参数供用户根据本身的应用特色和要求组合各个年代所使用的收集器。本文讨论的收集器基于Sun Hotspot虚拟机1.6版。

图1.Sun JVM1.6的垃圾收集器


  图1展现了1.6中提供的6种做用于不一样年代的收集器,两个收集器之间存在连线的话就说明它们能够搭配使用。在介绍着些收集器以前,咱们先明确一个观点:没有最好的收集器,也没有万能的收集器,只有最合适的收集器。

1.Serial收集器
  单线程收集器,收集时会暂停全部工做线程(咱们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。

2.ParNew收集器
  ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其他行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸同样。对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。

3.Parallel Scavenge收集器
  Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不一样,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它容许较长时间的STW换取总吞吐量最大化。

4.Serial Old收集器
  Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收集器。

5.Parallel Old收集器
  老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此以前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,由于PS没法与CMS收集器配合工做。

6.CMS(Concurrent Mark Sweep)收集器
  CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(整体GC时间最小),但它能尽量下降GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来讲相当重要,这类应用对于长时间STW通常是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,因此虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。
内存分配与回收策略

  了解GC其中很重要一点就是了解JVM的内存分配策略:即对象在哪里分配和对象何时回收。

  关于对象在哪里分配,往大方向讲,主要就在堆上分配,但也可能通过JIT进行逃逸分析后进行标量替换拆散为原子类型在栈上分配,也可能分配在DirectMemory中(详见本文第一章)。往细节处讲,对象主要分配在新生代eden上,也可能会直接老年代中,分配的细节决定于当前使用的垃圾收集器类型与VM相关参数设置。咱们能够经过下面代码来验证一下Serial收集器(ParNew收集器的规则与之彻底一致)的内存分配和回收的策略。读者看完Serial收集器的分析后,不妨本身根据JVM参数文档写一些程序去实践一下其它几种收集器的分配策略。

清单1:内存分配测试代码
算法

 
  1. public class YoungGenGC {   
  2.   
  3.     private static final int _1MB = 1024 * 1024;   
  4.   
  5.     public static void main(String[] args) {   
  6.         // testAllocation();  
  7.         testHandlePromotion();   
  8.         // testPretenureSizeThreshold();  
  9.         // testTenuringThreshold();  
  10.         // testTenuringThreshold2();  
  11.     }   
  12.   
  13.     /** 
  14.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
  15.      */  
  16.     @SuppressWarnings("unused")   
  17.     public static void testAllocation() {   
  18.         byte[] allocation1, allocation2, allocation3, allocation4;   
  19.         allocation1 = new byte[2 * _1MB];   
  20.         allocation2 = new byte[2 * _1MB];   
  21.         allocation3 = new byte[2 * _1MB];   
  22.         allocation4 = new byte[4 * _1MB];  // 出现一次Minor GC  
  23.     }   
  24.   
  25.     /** 
  26.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
  27.      * -XX:PretenureSizeThreshold=3145728 
  28.      */  
  29.     @SuppressWarnings("unused")   
  30.     public static void testPretenureSizeThreshold() {   
  31.         byte[] allocation;   
  32.         allocation = new byte[4 * _1MB];  //直接分配在老年代中  
  33.     }   
  34.   
  35.     /** 
  36.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 
  37.      * -XX:+PrintTenuringDistribution 
  38.      */  
  39.     @SuppressWarnings("unused")   
  40.     public static void testTenuringThreshold() {   
  41.         byte[] allocation1, allocation2, allocation3;   
  42.         allocation1 = new byte[_1MB / 4];  // 何时进入老年代决定于XX:MaxTenuringThreshold设置  
  43.         allocation2 = new byte[4 * _1MB];   
  44.         allocation3 = new byte[4 * _1MB];   
  45.         allocation3 = null;   
  46.         allocation3 = new byte[4 * _1MB];   
  47.     }   
  48.   
  49.     /** 
  50.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
  51.      * -XX:+PrintTenuringDistribution 
  52.      */  
  53.     @SuppressWarnings("unused")   
  54.     public static void testTenuringThreshold2() {   
  55.         byte[] allocation1, allocation2, allocation3, allocation4;   
  56.         allocation1 = new byte[_1MB / 4];   // allocation1+allocation2大于survivo空间一半  
  57.         allocation2 = new byte[_1MB / 4];     
  58.         allocation3 = new byte[4 * _1MB];   
  59.         allocation4 = new byte[4 * _1MB];   
  60.         allocation4 = null;   
  61.         allocation4 = new byte[4 * _1MB];   
  62.     }   
  63.   
  64.     /** 
  65.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure 
  66.      */  
  67.     @SuppressWarnings("unused")   
  68.     public static void testHandlePromotion() {   
  69.         byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;   
  70.         allocation1 = new byte[2 * _1MB];   
  71.         allocation2 = new byte[2 * _1MB];   
  72.         allocation3 = new byte[2 * _1MB];   
  73.         allocation1 = null;   
  74.         allocation4 = new byte[2 * _1MB];   
  75.         allocation5 = new byte[2 * _1MB];   
  76.         allocation6 = new byte[2 * _1MB];   
  77.         allocation4 = null;   
  78.         allocation5 = null;   
  79.         allocation6 = null;   
  80.         allocation7 = new byte[2 * _1MB];   
  81.     }   
  82. }  



规则一:一般状况下,对象在eden中分配。当eden没法分配时,触发一次Minor GC。

  执行testAllocation()方法后输出了GC日志以及内存分配情况。-Xms20M -Xmx20M -Xmn10M这3个参数肯定了Java堆大小为20M,不可扩展,其中10M分配给新生代,剩下的10M即为老年代。-XX:SurvivorRatio=8决定了新生代中eden与survivor的空间比例是1:8,从输出的结果也清晰的看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216K(eden+1个survivor)。

  咱们也注意到在执行testAllocation()时出现了一次Minor GC,GC的结果是新生代6651K变为148K,而总占用内存则几乎没有减小(由于几乎没有可回收的对象)。此次GC是发生的缘由是为allocation4分配内存的时候,eden已经被占用了6M,剩余空间已不足分配allocation4所需的4M内存,所以发生Minor GC。GC期间虚拟机发现已有的3个2M大小的对象所有没法放入survivor空间(survivor空间只有1M大小),因此直接转移到老年代去。GC后4M的allocation4对象分配在eden中。

清单2:testAllocation()方法输出结果

[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation   total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
  from space 1024K,  14% used [0x032d0000, 0x032f5370, 0x033d0000)
  to   space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
tenured generation   total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)
   the space 10240K,  60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

规则二:配置了PretenureSizeThreshold的状况下,对象大于设置值将直接在老年代分配。

  执行testPretenureSizeThreshold()方法后,咱们看到eden空间几乎没有被使用,而老年代的10M控件被使用了40%,也就是4M的allocation对象直接就分配在老年代中,则是由于PretenureSizeThreshold被设置为3M,所以超过3M的对象都会直接从老年代分配。

清单3:

Heap
def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)
  eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
   the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)
   the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)
No shared spaces configured.

规则三:在eden通过GC后存活,而且survivor能容纳的对象,将移动到survivor空间内,若是对象在survivor中继续熬过若干次回收(默认为15次)将会被移动到老年代中。回收次数由MaxTenuringThreshold设置。

  分别以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15两种设置来执行testTenuringThreshold(),方法中allocation1对象须要256K内存,survivor空间能够容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后很是干净的变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代survivor空间,这时候新生代仍然有404KB被占用。

清单4:
MaxTenuringThreshold=1

[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:     414664 bytes,     414664 total
: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation   total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)
   the space 10240K,  43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

MaxTenuringThreshold=15
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:     414664 bytes,     414664 total
: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   2:     414520 bytes,     414520 total
: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation   total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
  from space 1024K,  39% used [0x031d0000, 0x03235338, 0x032d0000)
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
   the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

规则四:若是在survivor空间中相同年龄全部对象大小的累计值大于survivor空间的一半,大于或等于个年龄的对象就能够直接进入老年代,无需达到MaxTenuringThreshold中要求的年龄。

  执行testTenuringThreshold2()方法,并将设置-XX:MaxTenuringThreshold=15,发现运行结果中survivor占用仍然为0%,而老年代比预期增长了6%,也就是说allocation一、allocation2对象都直接进入了老年代,而没有等待到15岁的临界年龄。由于这2个对象加起来已经到达了512K,而且它们是同年的,知足同年对象达到survivor空间的一半规则。咱们只要注释掉其中一个对象new操做,就会发现另一个就不会晋升到老年代中去了。

清单5:
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     676824 bytes,     676824 total
: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)
   the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
   the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

规则五:在Minor GC触发时,会检测以前每次晋升到老年代的平均大小是否大于老年代的剩余空间,若是大于,改成直接进行一次Full GC,若是小于则查看HandlePromotionFailure设置看看是否容许担保失败,若是容许,那仍然进行Minor GC,若是不容许,则也要改成进行一次Full GC。

  前面提到过,新生代才有复制收集算法,但为了内存利用率,只使用其中一个survivor空间来做为轮换备份,所以当出现大量对象在GC后仍然存活的状况(最极端就是GC后全部对象都存活),就须要老年代进行分配担保,把survivor没法容纳的对象直接放入老年代。与生活中贷款担保相似,老年代要进行这样的担保,前提就是老年代自己还有容纳这些对象的剩余空间,一共有多少对象在GC以前是没法明确知道的,因此取以前每一次GC晋升到老年代对象容量的平均值与老年代的剩余空间进行比较决定是否进行Full GC来让老年代腾出更多空间。

  取平均值进行比较其实仍然是一种动态几率的手段,也就是说若是某次Minor GC存活后的对象突增,大大高于平均值的话,依然会致使担保失败,这样就只好在失败后从新进行一次Full GC。虽然担保失败时作的绕的圈子是最大的,但大部分状况下都仍是会将HandlePromotionFailure打开,避免Full GC过于频繁。

清单6:
HandlePromotionFailure = false

[GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
[GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]


总结

  本章介绍了垃圾收集的算法、6款主要的垃圾收集器,以及经过代码实例具体介绍了新生代串行收集器对内存分配及回收的影响。

  GC在不少时候都是系统并发度的决定性因素,虚拟机之因此提供多种不一样的收集器,提供大量的调节参数,是由于只有根据实际应用需求、实现方式选择最优的收集方式才能获取最好的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也没有什么必然的行为。笔者看过一些文章,撇开具体场景去谈论老年代达到92%会触发Full GC(92%应当来自CMS收集器触发的默认临界点)、98%时间在进行垃圾收集系统会抛出OOM异常(98%应该来自parallel收集器收集时间比率的默认临界点)其实意义并不太大。所以学习GC若是要到实践调优阶段,必须了解每一个具体收集器的行为、优点劣势、调节参数。
数组

 

2. 一次Java垃圾收集调优实战缓存

http://www.iteye.com/topic/212967多线程

1 资料

  • JDK5.0垃圾收集优化之--Don't Pause(花钱的年华) 
  • 编写对GC友好,又不泄漏的代码(花钱的年华) 
  • JVM调优总结 
  • JDK 6全部选项及默认值 

2 GC日志打印

  GC调优是个很实验很伽利略的活儿,GC日志是先决的数据参考和最终验证:并发

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps(GC发生的时间)
-XX:+PrintGCApplicationStoppedTime(GC消耗了多少时间)
-XX:+PrintGCApplicationConcurrentTime(GC之间运行了多少时间)

3 收集器选择

CMS收集器:暂停时间优先

   配置参数:-XX:+UseConcMarkSweepGC
   已默认无需配置的参数:-XX:+UseParNewGC(Parallel收集新生代) -XX:+CMSPermGenSweepingEnabled(CMS收集持久代) -XX:UseCMSCompactAtFullCollection(full gc时压缩年老代)框架

   初始效果:1g堆内存的新生代约60m,minor gc约5-20毫秒,full gc约130毫秒。less

Parallel收集器:吞吐量优先

    配置参数: -XX:+UseParallelGC -XX:+UseParallelOldGC(Parallel收集年老代,从JDK6.0开始支持)jvm

    已默认无需配置的参数: -XX:+UseAdaptiveSizePolicy(动态调整新生代大小)

    初始效果:1g堆内存的新生代约90-110m(动态调整),minor gc约5-20毫秒,full gc有无UseParallelOldGC 参数分别为1.3/1.1秒,差异不大。

    另外-XX:MaxGCPauseMillis=100 设置minor gc的指望最大时间,JVM会以此来调整新生代的大小,但在此测试环境中对象死的太快,此参数做用不大。

4 调优实战

      Parallel收集高达1秒的暂停时间基本不可忍受,因此选择CMS收集器。

      在被压测的Mule 2.0应用里,每秒都有大约400M的海量短命对象产生:

  1. 由于默认60M的新生代过小了,频繁发生minor gc,大约0.2秒就进行一次。
  2. 由于CMS收集器中MaxTenuringThreshold(生代对象撑过过多少次minor gc才进入年老代的设置)默认0,存活的临时对象不通过Survivor区直接进入年老代,不久就占满年老代发生full gc。

     对这两个参数的调优,既要改善上面两种状况,又要避免新生代过大,复制次数过多形成minor gc的暂停时间过长。

  1. 使用-Xmn调到1/3 总内存。观察后设置-Xmn500M,新生代实际约460m。(用-XX:NewRatio设置无效,只能用 -Xmn)。
  2. 添加-XX:+PrintTenuringDistribution 参数观察各个Age的对象总大小,观察后设置-XX:MaxTenuringThreshold=5。

      优化后,大约1.1秒才发生一次minor gc,且速度依然保持在15-20ms之间。同时年老代的增加速度大大减缓,好久才发生一次full gc,

      参数定稿:

 -server -Xms1024m -Xmx1024m -Xmn500m -XX:+UseConcMarkSweepGC   -XX:MaxTenuringThreshold=5 
-XX:+ExplicitGCInvokesConcurrent

      最后服务处理速度从1180 tps 上升到1380 tps,调整两个参数提高17%的性能仍是笔很划算的买卖。

 

     另外,JDK6 Update 7自带了一个VisualVM工具,内里就是以前也有用过的Netbean Profiler,相似JConsole同样使用,能够看到线程状态,内存中对象以及方法的CPU时间等调优重要参考依据。免费捆绑啊,Sun 这样搞法,其余作Profiler的公司要关门了。

3.JVM gc参数设置与分析

来自:http://hi.baidu.com/i1see1you/blog/item/7ba0d250c30131481038c20c.html

 

一.概述

java的最大好处是自动垃圾回收,这样就无需咱们手动的释放对象空间了,可是也产生了相应的负效果,gc是须要时间和资源的,很差的gc会严重影响系统的系能,所以良好的gc是JVM的高性能的保证。JVM堆分为新生代,旧生代和年老代,新生代可用的gc方式有:串行gc(Serial Copying),并行回收gc(Parellel Scavenge),并行gc(ParNew),旧生代和年老代可用的gc方式有串行gc(Serial MSC),并行gc(Parallel MSC),并发gc(CMS)。

二.回收方式的选择

jvm有client和server两种模式,这两种模式的gc默认方式是不一样的:

clien模式下,新生代选择的是串行gc,旧生代选择的是串行gc

server模式下,新生代选择的是并行回收gc,旧生代选择的是并行gc

通常来讲咱们系统应用选择有两种方式:吞吐量优先和暂停时间优先,对于吞吐量优先的采用server默认的并行gc方式,对于暂停时间优先的选用并发gc(CMS)方式。

三.CMS gc

CMS,全称Concurrent Low Pause Collector,是jdk1.4后期版本开始引入的新gc算法,在jdk5和jdk6中获得了进一步改进,它的主要适合场景是对响应时间的重要性需求大于对吞吐量的要求,可以承受垃圾回收线程和应用线程共享处理器资源,而且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽可能减小应用的暂停时间,减小full gc发生的概率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。在咱们的应用中,由于有缓存的存在,而且对于响应时间也有比较高的要求,所以但愿能尝试使用CMS来替代默认的server型JVM使用的并行收集器,以便得到更短的垃圾回收的暂停时间,提升程序的响应性。
    CMS并不是没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样:
    初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 从新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)

    其中的1,3两个步骤须要暂停全部的应用程序线程的。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记以后,暂停全部应用程序线程,从新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新致使)。第一次暂停会比较短,第二次暂停一般会比较长,而且 remark这个阶段能够并行标记。

    而并发标记、并发清除、并发重设阶段的所谓并发,是指一个或者多个垃圾回收线程和应用程序线程并发地运行,垃圾回收线程不会暂停应用程序的执行,若是你有多于一个处理器,那么并发收集线程将与应用线程在不一样的处理器上运行,显然,这样的开销就是会下降应用的吞吐量。Remark阶段的并行,是指暂停了全部应用程序后,启动必定数目的垃圾回收进程进行并行标记,此时的应用线程是暂停的。

四.full  gc

full gc是对新生代,旧生代,以及持久代的统一回收,因为是对整个空间的回收,所以比较慢,系统中应当尽可能减小full gc的次数。

以下几种状况下会发生full gc:

《旧生代空间不足

《持久代空间不足

《CMS GC时出现了promotion failed和concurrent mode failure

《统计获得新生代minor gc时晋升到旧生代的平均大小小于旧生代剩余空间

《直接调用System.gc,能够DisableExplicitGC来禁止

《存在rmi调用时,默认会每分钟执行一次System.gc,能够经过-Dsun.rmi.dgc.server.gcInterval=3600000来设置大点的间隔。

五.示例

下面对以下的参数进行分析:

JAVA_OPTS="-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4

-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log -Djava.awt.headless=true -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000

-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"

-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m

Xms,即为jvm启动时得JVM初始堆大小,Xmx为jvm的最大堆大小,xmn为新生代的大小,permsize为永久代的初始大小,MaxPermSize为永久代的最大空间。

-XX:SurvivorRatio=4

SurvivorRatio为新生代空间中的Eden区和救助空间Survivor区的大小比值,默认是32,也就是说Eden区是 Survivor区的32倍大小,要注意Survivo是有两个区的,所以Surivivor其实占整个young genertation的1/34。调小这个参数将增大survivor区,让对象尽可能在survitor区呆长一点,减小进入年老代的对象。去掉救助空间的想法是让大部分不能立刻回收的数据尽快进入年老代,加快年老代的回收频率,减小年老代暴涨的可能性,这个是经过将-XX:SurvivorRatio 设置成比较大的值(好比65536)来作到。

 -verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log

将虚拟机每次垃圾回收的信息写到日志文件中,文件名由file指定,文件格式是平文件,内容和-verbose:gc输出内容相同。

-Djava.awt.headless=true

Headless模式是系统的一种配置模式。在该模式下,系统缺乏了显示设备、键盘或鼠标。

-XX:+PrintGCTimeStamps -XX:+PrintGCDetails

设置gc日志的格式

-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000 

指定rmi调用时gc的时间间隔

-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15

采用并发gc方式,通过15次minor gc 后进入年老代六.一些常见问题1.为了不Perm区满引发的full gc,建议开启CMS回收Perm区选项:
+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled

2.默认CMS是在tenured generation沾满68%的时候开始进行CMS收集,若是你的年老代增加不是那么快,而且但愿下降CMS次数的话,能够适当调高此值:
-XX:CMSInitiatingOccupancyFraction=80

3.遇到两种fail引发full gc:Prommotion failed和Concurrent mode failed时:Prommot
相关文章
相关标签/搜索