《深入Java虚拟机学习笔记》- 第9章 垃圾收集

java

一、Java内存组成

  1. 组成图

  2. 堆(Heap)

    1. 运行时数据区域,所有类实例和数组的内存均从此处分配。Java虚拟机启动时创建。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。
    2. 组成 组成 详解

      Young Generation即图中的Eden + From Space + To Space 
      1.Eden存放新生的对象 
      2.Survivor Space有两个,存放每次垃圾回收后存活的对象
      Old GenerationTenured Generation 即图中的Old Space 
      主要存放应用程序中生命周期长的存活对象

  3. 非堆内存

    1. JVM具有一个由所有线程共享的方法区。方法区属于非堆内存。它存储每个类结构,如运行时常数池、字段和方法数据,以及方法的代码。它是在Java虚拟机启动时创建的。
    2. 除了方法区外,Java虚拟机实现可能需要用于内部处理或优化的内存,这种内存也是非堆内存。例如,JIT编译器需要内存来存储从Java虚拟机代码转换而来的本机代码,从而获得高性能。
    3. 组成 组成 详解

      Permanent Generation即图中的Permanent Space 
      存放JVM自己的反射对象,比如类对象和方法对象
      native heapJVM内部处理或优化

二、GC策略

    1. JVM采用一种分代回收(generational collection)的策略,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫做minor collection,而对老对象(old generation)的检查回收频率要低很多,称为major collection。这样就不需要每次GC都将内存中所有对象都检查一遍。

  1. 非堆内存

    1. GC不会在主程序运行期对PermGen Space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen Space错误。

三、内存申请、对象衰老过程

  1. 内存申请过程

    1. JVM会试图为相关Java对象在Eden中初始化一块内存区域;
    2. 当Eden空间足够时,内存申请结束。否则到下一步;
    3. JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
    4. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
    5. 当OLD区空间不够时,JVM会在OLD区进行major collection;
    6. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";

  2. 对象衰老过程

    1. young generation的内存,由一块Eden和两块Survivor Space构成。新创建的对象的内存都分配自eden。两块Survivor Space总有会一块是空闲的,用作copying collection的目标空间。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。所谓survivor,也就是大部分对象在Eden出生后,根本活不过一次GC。对象在young generation里经历了一定次数的minor collection后,年纪大了,就会被移到old generation中,称为tenuring。
    2. 剩余内存空间不足会触发GC,如eden空间不够了就要进行minor collection,old generation空间不够要进行major collection,permanent generation空间不足会引发Full GC。

四、JVM参数

  1. 参数说明 参数 说明 默认值 生产环境约定

JVM参数的含义

参数名称

含义

默认值

建议

-Xms

初始堆大小

定义YOUNG+OLD段的总尺寸,mx为最大可占用的YOUNG+OLD内存大小。

物理内存的1/64(<1GB)

默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.

-Xms

最大堆大小

定义YOUNG+OLD段的总尺寸, mx为最大可占用的YOUNG+OLD内存大小。

物理内存的1/4(<1GB)

默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

-Xmn

年轻代大小(1.4or lator)

整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。

整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.

增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

-XX:NewSize

设置年轻代大小(for 1.3/1.4)

 

 

-XX:MaxNewSize

设置年轻代最大值(for 1.3/1.4)

 

 

-XX:PermSize

设置持久代(perm gen)初始值

物理内存的1/64

 

-XX:MaxPermSize

设置持久代(perm gen)最大值

物理内存的1/4

在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。

-XX:NewRaito 

年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)

 

-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

-XX:SurvivorRaito 

设置YOUNG段中Eden区与Survivor区的比值,如此值为4,则Eden为4/6,两个Survivor分别为1/6。

 

设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

-XX:LargePageSizeInBytes

内存页的大小不可设置过大, 会影响Perm的大小

 

=128m

-XX:+UseFastAccessorMethods

原始类型的快速优化

 

 

-XX:+DisableExplicitGC

关闭System.gc()

 

这个参数需要严格的测试

-XX:MaxTenuringThreshold 

设置垃圾最大年龄。 

如果设置为0的话,则新生对象不经过Survivor区,直接进入OLD段。对于OLD对象比较多的应用,可以提高效率。如果将此值设置为一个较大值,则新生对象会在Survivor区进行多次复制,这样可以增加对象的存活时间,增加在minor

collection即被回收的概率。 

 

-XX:+AggressiveOpts

加快编译

 

 

-XX:+UseBiasedLocking

锁机制的性能改善

 

 

-Xnoclassgc

禁用垃圾回收

 

 

-XX:SoftRefLRUPolicyMSPerMB

每兆堆空闲空间中SoftReference的存活时间

1s

softly reachable

objects will remain alive for some amount of time after the last time they

were referenced. The default value is one second of lifetime per free

megabyte in the heap

-XX:PretenureSizeThreshold

对象超过多大是直接在旧生代分配

0

单位字节 新生代采用Parallel

Scavenge GC时无效

另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.

-XX:TLABWasteTargetPercent

TLAB占eden区的百分比

1%

 

-XX:+CollectGen0First

FullGC时是否先YGC

false

 

-Xss

设置堆栈的大小。 

JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。

在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

-XX:ThreadStackSize

Thread Stack Size

 

(0 means

use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0

and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and

earlier); all others 0.]

 

并行收集器相关参数

参数名称

含义

默认值

建议

-XX:+UseParallelGC

选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

 

 

-XX:+UseParNewGC

设置年轻代为并行收集

可与CMS收集同时使用

JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值

-XX:ParallelGCThreads 

配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。

此值最好配置与处理器数目相等。

 

-XX:+UseParallelOldGC 

配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。 

 

这个是JAVA 6出现的参数选项

-XX:MaxGCPauseMillis 

设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

 

 

-XX:+UseAdaptiveSizePolicy

设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等。

此值建议使用并行收集器时,一直打开。

 

-XX:GCTimeRatio

设置垃圾回收时间占程序运行时间的百分比

公式为1/(1+n)

-XX:+ScavengeBeforeFullGC

Full GC前调用YGC

true

Do young generation

GC prior to a full GC. (Introduced in 1.4.1.)

CMS相关参数

-XX:+UseConcMarkSweepGC

使用CMS内存收集

测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.???

-XX:+AggressiveHeap

试图是使用大量的物理内存

长时间大内存使用的优化,能检查计算资源(内存, 处理器数量)

至少需要256MB内存

大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)

-XX:CMSFullGCsBeforeCompaction

多少次后进行内存压缩

由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理.

-XX:+CMSParallelRemarkEnabled

降低标记停顿

-XX+UseCMSCompactAtFullCollection

在FULL GC的时候, 对年老代的压缩

CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。

可能会影响性能,但是可以消除碎片

-XX:+UseCMSInitiatingOccupancyOnly

使用手动定义初始化定义开始CMS收集

禁止hostspot自行触发CMS GC

-XX:CMSInitiatingOccupancyFraction=70

使用cms作为垃圾回收

使用70%后开始CMS收集

92

为了保证不出现promotion

failed(见下面介绍)错误,该值的设置需要满足以下公式CMSInitiatingOccupancyFraction计算公式

-XX:CMSInitiatingPermOccupancyFraction

设置Perm Gen使用到达多少比率时触发

92

-XX:+CMSIncrementalMode

设置为增量模式

用于单CPU情况

-XX:+CMSClassUnloadingEnabled

辅助信息

-XX:+PrintGC

 

 

输出形式:

[GC 118250K->113543K(130112K), 0.0094143

secs]

[Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails

 

 

输出形式:[GC [DefNew:

8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K),

0.0124633 secs]

[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured:

112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K),

0.0436268 secs]

-XX:+PrintGCTimeStamps

 

 

 

-XX:+PrintGC:PrintGCTimeStamps

 

 

可与-XX:+PrintGC -XX:+PrintGCDetails混合使用

输出形式:11.851: [GC 98328K->93620K(130112K),

0.0082960 secs]

-XX:+PrintGCApplicationStoppedTime

打印垃圾回收期间程序暂停的时间.可与上面混合使用

 

输出形式:Total time for

which application threads were stopped: 0.0468229 seconds

-XX:+PrintGCApplicationConcurrentTime

打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用

 

输出形式:Application time:

0.5291524 seconds

-XX:+PrintHeapAtGC

打印GC前后的详细堆栈信息

 

 

-Xloggc:filename

把相关日志信息记录到文件以便分析.

与上面几个配合使用

 

 

-XX:+PrintClassHistogram

garbage collects before printing the

histogram.

 

 

-XX:+PrintTLAB

查看TLAB空间的使用情况

 

 

XX:+PrintTenuringDistribution

查看每次minor GC后新的存活周期的阈值

 

Desired survivor size 1048576 bytes, new

threshold 7 (max 15)

new threshold 7即标识新的存活周期的阈值为7。

      

 

  1. 举例说明

  1. MEM_ARGS="-Xms512m -Xmx512m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=128m -XX:MaxPermSize=128m -XX:SurvivorRatio=6"

    上例中,
    YOUNG+OLD: 512M
    YOUNG: 256M
    Perm: 128M
    Eden: YOUNG*6/(6+1+1)=192M
    单个Survivor: YOUNG/(6+1+1)=32M

 JVM的回收执行机制:

  在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)

1)串行GC

在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定

2)并行回收GC

在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数

3)并行GC

与旧生代的并发GC配合使用

旧生代的GC:

旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行GC(SerialMSC)、并行GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。

以上各种GC机制是需要组合使用的,指定方式由下表所示:

JVM的GC方式:

Minor GC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:

  1. 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
  2. 内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
  3. 执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
  4. 质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。

Major GC vs Full GC

大家应该注意到,目前,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们一看就知道这些在我们已经知道的基础之上做出的定义是正确的,Minor GC 清理年轻带内存应该被设计得简单:

  • Major GC 是清理永久代。

  • Full GC 是清理整个堆空间—包括年轻代和永久代。

很不幸,实际上它还有点复杂且令人困惑。首先,许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。另一方面,许多现代垃圾收集机制会清理部分永久代空间,所以使用“cleaning”一词只是部分正确。

这使得我们不用去关心到底是叫 Major GC 还是 Full GC,大家应该关注当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程。

这种混乱甚至内置到 JVM 标准工具。下面一个例子很好的解释了我的意思。让我们比较两个不同的工具 Concurrent Mark 和 Sweep collector (-XX:+UseConcMarkSweepGC)在 JVM 中运行时输出的跟踪记录。

第一次尝试通过 jstat 输出:

?

1

my-precious: me$ jstat -gc -t 4235 1s

?

1

2

3

4

5

6

7

8

9

10

11

12

13

Time S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT  

 5.7 34048.0 34048.0  0.0   34048.0 272640.0 194699.7 1756416.0   181419.9  18304.0 17865.1 2688.0 2497.6      3    0.275   0      0.000    0.275

 6.7 34048.0 34048.0 34048.0  0.0   272640.0 247555.4 1756416.0   263447.9  18816.0 18123.3 2688.0 2523.1      4    0.359   0      0.000    0.359

 7.7 34048.0 34048.0  0.0   34048.0 272640.0 257729.3 1756416.0   345109.8  19072.0 18396.6 2688.0 2550.3      5    0.451   0      0.000    0.451

 8.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0  444982.5  19456.0 18681.3 2816.0 2575.8      7    0.550   0      0.000    0.550

 9.7 34048.0 34048.0 34046.7  0.0   272640.0 16777.0  1756416.0   587906.3  20096.0 19235.1 2944.0 2631.8      8    0.720   0      0.000    0.720

10.7 34048.0 34048.0  0.0   34046.2 272640.0 80171.6  1756416.0   664913.4  20352.0 19495.9 2944.0 2657.4      9    0.810   0      0.000    0.810

11.7 34048.0 34048.0 34048.0  0.0   272640.0 129480.8 1756416.0   745100.2  20608.0 19704.5 2944.0 2678.4     10    0.896   0      0.000    0.896

12.7 34048.0 34048.0  0.0   34046.6 272640.0 164070.7 1756416.0   822073.7  20992.0 19937.1 3072.0 2702.8     11    0.978   0      0.000    0.978

13.7 34048.0 34048.0 34048.0  0.0   272640.0 211949.9 1756416.0   897364.4  21248.0 20179.6 3072.0 2728.1     12    1.087   1      0.004    1.091

14.7 34048.0 34048.0  0.0   34047.1 272640.0 245801.5 1756416.0   597362.6  21504.0 20390.6 3072.0 2750.3     13    1.183   2      0.050    1.233

15.7 34048.0 34048.0  0.0   34048.0 272640.0 21474.1  1756416.0   757347.0  22012.0 20792.0 3200.0 2791.0     15    1.336   2      0.050    1.386

16.7 34048.0 34048.0 34047.0  0.0   272640.0 48378.0  1756416.0   838594.4  22268.0 21003.5 3200.0 2813.2     16    1.433   2      0.050    1.484

这个片段是 JVM 启动后第17秒提取的。基于该信息,我们可以得出这样的结果,运行了12次 Minor GC、2次 Full GC,时间总跨度为50毫秒。通过 jconsole 或者 jvisualvm 这样的基于GUI的工具你能得到同样的结果。

?

1

java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

3.157: [GC (Allocation Failure) 3.157: [ParNew: 272640K->34048K(306688K), 0.0844702 secs] 272640K->69574K(2063104K), 0.0845560 secs] [Times: user=0.23 sys=0.03, real=0.09 secs]

4.092: [GC (Allocation Failure) 4.092: [ParNew: 306688K->34048K(306688K), 0.1013723 secs] 342214K->136584K(2063104K), 0.1014307 secs] [Times: user=0.25 sys=0.05, real=0.10 secs]

... cutforbrevity ...

11.292: [GC (Allocation Failure) 11.292: [ParNew: 306686K->34048K(306688K), 0.0857219 secs] 971599K->779148K(2063104K), 0.0857875 secs] [Times: user=0.26 sys=0.04, real=0.09 secs]

12.140: [GC (Allocation Failure) 12.140: [ParNew: 306688K->34046K(306688K), 0.0821774 secs] 1051788K->856120K(2063104K), 0.0822400 secs] [Times: user=0.25 sys=0.03, real=0.08 secs]

12.989: [GC (Allocation Failure) 12.989: [ParNew: 306686K->34048K(306688K), 0.1086667 secs] 1128760K->931412K(2063104K), 0.1087416 secs] [Times: user=0.24 sys=0.04, real=0.11 secs]

13.098: [GC (CMS Initial Mark) [1 CMS-initial-mark: 897364K(1756416K)] 936667K(2063104K), 0.0041705 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]

13.102: [CMS-concurrent-mark-start]

13.341: [CMS-concurrent-mark: 0.238/0.238 secs] [Times: user=0.36 sys=0.01, real=0.24 secs]

13.341: [CMS-concurrent-preclean-start]

13.350: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]

13.350: [CMS-concurrent-abortable-preclean-start]

13.878: [GC (Allocation Failure) 13.878: [ParNew: 306688K->34047K(306688K), 0.0960456 secs] 1204052K->1010638K(2063104K), 0.0961542 secs] [Times: user=0.29 sys=0.04, real=0.09 secs]

14.366: [CMS-concurrent-abortable-preclean: 0.917/1.016 secs] [Times: user=2.22 sys=0.07, real=1.01 secs]

14.366: [GC (CMS Final Remark) [YG occupancy: 182593 K (306688 K)]14.366: [Rescan (parallel) , 0.0291598 secs]14.395: [weak refs processing, 0.0000232 secs]14.395: [class unloading, 0.0117661 secs]14.407: [scrub symbol table, 0.0015323 secs]14.409: [scrub string table, 0.0003221 secs][1 CMS-remark: 976591K(1756416K)] 1159184K(2063104K), 0.0462010 secs] [Times: user=0.14 sys=0.00, real=0.05 secs]

14.412: [CMS-concurrent-sweep-start]

14.633: [CMS-concurrent-sweep: 0.221/0.221 secs] [Times: user=0.37 sys=0.00, real=0.22 secs]

14.633: [CMS-concurrent-reset-start]

14.636: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

在点头同意这个结论之前,让我们看看来自同一个 JVM 启动收集的垃圾收集日志的输出。显然- XX : + PrintGCDetails 告诉我们一个不同且更详细的故事:

基于这些信息,我们可以看到12次 Minor GC 后开始有些和上面不一样了。没有运行两次 Full GC,这不同的地方在于单个 GC 在永久代中不同阶段运行了两次:

  • 最初的标记阶段,用了0.0041705秒也就是4ms左右。这个阶段会暂停“全世界( stop-the-world)”的事件,停止所有应用程序的线程,然后开始标记。
  • 并行执行标记和清洗阶段。这些都是和应用程序线程并行的。
  • 最后 Remark 阶段,花费了0.0462010秒约46ms。这个阶段会再次暂停所有的事件。
  • 并行执行清理操作。正如其名,此阶段也是并行的,不会停止其他线程。

所以,正如我们从垃圾回收日志中所看到的那样,实际上只是执行了 Major GC 去清理老年代空间而已,而不是执行了两次 Full GC。

如果你是后期做决 定的话,那么由 jstat 提供的数据会引导你做出正确的决策。它正确列出的两个暂停所有事件的情况,导致所有线程停止了共计50ms。但是如果你试图优化吞吐量,你会被误导的。清 单只列出了回收初始标记和最终 Remark 阶段,jstat的输出看不到那些并发完成的工作。

结论

考虑到这种情况,最好避免以 Minor、Major、Full GC 这种方式来思考问题。而应该监控应用延迟或者吞吐量,然后将 GC 事件和结果联系起来。

随着这些 GC 事件的发生,你需要额外的关注某些信息,GC 事件是强制所有应用程序线程停止了还是并行的处理了部分事件。

jvm垃圾回收算法

JVM中标记垃圾使用的算法是一种根搜索算法。简单的说,就是从一个叫GC Roots的对象开始 , 向下搜索 , 如果一个对象不能达到GC Roots对象的时候 , 说明它可以被回收了。这种算法比一种叫做引用计数法的垃圾标记算法要好,因为它避免了当两个对象啊互相引用时无法被回收的现象。

     JVM中对于被标记为垃圾的对象进行回收时又分为了一下3种算法:

     1.标记清除算法 ,该算法是从根集合扫描整个空间,标记存活的对象,然后在扫描整个空间对没有被标记的对象进行回收,这种算法在存活对象较多时比较高效,但会产生内存碎片。

     2.复制算法 ,该算法是从根集合扫描,并将存活的对象复制到新的空间,这种算法在存活对象少时比较高效。

     3.标记整理算法 ,标记整理算法和标记清除算法一样都会扫描并标记存活对象,在回收未标记对象的同时会整理被标记的对象,解决了内存碎片的问题。

     JVM中,不同的 内存区域作用和性质不一样,使用的垃圾回收算法也不一样,所以JVM中又定义了几种不同的垃圾回收器(图中连线代表两个回收器可以同时使用):

     1.Serial GC 。从名字上看,串行GC意味着是一种单线程的,所以它要求收集的时候所有的线程暂停。这对于高性能的应用是不合理的,所以串行GC一般用于Client模式的JVM中。

     2.ParNew GC 。是在SerialGC的基础上,增加了多线程机制。但是如果机器是单CPU的,这种收集器是比SerialGC效率低的。

     3.Parrallel Scavenge GC 。这种收集器又叫吞吐量优先收集器,而吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用 1分钟,那么吞吐量就是99%。Parallel Scavenge GC由于可以提供比较不错的吞吐量,所以被作为了server模式JVM的默认配置。

     4.ParallelOld 是老生代并行收集器的一种,使用了标记整理算法,是JDK1.6中引进的,在之前 老生代 只能使用串行回收收集器。

     5.Serial Old 是老生代client模式下的默认收集器,单线程执行,同时也作为CMS收集器失败后的备用收集器。

     6.CMS 又称响应时间优先回收器,使用标记清除算法。他的回收线程数为(CPU核心数+3)/4,所以当CPU核心数为2时比较高效些。CMS分为4个过程:初始标记、并发标记、重新标记、并发清除。

     7.GarbageFirst(G1) 。比较特殊的是G1回收器既可以回收Young Generation,也可以回收Tenured Generation。它是在JDK6的某个版本中才引入的,性能比较高,同时注意了吞吐量和响应时间。

     对于垃圾收集器的组合使用可以通过下表中的参数指定:

     默认的GC种类可以通过jvm.cfg或者通过jmap dump出heap来查看,一般我们通过jstat -gcutil [pid] 1000可以查看每秒gc的大体情况,或者可以在启动参数中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log来记录GC日志。

     GC中有一种情况叫做Full GC,以下几种情况会触发Full GC:

     1.Tenured Space空间不足以创建打的对象或者数组,会执行FullGC,并且当FullGC之后空间如果还不够,那么会OOM:java heap space。

     2.Permanet Generation的大小不足,存放了太多的类信息,在非CMS情况下回触发FullGC。如果之后空间还不够,会OOM:PermGen space。

     3.CMS GC时出现promotion failed和concurrent mode failure时,也会触发FullGC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

     4.判断MinorGC后,要晋升到TenuredSpace的对象大小大于TenuredSpace的大小,也会触发FullGC。

     可以看出,当FullGC频繁发生时,一定是内存出问题了。

**********************************************************

JVM垃圾回收机制

Java语言出来之前,大家都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的allocated,然后不停的~析构。于是,有人就提出,能不能写一段程序在实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?

1960年 基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时Java还没有出世呢!所以实际上GC并不是Java的专利,GC的历史远远大于Java的历史!

那究竟GC为我们做了什么操作呢?

1、    哪些内存需要回收?

2、    什么时候回收?

3、    如何回收?

这时候有人就会疑惑了,既然GC已经为我们解决了这个矛盾,我们还需要学习GC么?当然当然是肯定的,那究竟什么时候我们还需要用到的呢?

1、    排查内存溢出

2、    排查内存泄漏

3、    性能调优,排查并发瓶颈

我们知道,GC主要处理的是对象的回收操作,那么什么时候会触发一个对象的回收的呢?

1、    对象没有引用

2、    作用域发生未捕获异常

3、    程序在作用域正常执行完毕

4、    程序执行了System.exit()

5、    程序发生意外终止(被杀进程等)

其实,我们最容易想到的就是当对象没有引用的时候会将这个对象标记为可回收对象,那么现在就有一个问题,是不是这个对象被赋值为null以后就一定被标记为可回收对象了呢?我们来看一个例子:

package com.yhj.jvm.gc.objEscape.finalizeEscape;

import com.yhj.jvm.gc.objEscape.pojo.FinalizedEscapeTestCase;

/**

 * @Described:逃逸分析测试

 * @author YHJ create at 2011-12-24 下午05:08:09

 * @FileNmae com.yhj.jvm.gc.finalizeEscape.FinalizedEscape.java

 */

public class FinalizedEscape {

    public static void main(String[] args) throwsInterruptedException {

        System.out.println(FinalizedEscapeTestCase.caseForEscape);

       FinalizedEscapeTestCase.caseForEscape = newFinalizedEscapeTestCase();

        System.out.println(FinalizedEscapeTestCase.caseForEscape);

       FinalizedEscapeTestCase.caseForEscape=null;

       System.gc();

       Thread.sleep(100);

        System.out.println(FinalizedEscapeTestCase.caseForEscape);

    }

}

package com.yhj.jvm.gc.objEscape.pojo;

/**

 * @Described:逃逸分析测试用例

 * @author YHJ create at 2011-12-24 下午05:07:05

 * @FileNmae com.yhj.jvm.gc.pojo.TestCaseForEscape.java

 */

public class FinalizedEscapeTestCase {

    public static FinalizedEscapeTestCase caseForEscape = null;

    @Override

    protected void finalize() throws Throwable {

       super.finalize();

       System.out.println("哈哈,我已逃逸!");

       caseForEscape = this;

    }

}

程序的运行结果回事什么样子的呢?

我们来看这段代码

1、  System.out.println(FinalizedEscapeTestCase.caseForEscape);

2、  FinalizedEscapeTestCase.caseForEscape = newFinalizedEscapeTestCase();

3、  System.out.println(FinalizedEscapeTestCase.caseForEscape);

4、  FinalizedEscapeTestCase.caseForEscape=null;

5、  System.gc();

6、  Thread.sleep(100);

7、    System.out.println(FinalizedEscapeTestCase.caseForEscape);

1、    当程序执行第一行是,因为这个对象没有值,结果肯定是null

2、    程序第二行给该对象赋值为新开辟的一个对象

3、    第三行打印的时候,肯定是第二行对象的hash代码

4、    第四行将该对象重新置为null

5、    第五行触发GC

6、    为了保证GC能够顺利执行完毕,第六行等待100毫秒

7、    第七行打印对应的值,回事null么?一定会是null么?

我们来看一下对应的运行结果


 本例中打印了

GC的日志,让我们看的更清晰一点,我们很清晰的看出,最后一句打印的不是null,并且子啊之前,还出现了逃逸的字样。说明这个对象逃逸了,在垃圾回收之前逃逸了,我们再来看这个pojo的写法,就会发现,我们重写了方法finalize,而这个方法就相当于C++中的析构方法,在GC回收之前,会先调用一次这个方法,而这个方法又将this指针指向他自己,因此得以成功逃逸!可见,并不是这个对象被赋值为null之后就一定被标记为可回收,有可能会发生逃逸!

下面我们来看一下几种垃圾收集算法

1、              在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题

当我们的代码出现下面的情形时,该算法将无法适应

a)         ObjA.obj = ObjB

b)         ObjB.obj - ObjA

                 这样的代码会产生如下引用情形 objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。

               


 

2、              根搜索算法

                   根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。



 

目前java中可作为GC Root的对象有

1、    虚拟机栈中引用的对象(本地变量表)

2、    方法区中静态属性引用的对象

3、    方法区中常量引用的对象

4、    本地方法栈中引用的对象(Native对象)

说了这么多,其实我们可以看到,所有的垃圾回收机制都是和引用相关的,那我们来具体的来看一下引用的分类,到底有哪些类型的引用?每种引用都是做什么的呢?

Java中存在四种引用,每种引用如下:

1、  强引用

只要引用存在,垃圾回收器永远不会回收

Object obj = new Object();

//可直接通过obj取得对应的对象 如obj.equels(new Object());

而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。

2、  软引用

非必须引用,内存溢出之前进行回收,可以通过以下代码实现

Object obj = new Object();

SoftReference<Object> sf = new SoftReference<Object>(obj);

obj = null;

sf.get();//有时候会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

3、  弱引用

第二次垃圾回收时回收,可以通过如下代码实现

Object obj = new Object();

WeakReference<Object> wf = new WeakReference<Object>(obj);

obj = null;

wf.get();//有时候会返回null

wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。

弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器

4、  虚引用(幽灵/幻影引用)

           垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现

Object obj = new Object();

PhantomReference<Object> pf = new PhantomReference<Object>(obj);

obj=null;

pf.get();//永远返回null

pf.isEnQueued();//返回从内存中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。

虚引用主要用于检测对象是否已经从内存中删除。

在上文中已经提到了,我们的对象在内存中会被划分为5块区域,而每块数据的回收比例是不同的,根据IBM的统计,数据如下图所示:



 

 我们知道,方法区主要存放类与类之间关系的数据,而这部分数据被加载到内存之后,基本上是不会发生变更的,

Java堆中的数据基本上是朝生夕死的,我们用完之后要马上回收的,而Java栈和本地方法栈中的数据,因为有后进先出的原则,当我取下面的数据之前,必须要把栈顶的元素出栈,因此回收率可认为是100%;而程序计数器我们前面也已经提到,主要用户记录线程执行的行号等一些信息,这块区域也是被认为是唯一一块不会内存溢出的区域。在SunHostSpot的虚拟机中,对于程序计数器是不回收的,而方法区的数据因为回收率非常小,而成本又比较高,一般认为是“性价比”非常差的,所以Sun自己的虚拟机HotSpot中是不回收的!但是在现在高性能分布式J2EE的系统中,我们大量用到了反射、动态代理、CGLIB、JSP和OSGI等,这些类频繁的调用自定义类加载器,都需要动态的加载和卸载了,以保证永久带不会溢出,他们通过自定义的类加载器进行了各项操作,因此在实际的应用开发中,类也是被经常加载和卸载的,方法区也是会被回收的!但是方法区的回收条件非常苛刻,只有同时满足以下三个条件才会被回收!

1、所有实例被回收

2、加载该类的ClassLoader被回收

3、Class对象无法通过任何途径访问(包括反射)

好了,我们现在切入正题,Java1.2之前主要通过引用计数器来标记是否需要垃圾回收,而1.2之后都使用根搜索算法来收集垃圾,而收集后的垃圾是通过什么算法来回收的呢?

1、    标记-清除算法

2、    复制算法

3、    标记-整理算法

我们来逐一过一下

1、    标记-清除算法

        



 

标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。

标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!

2、    复制算法



 

 

 

 

 复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。也就是我们前面提到的

s0 s1等空间。

3、    标记-整理算法



 

 

 

 标记

-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

我们知道,JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,下图展示了minor GC的执行过程。



 

 

 对于新生代和旧生代,

JVM可使用很多种垃圾回收器进行垃圾回收,下图展示了不同生代不通垃圾回收器,其中两个回收器之间有连线表示这两个回收器可以同时使用。



 

 而这些垃圾回收器又分为串行回收方式、并行回收方式合并发回收方式执行,分别运用于不同的场景。如下图所示



 

 下面我们来逐一介绍一下每个垃圾回收器。

1、    Serial收集器

看名字我们都可以看的出来,这个属于串行收集器。其运行示意图如下



 

 

 Serial

收集器是历史最悠久的一个回收器,JDK1.3之前广泛使用这个收集器,目前也是ClientVM下 ServerVM 4核4GB以下机器的默认垃圾回收器。串行收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需要中断所有的用户线程,知道它回收结束为止,因此又号称“Stop The World” 的垃圾回收器。注意,JVM中文名称为java虚拟机,因此它就像一台虚拟的电脑一样在工作,而其中的每一个线程就被认为是JVM的一个处理器,因此大家看到图中的CPU0、CPU1实际为用户的线程,而不是真正机器的CPU,大家不要误解哦。

串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。

Serial收集器默认新旧生代的回收器搭配为Serial+ SerialOld

2、    ParNew收集器

ParNew收集器其实就是多线程版本的Serial收集器,其运行示意图如下



 

 

 

 同样有

Stop The World的问题,他是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收效率远远低于Serial收集器,所以一定要注意场景哦),也是Server模式下的默认收集器。

3、    ParallelScavenge

ParallelScavenge又被称为是吞吐量优先的收集器,器运行示意图如下



 

 

 ParallelScavenge

所提到的吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。在当今网络告诉发达的今天,良好的响应速度是提升用户体验的一个重要指标,多核并行云计算的发展要求程序尽可能的使用CPU和内存资源,尽快的计算出最终结果,因此在交互不多的云端,比较适合使用该回收器。

4、    ParallelOld

ParallelOld是老生代并行收集器的一种,使用标记整理算法、是老生代吞吐量优先的一个收集器。这个收集器是JDK1.6之后刚引入的一款收集器,我们看之前那个图之间的关联关系可以看到,早期没有ParallelOld之前,吞吐量优先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6之后,才能真正做到较高效率的吞吐量优先。其运行示意图如下



 

  

 5、

    SerialOld

SerialOld是旧生代Client模式下的默认收集器,单线程执行;在JDK1.6之前也是ParallelScvenge回收新生代模式下旧生代的默认收集器,同时也是并发收集器CMS回收失败后的备用收集器。其运行示意图如下



 

 6、

    CMS

CMS又称响应时间优先(最短回收停顿)的回收器,使用并发模式回收垃圾,使用标记-清除算法,CMS对CPU是非常敏感的,它的回收线程数=(CPU+3)/4,因此当CPU是2核的实惠,回收线程将占用的CPU资源的50%,而当CPU核心数为4时仅占用25%。他的运行示意图如下



 

 CMS

模式主要分为4个过程


  

 在初始标记的时候,需要中断所有用户线程,在并发标记阶段,用户线程和标记线程

并发执行,而在这个过程中,随着内存引用关系的变化,可能会发生原来标记的对象被释放,进而引发新的垃圾,因此可能会产生一系列的浮动垃圾,不能被回收。

CMS 为了确保能够扫描到所有的对象,避免在Initial Marking 中还有未标识到的对象,采用的方法为找到标记了的对象,并将这些对象放入Stack 中,扫描时寻找此对象依赖的对象,如果依赖的对象的地址在其之前,则将此对象进行标记,并同时放入Stack 中,如依赖的对象地址在其之后,则仅标记该对象。

在进行Concurrent Marking 时minor GC 也可能会同时进行,这个时候很容易造成旧生代对象引用关系改变,CMS 为了应对这样的并发现象,提供了一个Mod Union Table 来进行记录,在这个Mod Union Table中记录每次minor GC 后修改了的Card 的信息。这也是ParallelScavenge不能和CMS一起使用的原因。

CMS产生浮动垃圾的情况请见如下示意图



 

在运行回收过后,c就变成了浮动垃圾。

由于CMS会产生浮动垃圾,当回收过后,浮动垃圾如果产生过多,同时因为使用标记-清除算法会产生碎片,可能会导致回收过后的连续空间仍然不能容纳新生代移动过来或者新创建的大资源,因此会导致CMS回收失败,进而触发另外一次FULL GC,而这时候则采用SerialOld进行二次回收。

同时CMS因为可能产生浮动垃圾,而CMS在执行回收的同时新生代也有可能在进行回收操作,为了保证旧生代能够存放新生代转移过来的数据,CMS在旧生代内存到达全部容量的68%就触发了CMS的回收!

7、    GarbageFirst(G1 )

我们再来看垃圾回收器的总图,刚才我们可以看到,我在图上标记了一个?,其实这是一个新的垃圾回收器,既可以回收新生代也可以回收旧生代,SunHotSpot 1.6u14以上EarlyAccess版本加入了这个回收器,sun公司预期SunHotSpot1.7发布正式版,他是商用高性能垃圾回收器,通过重新划分内存区域,整合优化CMS,同时注重吞吐量和响应时间,但是杯具的是被oracle收购之后这个收集器属于商用收费收集器,因此目前基本上没有人使用,我们在这里也就不多介绍,更多信息可以参考oracle新版本JDK说明。

下面我们再来看下JVM的一些内存分配与回收策略

1、    优先在Edon上分配对象

代码示例

package com.yhj.jvm.gc.edenFirst;

/**

 * @Described:Edon优先划分对象测试

 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc

 * Edon s0 s1 old

 *   8  1   1  10

 * @author YHJ create at 2012-1-3 下午04:44:43

 * @FileNmae com.yhj.jvm.gc.edenFirst.EdonFirst.java

 */

public class EdonFirst {

    private final static int ONE_MB = 1024*1024;

    /**

     * @param args

     * @Author YHJ create at 2012-1-3 下午04:44:38

     */

    public static void main(String[] args) {

       @SuppressWarnings("unused")

       byte[] testCase1,testCase2,testCase3,testCase4;

       testCase1 = new byte[2*ONE_MB];

       testCase2 = new byte[2*ONE_MB];

       testCase3 = new byte[2*ONE_MB];

//     testCase1 = null;

//     testCase2 = null;

//     testCase3 = null;

       testCase4 = new byte[2*ONE_MB];

    }

}

运行结果



 

 结果分析

从运行结果我们可以很清晰的看到,eden有8MB的存储控件(通过参数配置),前6MB的数据优先分配到eden区域,当下一个2MB存放时,因空间已满,触发一次GC,但是这部分数据因为没有回收(引用还在,当赋值为null后则不会转移),数据会被复制到s0区域,但是s0区域不够存储,因此直接放入老生代区域,新的2MB数据存放在eden区域

2、    大对象直接进入老生代

代码示例

package com.yhj.jvm.gc.bigObjIntoOld;

/**

 * @Described:大对象直接进入老生代测试

 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc

 * Edon s0 s1 old

 *   8  1   1  10

 * @author YHJ create at 2012-1-3 下午05:28:47

 * @FileNmae com.yhj.jvm.gc.bigObjIntoOld.BigObjIntoOld.java

 */

public class BigObjIntoOld {

    private final static int ONE_MB = 1024*1024;

    /**

     * @param args

     * @Author YHJ create at 2012-1-3 下午04:44:38

     */

    public static void main(String[] args) {

       @SuppressWarnings("unused")

       byte[] testCase1,testCase2,testCase3,testCase4;

       testCase1 = new byte[8*ONE_MB];

//     testCase2 = new byte[2*ONE_MB];

//     testCase3 = new byte[2*ONE_MB];

//     testCase1 = null;

//     testCase2 = null;

//     testCase3 = null;

//     testCase4 = new byte[2*ONE_MB];

    }

}

运行结果



 

 结果分析

我们看到,没有触发GC日志,而数据是直接进入老生代的

3、    年长者(长期存活对象)进入老生代

代码示例:

package com.yhj.jvm.gc.longLifeTimeIntoOld;

/**

 * @Described:当年龄大于一定值的时候进入老生代  默认值15岁

 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=1-XX:+PrintGCDetails -verbose:gc

 * Edon s0 s1 old  age

 *   8  1   1  10   1

 * @author YHJ create at 2012-1-3 下午05:39:16

 * @FileNmaecom.yhj.jvm.gc.longLifeTimeIntoOld.LongLifeTimeIntoOld.java

 */

public class LongLifeTimeIntoOld {

    private final static int ONE_MB = 1024*1024;

    /**

     * @param args

     * @Author YHJ create at 2012-1-3 下午04:44:38

     */

    public static void main(String[] args) {

       @SuppressWarnings("unused")

       byte[] testCase1,testCase2,testCase3,testCase4;

       testCase1 = new byte[1*ONE_MB/4];

       testCase2 = new byte[7*ONE_MB+3*ONE_MB/4];

       testCase2 = null;

       testCase3 = new byte[7*ONE_MB+3*ONE_MB/4];

       testCase3 = null;

       testCase4 = new byte[ONE_MB];

    }

}

运行结果



 

 结果分析

从代码中我们可以看到,当testCase1划分为0.25MB数据,进行多次大对象创建之后,testCase1应该在GC执行之后被复制到s0区域(s0足以容纳testCase1),但是我们设置了对象的年龄为1,即超过1岁便进入老生代,因此GC执行2次后testCase1直接被复制到了老生代,而默认进入老生代的年龄为15。我们通过profilter的监控工具可以很清楚的看到对象的年龄,如图所示



 

 右侧的年代数目就是对象的年龄

4、    群体效应(大批中年对象进入老生代)

代码示例

package com.yhj.jvm.gc.dynamicMoreAVG_intoOld;

/**

 * @Described:s0占用空间到达50%直接进入老生代

 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=15-XX:+PrintGCDetails -verbose:gc

 * Edon s0 s1 old  age

 *   8  1   1  10  15

 *   0.5 0  0   7.5

 *   7.5 0.5 0  7.5

 *   7.5  0  0 8

 * @author YHJ create at 2012-1-3 下午05:50:40

 * @FileNmae com.yhj.jvm.gc.dynamicMoreAVG_intoOld.MoreAVG_intoOld.java

 */

public class MoreAVG_intoOld {

    private final static int ONE_MB = 1024*1024;

    /**

     * @param args

     * @Author YHJ create at 2012-1-3 下午04:44:38

     */

    public static void main(String[] args) {

       @SuppressWarnings("unused")

       byte[] testCase1,testCase2,testCase3,testCase4;

       testCase1 = new byte[7*ONE_MB+ONE_MB/2];

       testCase2 = new byte[ONE_MB/2];

       testCase3 = new byte[7*ONE_MB+ONE_MB/2];

       testCase3 = null;

       testCase4 = new byte[7*ONE_MB+ONE_MB/2];

      

//     testCase1 = new byte[7*ONE_MB+3*ONE_MB/4];

//     testCase2 = new byte[ONE_MB/4];

//     testCase3 = new byte[7*ONE_MB+3*ONE_MB/4];

      

    }

}

运行结果



 

 结果分析

我们看到,当创建后testCase3,testCase2被移动到s0区域,当被释放后,继续创建testCase3,按理说testCase2应该移动到s1区域,但是因为超过了s1区域的1/2,因此直接进入老生代

5、    担保GC(担保minorGC)

担保GC就是担保minorGC能够满足当前的存储空间,而无需触发老生代的回收,由于大部分对象都是朝生夕死的,因此,在实际开发中这种很起效,但是也有可能会发生担保失败的情况,当担保失败的时候会触发FullGC,但是失败毕竟是少数,因此这种一般是很划算的。

代码示例

package com.yhj.jvm.gc.securedTransactions;

/**

 * @Described:担保交易测试

 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc-XX:-HandlePromotionFailure  无担保

 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc-XX:+HandlePromotionFailure  有担保

 * Edon s0 s1 old  

 *   8  1   1  10  

 * @author YHJ create at 2012-1-3 下午06:11:17

 * @FileNmaecom.yhj.jvm.gc.securedTransactions.SecuredTransactions.java

 */

public class SecuredTransactions {

    private final static int ONE_MB = 1024*1024;

    /**

     * @param args

     * @Author YHJ create at 2012-1-3 下午04:44:38

     */

    public static void main(String[] args) {

       @SuppressWarnings("unused")

       byte[] testCase1,testCase2,testCase3,testCase4,testCase5,testCase6,testCase7;

       testCase1 = new byte[2*ONE_MB];

       testCase2 = new byte[2*ONE_MB];

       testCase3 = new byte[2*ONE_MB];

       testCase1 = null;

       testCase4 = new byte[2*ONE_MB];

       testCase5 = new byte[2*ONE_MB];

       testCase6 = new byte[2*ONE_MB];

       testCase4 = null;

       testCase5 = null;

       testCase6 = null;

       testCase7 = new byte[2*ONE_MB];

      

    }

}

 运行结果

1、  无担保

 

 2、



   有担保



 

 结果分析

我们可以很清楚的看到,当无担保的时候,触发了一次FullGC 而有担保的情况下,只有monorGC则完成了回收,大大提升了效率。

当我们注释掉对应的代码

//     testCase4 = null;

//     testCase5 = null;

//     testCase6 = null;

的时候,就会引发担保失败,如下图所示


 JVM

默认情况是是开启担保的,无需设置参数。

以上是 《深入Java虚拟机学习笔记》- 第9章 垃圾收集 的全部内容, 来源链接: utcz.com/z/392946.html

回到顶部