memory leak:未回收的垃圾
oom:memory leak达到一定程度无法再分配内存
一、判断垃圾
- 引用计数
无法解决循环引用问题,比如A->B->C->A。python是这样判定的
- Root可达判定
GC Roots:
- 方法区类静态属性
- 方法区常量
- 虚拟机栈局部变量表
- 本地方法栈JNI

引用:有4种,引用强度依次减弱
- 强引用,只要存在就不会被回收
- 软引用,系统将要发生OOM前,把这些对象列到回收范围进行第二次回收,若还没有足够的内存则OOM
- 弱引用,GC只要工作就会回收这些对象
- 虚引用,不会影响对象生命周期,不能通过虚引用取得一个对象实例,唯一目的是能在虚引用对象被回收时收到一个系统通知
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行",若有必要执行,则会调用对象的finalize(),此时是对象的最后一次自救机会
二、GC算法
堆回收:
- 分代算法:mark-sweep、copy、mark-compact,分新生代和老年代,各自用不同的GC算法
- 分区算法:适用于大内存,不同区用同一种GC算法,分区内部分代
方法区回收:
- 废弃常量:如果没有对象引用该常量,即回收
- 无用类:同时满足以下三条件,根据-Xnoclassgc控制是否回收
- 该类所有实例已被回收
- 加载该类的ClassLoader已被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
1. GC Root扫描
在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了,这样可以很快完成GC root扫描
2. 如何发起GC
为防止使用OopMap时出现引用关系变化影响GC,因此就要停顿下来,但为了防止影响效率,HotSpot没有为每条指令都生成OopMap,只是在"特定的位置"记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
如何在GC发生时让所有线程都跑到安全点再停顿下来?主要使用抢断式中断,不需要线程的执行代码主动配合,在GC发生时首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就继续运行直至到安全点。但当线程处于sleep或blocked状态时,线程无法响应中断,所以扩展安全点到安全区域,安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。线程执行到安全区域时首先标记自己进入了此区域,GC发生时这些线程会正常停顿,当线程要离开此区域前先检查是否完成了GC root扫描,只有完成扫描了才可以离开
3. 典型的分代算法
分配内存的规则:
- 对象优先在新生代的Eden分配
- 大对象直接进入老年代(-XX:PretenureSizeThreshold 默认是0即任何情况下都先分到Eden区,也可设置一个大于0的值表示大于此值时直接分到老年代)
- 长期存活的对象将进入老年代(-XX:MaxTenuringThreshold 默认经过15次GC仍未回收的)
- 动态年龄判断(相同年龄的总和大于Survivor一半,年龄不小于此的对象直接进入老年代)
- Young GC后存活对象大于Survivor,直接放到老年代
- 老年代空间分配担保(依次进行两类判断,在执行任何一次Young GC之前,先检查一下老年代可用内存是否大于新生代所有对象的总大小。如果大于则可放心Young GC,如果小于看-XX:HandlePromotionFailure,若为false则直接Full GC,若为true则检查老年代可用内存是否大于之前每次Young GC后进入老年代的平均大小,若小于则Full GC,若大于则Young GC,存活对象若大于老年代可用内存则又Full GC。若还是不够用则触发OOM)

Minor GC:新生代GC
Full GC/Major GC:老年代GC,会产生stop-the-world

整体详细过程:
部分对象可以直接分配到栈上,栈帧pop后直接回收而不需要GC复杂算法,效率很高,但需要满足两个条件:1、通过逃逸分析;2、标量替换
TLAB(Thread Local Allocation Buffer 线程本地分配缓冲区)如果直接把新生成的对象放到Eden区会产生竞争,为分配对象时提升效率,给每个线程分配个TLAB,每个线程的对象优先分配到这里

三、GC垃圾回收器

1. serial、serialOld
单线程,使用copying算法,只适用于小内存比如几K到几十M
2. parallel scavenge、parallel marksweep
多线程,java8默认,适用于几G
新生代和老年代的默认比例是1:2
3. ParNew、CMS (concurrent mark sweep)
适用于几十G
使用ParNew和CMS垃圾回收器,ParNew与Parallel scavenge类似,只是为了配合CMS增强了部分功能而已。
新生代和老年代的默认比例是1:2,ParNew默认使用与CPU核心数相同的线程数并发处理新生代,CMS默认启动的垃圾回收线程数是(CPU核心数+3)/4 处理老年代
并发、低STW,第一种支持GC与工作线程并发的垃圾回收器,有很多缺陷,未被任何一种JVM作为默认垃圾回收器
过程:
- 初始标记:STW,只标记root可达的第一级对象,工作量小,因此STW很短
- 并发标记:工作线程与GC线程并发,使用三色标记算法,容易出现两种情况:1.浮动垃圾,标记时不是垃圾后来又变成垃圾,会在下次GC时被清除,不影响GC正确性;2.漏标,灰白消失黑白出现,标记时是垃圾后来不是垃圾,出现野指针
- 重新标记:STW,为应对并发标记漏标,CMS使用Incremental Update重新标记黑变灰的节点
- 并发清理:清理垃圾对象(白色对象),产生碎片
并发标记阶段产生漏标的原因:不同垃圾回收线程间会存在覆盖标记的现象,比如两个GC线程分别处理一个对象两属性,A1属性指向D后,GC1将A变灰,GC2扫描完A2属性后,将A对象置黑,覆盖了GC1的标记,此时会漏标D


Concurrent Mode Failure问题:
老年代根据-XX:CMSInitiatingOccupancyFaction默认预留8%内存给并发清理期间新进入老年代的对象,若预留空间仍不够用则会出现Concurrent Mode Failure问题,自动使用Serial Old替换CMS执行STW,重新进行单线程GC Roots追踪并回收垃圾,系统卡死的时间可能很长
内存碎片问题:
标记清理算法会产生很多内存碎片。-XX:+UseCMSCompactAtFullCollection默认打开,即Full GC后STW进行碎片整理。-XX:CMSFullGCsBeforeCompaction,指定执行多少次Full GC后进行碎片整理,默认每次执行Full GC后都碎片整理,两个参数一般不用改
4. G1
逻辑分代,物理不分代,适于于上百G大内存
把JVM堆内存分为多个大小相等的Region,且某些Region属于新生代,某些属于老年代,是动态变化的
JVM默认分为2048个Region,也可通过-XX:G1HeapRegionSize指定Region大小,Region大小必须是2的整数倍
新生代一开始占堆内存的5%,可通过-XX:G1NewSizePercent设置初始占比,运行过程中会不断增大,但默认最多不超过60%,可通过-XX:G1MaxNewSizePercent设置最大比例
可设置垃圾回收的预期停顿时间,G1做到这点需要跟踪每个Region的回收价值,回收价值即单位时间可回收的内存量,尽可能地保证STW时间在停顿时间范围内
Minor GC:与ParNew类似,不过大对象不是到老年代,而是超过Region大小的50%的对象作为大对象,进行特殊处理,如果对象过大,可以横跨多个Region存储
Full GC:-XX:InitiatingHeapOccupancyPercent默认45%,老年代占堆内存超过45%时触发Full GC。使用三色标记算法,不过在重新标记阶段,追踪黑白新生的连接,然后将白标黑下次再扫描。在并发清理阶段,使用复制算法而非CMS的标记清理算法那样不会产生内存碎片,-XX:G1HeapWastePercent默认是5%,基于复制算法把Region存活的对象放入其他Region,然后清除掉本来的Region。那么当空闲的Region数量达到堆内存的5%,就会立即停止混合回收,-XX:G1MixedGCLiveThresholdPercent默认是85%,要回收的Region存活对象必须少于85%才可以被回收掉,否则复制成本会很高。如果在复制的时候发现没有空闲的Region可以承载存活的对象,那么会触发失败,立马停止系统进程,采用单线程进行标记、清理和压缩整理,空闲出一批Region,这个过程是极慢的

四、三色标记算法 白灰黑
标记:
标记结束后未标到的是垃圾,如果漏标了说明多标了垃圾
三色:
白色:未被标记(若重新标记完成时仍是白色则是垃圾)
灰色:自身被标记,指向的对象存在未标记的
黑色:自身、指向的对象均完成标记
漏标的对策:
CMS:Incremental update 增量更新,黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。重新标记的时候会暂停线程(STW)重新扫描变灰的旧黑对象
G1:SATB(Snapshot At the Beginning) 原始快照,当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色,目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾
五、记忆集与卡表
所有涉及部分区域收集(Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC和Shenandoah收集器, 都会面临跨代、跨区引用的问题,如果再去跨区域扫描,效率会很低
记忆集(Remember Set):存储某一块非收集区域是否存在指向收集区域的指针
卡表(Card Table):hotspot的记忆集实现,每个元素对应着其标识的内存区域一块特定大小的内存块,称为"卡页",hotSpot使用的卡页是2^9大小,即512字节
六、jvm参数调优
调优:
- 预调优:根据需求进行jvm规划和优化
- 优化jvm运行环境(cpu满载,可以用jstask查看线程情况,为了方便排查,开发代码时要定义线程池里每个线程的名称)
- 解决各种OOM问题,(tcpdump把流量复制一份到测试环境进行真实测试)
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
1、标准参数
-开头,任何版本都有
-version
-verbose:gc 查看内存回收情况,与-XX:+PrintGC功能一样,只是这个是稳定版本
-verbose:class 查看类加载情况
-verbose:jni 查看本地方法调用情况
2、非标参数
-X开头,有可能将来会替换
java -X -help 查看所有同类参数-
-Xms:最小内存:注意如果不设置最小内存,默认会设为物理内存的64分之一,建议设成与Xmx一致,避免因所使用的Java堆内存不够导致频繁full gc以及full gc中因动态调节Java堆大小而耗费延长其周期
-Xmx:最大内存
-Xoss:设置本地方法栈大小(HotSpot虚拟机不区分虚拟机栈和本地方法栈,无效)
-Xss:设置虚拟机栈大小(HotSpot中即虚拟机栈和本地方法栈总大小)
-Xmn:新生代
-Xint:禁止编译器运行,强制使用纯解释方式执行字节码,尽量不要用
-Xloggc:../gclogs/gc.log.date:指定 GC log 的路径
-Xverify:none:禁止字节码验证,在代码稳定时可以用
3、-XX
java -XX:+PrintCommandLineFlags -version 查看jvm默认启动参数
-XX:+PrintFlagsFinal 所有-XX参数当前值
-XX:+PrintFlagsInitial 所有-XX参数默认值
-XX:+HeapDumpOnOutOfMemoryError:JVM 就会在发生OOM时抓拍下当时的内存状态,也就是我们想要的堆转储文件。
-XX:+HeapDumpOnCtrlBreak:不想等到发生崩溃性的错误时才获得堆转储文件
-XX:+TraceClassLoading:追踪类加载过程
打印GC:
- -XX:+PrintGC:打印GC日志
- -XX:+PrintGCDetails:打印 GC 详情,包括 GC 前/内存等。
- -XX:+PrintGCDateStamps:打印GC的时间戳(以日期的形式,如 2017-09-04T21:53:59.234+0800)
- -XX:+PrintTenuringDistribution:打印 GC 发生时的代龄信息。
- -XX:+PrintGCApplicationStoppedTime:打印 GC 停顿时长
- -XX:+PrintGCApplicationConcurrentTime:打印 GC 间隔的服务运行时长
垃圾回收器:
- -XX:+UseSerialGC:使用Serial和Serial Old收集器组合进行内存回收
- +XX:+UseG1GC:使用G1
- -XX:+UseParallelGC:使用Parallel Scanvenge和Parallel MarkSweep分别清理新生代和老年代
- -XX:+UseParNewGC:使用ParNew作为新生代垃圾回收器
- -XX:+UseConcMarkSweepGC:使用CMS作为老年代垃圾回收器
- -XX:PretenureSizeThreshold=3145728:设置Serial、ParNew收集器的大对象最小字节数
- -XX:MaxTenuringThreshold:对象晋升老年代的年龄阈值
- -XX:SurvivorRatio=8:GC的复制算法中Survivor区域占总区域(2*Survivor+Eden)的1/(1+1+8)
- -XX:+DisableExplicitGC:屏蔽System.gc()
方法区:
- -verbose:class -XX:+TraceClassLoading、-XX:+TraceClassUnLoading:查看类加载和卸载信息
- -XX:PermSize:方法区大小
- -XX:MaxPermSize:最大方法区大小
- -XX:MaxMetaspaceSize:1.8元数据区
指针压缩:
不再保存所有引用,而是每隔8个字节保存一个引用,当引用被存入64位的寄存器时,JVM将其左移3位(相当于末尾添加3个0),当从寄存器读出时,JVM又可以右移3位,丢弃末尾的0
- -XX:+UseCompressedClassPointers
- -XX:+UseCompressedOops
七、命令行工具
1. jstack
打印堆栈信息
"Thread-1" 是线程的名字,prio 是线程的优先级,tid 是线程id, nid 是本地线程id, waiting to lock 等待去获取的锁,locked 自己拥有的锁。
"Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.itdragon.keyword.ITDragonDeadLock.rightLeft(ITDragonDeadLock.java:37)
- waiting to lock <0x00000000ecfdf9d0> (a java.lang.Object)
- locked <0x00000000ecfdf9e0> (a java.lang.Object)
at com.itdragon.keyword.ITDragonDeadLock$2.run(ITDragonDeadLock.java:54)
at java.lang.Thread.run(Thread.java:748)
2. jps
jps 进程名
jps -m main方法接受的参数
jps -v jvm参数
3. jinfo
jinfo pid
java进程信息
4. jstack
jstack pid
列出进程里所有的线程
5. jmap
jmap pid
-histo:live 查看堆情况,每个类对应的实例数量和占用的字节数。只要用就会触发Full GC
-dump:format=b,file=heap dump hprof格式的堆内存
-heap 显示堆内存概况
-clstats 类加载器统计信息
6. jcmd
jcmd pid
Thread.print, 打印线程栈信息
GC.class_histogram, 查看系统中类统计信息
GC.heap_dump, 导出堆信息,与jmap -dump功能一样
GC.run_finalization, 触发finalize()
GC.run, 触发Full gc()
VM.uptime, VM启动时间
VM.flags, 获取JVM启动参数
VM.system_properties, 获取系统Properties
VM.command_line, 启动时命令行指定的参数
VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB] 查看堆外内存,需要首先开启-XX参数启用堆外内存追踪,-XX:NativeMemoryTracking=[off | summary | detail],需要注意的是会造成5至10的性能损失
7. jhat
分析jvm heapdump文件,建立一个http服务器方便用户访问
8. javap
分析class文件字节码
javap -verbose Test.class
9. jstat
jstat -gc pid 静态输出统计
jstat -gc pid 1000 每1000毫秒输出统计
sudo -u impala jstat -gcutil $(pgrep -f IMPALAD) 10000
单位:容量KB

- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- OC:老年代大小
- OU:老年代使用大小
- MC:方法区大小
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:当前老年代大小
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- TT:对象在新生代存活的次数
- MTT:对象在新生代存活的最大次数
- DSS:期望的幸存区大小
- LGCC:最近一次GC的原因
- Compiled:最近编译方法的数量
- Size:最近编译方法的字节码数量
- Type:最近编译方法的编译类型。
- Method:方法名标识。
- Loaded:加载class的数量
- Bytes:所占用空间大小
- Unloaded:未加载数量
- Bytes:未加载占用空间
- Time:时间
10. pmap
报告进程的内存映射情况
pmap -x pid | sort -n -k3