zhaoyu@home:~$

jvm-内存区域和GC概述

内存区域和内存溢出

  • 程序计数器: 当前所执行程序的行号指示器。分支,循环跳转等都计数器选取下一行需要执行的代码。当JVM运行到本地方法时,程序计数器为null

  • Java虚拟机栈: 这就是我们通常说的栈,是线程私有的,和线程的生命周期相同 。每个方法创建时,都会创建一个栈帧,用于存放局部变量,动态链接,方法出口等。 每一个方法从调用到完成,就是一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 本地方法栈: 为虚拟机使用Native方法服务。

  • 堆: 是被所有线程共享的,虚拟机启动时创建。唯一目的是存放对象实例。是GC管理的主要对象,现在的GC基本采用分代收集算法,可分为新生代和老年代, 再细分还有Eden空间,From Survivor空间,to Survivor空间。从内存分配上讲,堆又可划分出多个线程私有的分配缓存区。

  • 方法区: 存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等。有一个别名叫“非堆”,和堆区分开来。垃圾收集在这个区域比较少出现。 运行时常量池:是方法区的一部分,用于存放编译器的各种常量和符号引用。编译器产生的常量会放入运行常量池中,不仅如此, 运行期的一些常量也有可能被放入。

  • 直接内存: java运行时直接分配的内存,并不是java运行时数据区的一部分,也不是JVM规范中定义的内存区域。一般通过本地方法分配。如:

    Unsafe.allocateMemory(1024*1024);
    

对象是否已死的判断方法:

  1. 引用计数(无法解决循环引用的问题)
  2. 可达性分析,从一个GC Root开始向下搜索,当一个对象到GC root没有任何引用链相连时,我们成为对象不可达。

能作为GC root的对象包括:

  1. 虚拟机栈中引用的对象。
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象。
  4. 本地方法中引用的对象。

GC

  • GC中有几个重要的术语,并行(parallel),stop-the-world,并发(concurrent)。并行表示多线程执行GC操作。stop-the-world表示在GC发生时, java应用线程会停止执行。并发表示在GC运行的同时,java应用程序也在执行中。

GC发生的区域

GC发生在方法区和堆两个内存区域中,

方法区回收:方法区(HotSpot虚拟机中的永久代)进行垃圾回收的效率比较低。主要回收两部分内容:废弃常量和无用类。 废弃常量的判断:没有任何引用,比较简单。

无用类的判断:

  1. 所有实例已被回收,
  2. 加载该类的classLoader已被回收
  3. 该类对应的Class对象没有任何地方引用,无法通过反射访问该类的方法。

堆区回收:可分为新生代和老年代,再细分还有Eden空间,From Survivor空间,to Survivor空间。

Minor GC

当Eden区域分配内存时,发现空间不足,JVM就会触发Minor GC,程序中System.gc()也可能触发。

HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1。一般情况下,新创建的对象都会被分配到 Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC, 年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块, 每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制 到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值的对象会被移动到年老代中,没有达到阈值的对象会被复制到 “To”区域,这个阈值通过下面参数设置。

-XX:MaxTenuringThreshold

经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上 次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程。

Yong区JVM参数的设置
-XX:NewSize和-XX:MaxNewSize

用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

-XX:SurvivorRatio

用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

-XX:+PrintTenuringDistribution

这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1

Full GC

  • 旧生代空间不足:java.lang.outOfMemoryError:java heap space;
  • Perm空间满:java.lang.outOfMemoryError:PermGen space;
  • CMS GC时出现promotion failed 和concurrent mode failure,或CMS在old达到一个阈值时。
  • 统计得到的minor GC晋升到旧生代的平均大小大于旧生代的剩余空间(悲观策略);
  • 主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题;

GC日志

查看GC日志参数如下:

-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/test/logs/gc.log

一个minorGc的日志:

2016-08-23T02:23:07.219-02001: 64.3222:[GC3(Allocation Failure4) 64.322: 
[ParNew5: 613404K->68068K6(613440K)7, 0.1020465 secs8] 10885349K->10880154K9(12514816K)10, 0.1021309 secs11]
[Times: user=0.78 sys=0.01, real=0.11 secs]12

解释如下:

  • 2016-08-23T02:23:07.219-0200 – GC发生的时间;
  • 64.322 – GC开始,相对JVM启动的相对时间,单位是秒;
  • GC – 区别MinorGC和FullGC的标识,这次代表的是MinorGC;
  • Allocation Failure – MinorGC的原因,在这个case里边,由于年轻代不满足申请的空间,因此触发了MinorGC;
  • ParNew – yong代收集器的名称,
  • 613404K->68068K – 收集前后年轻代的使用情况;
  • (613440K) – 整个年轻代的容量;
  • 0.1020465 secs – 最终清理持续的时间.
  • 10885349K->10880154K – 收集前后整个堆的使用情况;
  • (12514816K) – 整个堆的容量;
  • 0.1021309 secs – ParNew收集器标记和复制年轻代活着的对象所花费的时间(包括和老年代通信的开销、对象晋升到老年代时间、垃圾收集周期结束一些 最后的清理对象等的花销);
  • [Times: user=0.78 sys=0.01, real=0.11 secs] – GC事件在不同维度的耗时,具体的用英文解释起来更加合理: user – GC线程在本次收集中使用的CPU总时间 sys – OS calls或等待系统事件花费的时间 real – 应用停止的时间,Parallel GC,这个数字于(user+sys)/gc使用的线程数。本例中使用的线程数为8。

JVM的悲观策略

所谓的悲观策略,就是JVM不按照JVM指定的参数来进行CMS GC,而是根据内存情况以及之前回收的方式动态调整,自行进行GC。旧生代剩余的空间(available) 大于新生代中使用的空间(max_promotion_in_bytes),或者大于之前平均晋升的old的大小(av_promo),返回false。cms gc是每隔一个周期(默认2s) 就会做一次这个检查,如果为false,则不执行YGC,而触发cms gc。

使用下面选项禁用悲观策略:

-XX:UseCMSInitiatingOccupancyOnly

GC的常见错误

  1. Promotion failed, promotion的意思为晋升,Minor GC后,Survivor空间容纳不了剩余对象,要放入old代,old代有碎片,或者还没来得及FullGC导致空间不够,不能容纳这 些对象,就会发生Promotion failed错误,并可能伴随着concurrent mode failure错误。一条Promotion failed的gc日志如下:
    [ParNew (promotion failed): 320138K->320138K(353920K), 0.2365970 secs]42576.951: [CMS: 1139969K->1120688K(  
    166784K), 9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs] 
    

    如果是碎片引起的原因,那么让CMS在进行一定Full GC后进行一次整理算法,通过如下参数控制,

    -XX:UseCMSCompactAtFullCollection 
    -XX:CMSFullGCBeforeCompaction=5
    

    也就是进行5次FullGC后进行一次整理算法,从而控制old代的碎片在一定范围内。

如果是old代没有足够的空间引起的原因:那么一是增大Survivor。通过增大yong代或者调整-XX:SurvivorRatio参数,这个参数是Eden区和Survivor区的 大小比值,要注意Survivor是有两个区的,默认Eden是Survivor的若干倍。调小这个参数将增大survivor区,让对象尽量在survitor区呆长一点,尽可能的 让minor GC回收掉,减少进入年老代的对象。二是调低触发CMS GC执行的阀值,提前让CMS GC,以获得更大的old代剩余空间。CMS GC触发主要由 CMSInitiatingOccupancyFraction值决定,默认情况是当旧生代已用空间为68%。

+XX:CMSInitiatingOccupancyFraction=68
  1. concurrent mode failre 该问题是执行CMS GC的过程中业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代, 而老年代也放不下而产生的。下面是一个concurrent mode failure的一条gc日志:
    0.195: [GC 0.195: [ParNew: 2986K->2986K(8128K), 0.0000083 secs]0.195: 
    [CMS0.212: [CMS-concurrent-preclean: 0.011/0.031 secs] [Times: user=0.03 sys=0.02, real=0.03 secs] 
    (concurrent mode failure): 56046K->138K(57344K), 0.0271519 secs] 59032K->138K(65472K),
     [CMS Perm : 2079K->2078K(12288K)], 0.0273119 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
    

解决这个问题:一是调低触发CMS GC执行的阀值,提前让CMS GC,以获得更大的old代剩余空间。调整CMSInitiatingOccupancyFraction的值。