JAVA 之 GC 一
Stop-the-world
Stop-the-world会在任何一种GC算法中发生。Stop-the-world意味着 JVM 因为要执行GC而停止了应用程序的执行。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成。GC优化很多时候就是指减少Stop-the-world发生的时间。
后续为HotSpot虚拟机
在Java程序中不能显式地分配和注销内存。有些人把相关的对象设置为null或者调用System.gc()来试图显式地清理内存。设置为null至少没什么坏处,但是调用System.gc()会显著地影响系统性能,必须彻底杜绝.
在Java中,开发人员无法直接在程序代码中清理内存,而是由垃圾回收器自动寻找不必要的垃圾对象,并且清理掉他们。垃圾回收器会在下面两种假设(hypotheses)成立的情况下被创建(称之为假设不如改为推测(suppositions)或者前提(preconditions))。
- 大多数对象会很快变得不可达
- 只有很少的由老对象(创建时间较长的对象)指向新生对象的引用
HotSpot虚拟机将其物理上划分为两个–新生代(young generation)和老年代(old generation)。
新生代(Young generation): 绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minor GC“。
老年代(Old generation): 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为”major GC“(或者”full GC“)
持久代( permanent generation )也被称为方法区(method area)。他用来保存类常量以及字符串常量。因此,这个区域不是用来永久的存储那些从老年代存活下来的对象。这个区域也可能发生GC。并且发生在这个区域上的GC事件也会被算为major GC。
新生代的构成
为了更好地理解GC,我们现在来学习新生代,新生代是用来保存那些第一次被创建的对象,他可以被分为三个空间
- 一个伊甸园空间(Eden )
- 两个幸存者空间(Survivor )
比例大概为(8:1)
一共有三个空间,其中包含两个幸存者空间。每个空间的执行顺序如下:
- 绝大多数刚刚被创建的对象会存放在伊甸园空间。
- 在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。
- 此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
- 当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。
- 在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。
如果你仔细观察这些步骤就会发现,其中一个幸存者空间必须保持是空的。
老年代GC处理机制
老年代空间的GC事件基本上是在空间已满时发生,执行的过程根据GC类型不同而不同,因此,了解不同的GC类型将有助于你理解本节的内容。
JDK7一共有5种GC类型:
- Serial GC
- Parallel GC
- Parallel Old GC (Parallel Compacting GC)
- Concurrent Mark & Sweep GC (or “CMS”)
- Garbage First (G1) GC
其中,Serial GC不应该被用在服务器上。这种GC类型在单核CPU的桌面电脑时代就存在了。使用Serial GC会显著的降低应用的性能指标。
1. Serial GC (-XX:+UseSerialGC)
老年代空间中的GC采取称之为”mark-sweep-compact“的算法。
- 算法的第一步是标记老年代中依然存活对象。(标记)
- 第二步,从头开始检查堆内存空间,并且只留下依然幸存的对象。(清理)
最后一步,从头开始,顺序地填满堆内存空间,并且将对内存空间分成两部分:一个保存着对象,另一个空着(压缩)。
2. Parallel GC (-XX:+UseParallelGC)
serial GC只使用一个线程执行GC,而parallel GC使用多个线程,因此parallel GC更高效。这种GC在内存充足以及多核的情况下会很有用,因此我们也称之为”throughput GC“。
3. Parallel Old GC(-XX:+UseParallelOldGC)
Parallel Old GC分为三步:标记-汇总-压缩(mark – summary – compaction)。汇总(summary)步骤与清理(sweep)的不同之处在于,其将依然幸存的对象分发到GC预先处理好的不同区域,算法相对清理来说略微复杂一点。
4. CMS GC (-XX:+UseConcMarkSweepGC)
第一步初始化标记(initial mark) 比较简单。这一步骤只是查找那些距离类加载器最近的幸存对象。因此,停顿的时间非常短暂。在之后的并行标记( concurrent mark )步骤,所有被幸存对象引用的对象会被确认是否已经被追踪和校验。这一步的不同之处在于,在标记的过程中,其他的线程依然在执行。在重新标记(remark)步骤,会再次检查那些在并行标记步骤中增加或者删除的与幸存对象引用的对象。最后,在并行交换( concurrent sweep )步骤,转交垃圾回收过程处理。垃圾回收工作会在其他线程的执行过程中展开。一旦采取了这种GC类型,由GC导致的暂停时间会极其短暂。CMS GC也被称为低延迟GC。它经常被用在那些对于响应时间要求十分苛刻的应用之上。
当然,这种GC类型在拥有stop-the-world时间很短的优点的同时,也有如下缺点:
- 它会比其他GC类型占用更多的内存和CPU
- 默认情况下不支持压缩步骤
在使用这个GC类型之前你需要慎重考虑。如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world的时间要比其他任何GC类型都长,你需要考虑压缩任务的发生频率以及执行时间。
5. G1 GC
注:GC的开销通常很大,而且它的运行具有不确定性,微软的编程规范里是强烈建议你不要显式调用GC。但你的代码中还是可以使用framework中GC的某些方法进行手动回收,前提是你必须要深刻理解GC的回收原理,否则手动调用GC在特定场景下很容易干扰到GC的正常回收甚至引入不可预知的错误。
Eclipse GC日志打印 在Eclipse.ini加 -verbose:gc -Xloggc:E:\GClog\gc.log -XX:+PrintGCTimeStamps -XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:+PrintGCDateStamps (from JDK 6 update 4)
842.467: [Full GC [PSYoungGen: 576K->0K(265728K)] [ParOldGen: 275707K->275900K(388608K)] 276283K->275900K(654336K) [PSPermGen: 130853K->130853K(182784K)], 0.6767182 secs] [Times: user=1.56 sys=0.00, real=0.68 secs] 902.455: [GC [PSYoungGen: 11935K->672K(226816K)] 287836K->276572K(615424K), 0.0104957 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 902.466: [Full GC [PSYoungGen: 672K->0K(226816K)] [ParOldGen: 275900K->276282K(388608K)] 276572K->276282K(615424K) [PSPermGen: 133030K->133029K(177664K)], 0.5321486 secs] [Times: user=1.62 sys=0.01, real=0.53 secs] 975.834: [GC [PSYoungGen: 225792K->13615K(250368K)] 502074K->289897K(638976K), 0.0142747 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 1466.749: [GC [PSYoungGen: 86064K->2654K(215552K)] 362346K->288833K(604160K), 0.0528907 secs] [Times: user=0.14 sys=0.00, real=0.05 secs] 1466.802: [Full GC [PSYoungGen: 2654K->0K(215552K)] [ParOldGen: 286179K->197300K(354816K)] 288833K->197300K(570368K) [PSPermGen: 262143K->139027K(259584K)], 0.8853333 secs] [Times: user=2.06 sys=0.02, real=0.88 secs] 1870.452: [GC [PSYoungGen: 212480K->3950K(231936K)] 409780K->201251K(586752K), 0.0286043 secs] [Times: user=0.09 sys=0.00, real=0.03 secs] 2631.725: [GC [PSYoungGen: 210286K->2048K(202240K)] 407587K->202547K(557056K), 0.0225240 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 3336.501: [GC [PSYoungGen: 202240K->2144K(218112K)] 402739K->203955K(572928K), 0.0190907 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 4050.503: [GC [PSYoungGen: 196704K->3488K(192512K)] 398515K->205299K(547328K), 0.0188174 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 4755.967: [GC [PSYoungGen: 192416K->4576K(203264K)] 394227K->206387K(558080K), 0.0208226 secs] [Times: user=0.08 sys=0.00, real=0.02 secs] 5409.812: [GC [PSYoungGen: 188384K->6827K(185856K)] 390195K->208638K(540672K), 0.0135900 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 6050.587: [GC [PSYoungGen: 185515K->7051K(191488K)] 387326K->208862K(546304K), 0.0234516 secs] [Times: user=0.08 sys=0.00, real=0.02 secs] 6680.430: [GC [PSYoungGen: 181131K->7979K(177664K)] 382942K->209790K(532480K), 0.0244785 secs] [Times: user=0.08 sys=0.00, real=0.02 secs] 7297.651: [GC [PSYoungGen: 177451K->9003K(181760K)] 379262K->210814K(536576K), 0.0196988 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 7853.957: [GC [PSYoungGen: 173867K->15813K(176640K)] 375678K->217624K(531456K), 0.0232200 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 7892.117: [GC [PSYoungGen: 176581K->24054K(178688K)] 378392K->229934K(533504K), 0.0272496 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] GC日志说明: GC打印时间: [垃圾回收类型回收时间: [收集器名称: 年轻代回收前占用大小->年轻代回收后占用大小(年轻代当前容量), 年轻代局部GC时JVM暂停处理的时间] 堆空间GC前占用的空间->堆空间GC后占用的空间(堆空间当前容量) ,GC过程中JVM暂停处理的时间]。 垃圾回收类型:分为GC和Full GC. GC一般为堆空间某个区发生了垃圾回收, Full GC基本都是整个堆空间及持久代发生了垃圾回收,通常优化的目标之一是尽量减少GC和Full GC的频率。 收集器名称:一般都为收集器的简称或别名,通过收集器名称基本都能判断出那个区发生了GC。 DefNew:年轻代(新生代)发生了GC (若为DefNew可知当前JVM年轻代使用的串行收集器) ParNew:年轻代(新生代)发生了GC (若为ParNew可知当前JVM年轻代使用了并行收集器) Tenured:老年代发生了GC Perm:持久代发生了GC