理解GC日志
我们用昨天对象自我拯救的代码示例来看
VM args:-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
我们在其中一段一段的说
2019-12-11T14:38:51.807+0800(GC时间,+0800为东8区): 0.180(GC启动与JVM启动间隔): [GC(GC代表Young GC,Full GC就是本身) (System.gc()(触发GC的原因,这里是因为调用了System.gc())) [PSYoungGen(垃圾收集器名称): 8256K->1419K(76288K)新生代前/后/(总)量] 8256K->1427K(251392K)(整个堆前后总量), 0.0012191 secs(Gc持续时间)] [Times: user=0.00(CPU时间) sys=0.00(操作系统调用和系统等待消耗的时间), real=0.00 secs(应用暂停的时间)]
2019-12-11T14:38:51.809+0800: 0.181: [Full GC (System.gc()) [PSYoungGen: 1419K->0K(76288K)] [ParOldGen: 8K->1182K(175104K)] 1427K->1182K(251392K), [Metaspace: 3134K->3134K(1056768K)], 0.0049875 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
完成自救!
完成自我救赎!我还活着!
2019-12-11T14:39:01.816+0800: 10.188: [GC (System.gc()) [PSYoungGen: 6553K->592K(76288K)] 7736K->1782K(251392K), 0.0013452 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-12-11T14:39:01.817+0800: 10.189: [Full GC (System.gc()) [PSYoungGen: 592K->0K(76288K)] [ParOldGen: 1190K->1318K(175104K)] 1782K->1318K(251392K), [Metaspace: 3782K->3782K(1056768K)], 0.0083281 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
大业未成!
Heap
PSYoungGen total 76288K, used 3058K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000)
eden space 65536K, 4% used [0x000000076b180000,0x000000076b47cb08,0x000000076f180000)
from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000)
to space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000)
ParOldGen total 175104K, used 1318K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
object space 175104K, 0% used [0x00000006c1400000,0x00000006c1549bf0,0x00000006cbf00000)
Metaspace used 3789K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 413K, capacity 428K, committed 512K, reserved 1048576K
- Young GC
- Full GC
垃圾收集器参数总结(长期更新)
| 参数 | 描述 |
|---|---|
| UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
| UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收 |
| UseConcMarkSweepGC | 打开此开关后,使用ParNew+ CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
| UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old (PS Mark Sweep)的收集器组合进行内存回收 |
| UserParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
| SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Survivor = 8:1 |
| PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
| MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代 |
| UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
| HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况,JDK6以后这个参数失效 |
| ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
| GCTimeRatio | GC时间占总时间的比率,默认值是99, 即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效 |
| MaxGCPauseMillis | 设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效 |
| CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代时间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效 |
| UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效 |
| CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用CMS收集器时生效 |
| -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m | 设置元空间初始大小和最大大小 |
| -Xmx20m -Xms10m | 堆空间最大,堆空间初始 |
| -XX:MaxDirectMemorySize=10M | 直接内存最大空间 |
| -XX:+PrintGC | 输出GC日志 |
| -XX:+PrintGCDetails | 输出GC的详细日志 |
| -XX:+PrintGCTimeStamps | 输出GC的时间戳(以基准时间的形式)就是距离JVM启动的时间 |
| -XX:+PrintGCDateStamps | 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)就是开头的GC执行时间 |
| -Xss128k | 栈深度 |
| -Xmn | 新生代大小 |
| -XX:+/-UseTLAB | 设定JVM是否使用TLAB |
| -XX:+printFlagsFinal | 查看参数默认值 |
| -XX:+HeapDumpOnOutOfMemoryError | OOM生成dump |
| -XX:+HeapDumpOnCtrlBreak | 按住ctrl加break生成dump |
| -XX:MaxDirectMemorySize | 直接内存大小 |
| -XX:+DisableExpicitGC | 禁止手动System.gc() |
| -XX:+AlwaysTenure | 表示没有幸存区,所有对象在第一次gc时,会晋升到老年代 |
| -XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期间程序暂停的时间.可与上面混合使用 |
| -Xloggc:gc.log | 同目录下输出gclog |
| -XX:+PrintReferenceGC | 用来跟踪系统内的(softReference)软引用,(weadReference)弱引用,(phantomReference)虚引用,显示引用过程 |
内存分配与回收策略
Java中的自动内存管理最终可以归结解决了两件事,自动化的给对象分配内存以及回收对象的内存,之前介绍了很多如何回收,今天来讲讲如何分配
对象的分配
对象的内存分配,往大方向上讲,就是堆上分配,主要分配在新生代的Eden区,如果启动了本地线程缓冲,将按线程优先在TLAB上分配,少数情况下会直接分配到老年代中,分配的规则决定于使用哪种收集器,以及设定的JVM参数
TLAB (Thread local allocation Buffer)即每个线程在Java堆中预先分配一小块内存,哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定是否使用TLAB,可以使用 -XX:+/-UseTLAB 设定
大多数情况下,对象在新生代Eden中分配,当Eden区没有足够的空间进行分配,虚拟机将发起一次minor GC
MinorGC 或者叫YoungGC 发生在新生代的GC,因为Java大多数对象都朝生夕灭.所以MinorGC很频繁,一般回收速度也很快
FullGC 发生在老年代的GC,一般都会伴随至少一次的MinorGC,一般比MinorGC慢10倍以上
package com.practice.JVM.ObjectAllocation;
/**
* @author zhaoxu
* @version 1.0
* @className EdenToOld
* @description eden中空间不足,借老年代来担保
* @date 2019/12/11 15:59
* vm args -XX:+UseSerialGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8
**/
public class EdenToOld {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
//维护1个Byte数组,里面有1024*1024个Byte元素,自然是1MB
// Byte[] allocation1 = new Byte[2 * _1MB];
// Byte[] allocation2 = new Byte[4 * _1MB];
// Byte[] allocation3 = new Byte[6 * _1MB];
Byte[] allocation1 = new Byte[2 * _1MB];
Byte[] allocation2 = new Byte[2 * _1MB];
Byte[] allocation3 = new Byte[2 * _1MB];
Byte[] allocation4 = new Byte[4 * _1MB];
}
}
print
2019-12-11T16:33:39.897+0800: 0.170: [GC (Allocation Failure) 2019-12-11T16:33:39.897+0800: 0.170: [DefNew: 5188K->1024K(9216K), 0.0015865 secs]2019-12-11T16:33:39.898+0800: 0.171: [Tenured: 8357K->9380K(10240K), 0.0043435 secs] 13380K->9380K(19456K), [Metaspace: 3195K->3195K(1056768K)], 0.0059865 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2019-12-11T16:33:39.903+0800: 0.176: [Full GC (Allocation Failure) 2019-12-11T16:33:39.903+0800: 0.176: [Tenured: 9380K->9332K(10240K), 0.0037593 secs] 9380K->9332K(19456K), [Metaspace: 3195K->3195K(1056768K)], 0.0037828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.practice.JVM.ObjectAllocation.EdenToOld.main(EdenToOld.java:21)
Heap
def new generation total 9216K, used 330K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 4% used [0x00000000fec00000, 0x00000000fec52ac0, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 9332K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 91% used [0x00000000ff600000, 0x00000000fff1d2c0, 0x00000000fff1d400, 0x0000000100000000)
Metaspace used 3285K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 1
程序的结果与书中完全不同,我是用了Serial收集器,不知道是什么占用了很多内存空间
大对象直接进入老年代
大对象是指,需要大量连续内存空间的Java对象,最典型的是那种,很长的字符串和数组 使用-XX:PretenureSizeThreshold=111B,来让大对象直升老年代 注意:这个参数只对Serial与ParNew有效,PS不认识
长期存活进入老年代
虚拟机给每个对象定义了一个Age计数器,当对象在Eden出生,经过一次minorGC,还能被Survivor容纳,那么对象每经过一次minorGC,Age就+1,默认参数-XX:MaxTenuringThreshold=15,当设置为1时,age为1的,在Survivor中的对象在下次GC中进入老年代
动态对象年龄判定
为了更好的适应不同的情况,虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringThreshold,如果Survivor空间中相同年龄的 所有对象大小的总和大于Survivor的一半,年龄大于或等于该年龄的对象可以直接进入老年代
如Survivor区总大小1024KB,其中两个300KB的对象,Age都为2,他俩可以直升老年代,如果只有一个为2,就不会直升老年代
空间分配担保
在发生MinorGC前,JVM会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,即便在极端情况下(新生代所有对象全部存活),这次MinorGC也是安全的
如果不成立,JVM会查看HandlePromotionFailure设置值是否允许担保失败 如果允许失败,继续检查老年代最大可用连续空间,是否大于历次晋升到老年代对象的平均大小 如果大于,尝试进行一次minorGC,尽管这次GC是有风险的,进行如果成功,即成功,如果失败,那么在失败后发起一次FullGC
如果小于,或者不允许冒险,那么改为一次FullGC
虽然说担保失败绕的圈子是最大,但是大部分情况还是允许担保失败,避免FullGC过于频繁
在JDK6之后,这个参数就失效了
现在的规则是,只要老年代连续空间大于新生代对象的总大小或者历次晋升的平均总大小就会进行MinorGC,否则将会进行FullGC.