JVM内存问题分析
String str1="hello"; String str2="he"+new String("llo"); System.out.println(str1==str2);//false
1、两个或者以上的字符串常量相加,【String str="s1"+"s2"】,在预编译的时候“+”会被优化,相当于把两个或者两个以上字符串常量自动合成一个字符串常量
2、字符串的+操作本质上是new了StringBuilder对象进行append操作,拼接后调用toString()返回String对象(可通过javap -c xxx.class查看字节码指令)
Code: 0: ldc #10 // String hello 2: astore_1 3: new #11 // class java/lang/StringBuilder 6: dup 7: invokespecial #12 // Method java/lang/StringBuilder."<init>":()V 10: ldc #13 // String he 12: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: new #15 // class java/lang/String 18: dup 19: ldc #16 // String llo 21: invokespecial #17 // Method java/lang/String."<init>":(Ljava/lang/String;)V 24: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 27: invokevirtual #18 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
一、GC日志分析
为了在内存溢出时排查原因,可以在JVM启动时加一些参数来控制,当JVM内存出问题时可以通过分析记录下来的GC日志,GC的频率和每次GC回收了哪些内存
GC的日志输入有以下参数
1、-verbose:gc 可以辅助输出一些详细的GC信息
2、-XX:+PrintGCDetails 输出GC的详细信息
3、-XX:+PrintGCApplicationStoppedTime 输出GC造成应用程序暂停的时间
4、-XX:+PrintGCDateStamps 输出GC发生的时间信息
5、-XX:PrintHeapAtGC 在GC前后输出堆中各个区域的大小
6、-Xloggc:[file] 将GC信息输出到单独的文件
每种GC方式输出日志的形式不同,除CMS的日志和其他GC方式差异较大外,其余GC方式的日志可以抽象成如下方式
[GC [<collector>: <starting occupancy1> -> <ending occupancy1> (total size1) , <pause time1> secs]
<starting occupancy2> -> <ending occupancy2> (total size2) , <pause time2> secs] ]
说明如下
1、<collectot>GC 表示垃圾收集器的名称
2、<starting occupancy1> 表示Young区在GC前占用的内存
3、<ending occupancy1> 表示Young区在GC后占用的内存
4、(total size1) 表示Young区的总内存大小
5、<pause time1> 表示Young区局部收集时JVM暂停处理的时间 secs表示单位秒
6、<starting occupancy2> 表示Heap在GC前占用的内存
7、<ending occupancy2> 表示Heap在GC后占用的内存
8、(total size2) 表示Heap的总内存
9、<pause time2> 表示在GC过程中JVM暂停处理的总时间
可以根据日志来判断是否存在内存泄漏的问题:
<starting occupancy1> - <ending occupancy1> 和 <starting occupancy2> - <ending occupancy2> 比较
1、如果前者差等于后者差,表明Young区GC 对象100%被回收,没有对象进入 Old区或者Perm区
2、如果前者大于后者,那么差值就是这次GC对象进入Old或者Perm区的大小
如果随着时间的的延长,<ending occupancy2>的大小一直在增长,而且Full GC很频繁,那么很可能就是内存泄漏导致的。
二、堆快照文件分析
1、通过命令 jmap -dump:format=b,file=[filename][pid] jmap(Memory Map for Java)
来记录下堆的内存快照,然后利用第三方工具如eclipse 插件MAT来分析整个Heap的对象关联情况。
如果内存耗尽可直接导致JVM退出,可以通过参数
-XX:+HeapDumpOnOutOfMemoryError 来配置当内存耗尽时记录下当时的内存快照
-XX:HeapDumpPath 指定内存快照文件的路径 文件快照的名称格式为 java_[pid].hprof
如果是OOM,可能有两方面的原因
1、内存分配过小,不满足程序运行所需要的内存
2、内存泄漏(FullGC频繁,回收后Heap占用的内存不断增长)
三、JVM Crash 日志分析
TODO
垃圾收集器与内存分配策略
栈的内存随着方法的结束和线程结束自动回收,因此Java堆和方法区是垃圾收集器所关注的内存
判断对象是否可以回收
1、 引用计数法:给对象中添加一个引用计数器,当有一个地方被引用时加1,引用失效减1,计数器为0的就是可以回收的,但是会有互相引用的情况
2、可达性分析法 对象到一系列称为GC Roots的对象有没有引用链相连
即使在可达性分析法中不可达的对象,也至少要经历两次标记过程
第一次标记:可达性分析后无与GC Roots相连的引用链
第二次标记:第一次标记后筛选(finalize()方法没有被JVM调用过)后放置在F-Queue队列中,仍无引用链和GC Roots相连则进行第二次标记
方法区的收集:废弃常量和无用的类
废弃的常量:如常量池中的字符串常量“abc”,没有String对象引用常量池的这个“abc”常量,那么abc就是废弃常量可以移除常量池
无用的类:1、该类的实例都被回收 2、加载该类的ClassLoader已被回收 3、该类的Class对象没有在任何地方被引用,也就是无法通过反射访问该类的方法
垃圾收集算法
1、复制
将内存划分为大小相等的两块,每次只使用其中一块,当其中的一块用完了将其上面存活的对象复制到另一块上面,然后把使用过的内存空间一次清理掉。缺点是将可用内存缩小为了原来的一半,对象存活率较高时不适合使用。
新生代中的对象98%都是朝生夕死的,因此新生代按照8:1:1的比例分为了eden,survivor from 和survivor to空间,每次回收将eden和survivor from中存活的对象复制到survivor to中,不够的话再放到old中,然后将eden,survivor from一次清除掉。
2、标记-清除
首先标记需要回收的对象,在标记完成后统一回收 问题1、效率问题:标记和清除效率都不高 2、空间问题:清除后会产生大量内存碎片,过多的话会导致以后分配大对象如数组找不到一块连续的内存而提前触发一次GC
3、标记-整理
首先标记需要回收的对象,然后将所有存活的对象向一侧移动与将要回收的对象分隔开,然后将要回收的对象一次清理掉,适合用再老年代上。
分代收集
新生代每次垃圾回收都有大量的对象死去少量存活,只需付出少量对象的复制成本即可完成收集。采用复制算法
老年代对象存活率高,没有额外的空间做担保, 只能采用标记-清除或者标记-整理算法
新生代垃圾收集器:Serial、ParNew、ParallelScavenge、G1
老年代垃圾收集器:CMS、Serial Old(MSC)、Parallel Old、G1
垃圾收集器的发展,使用户线程的停顿时间在不断缩短,但是仍没办法完全消除,因此寻找更优秀的垃圾收集器仍在继续!
Serial收集器:单线程,采用复制算法,而且进行垃圾收集时,必须暂停JVM其他所有的工作进程,直到它收集结束。仍是Client模式下虚拟机新生代默认收集器
ParNew收集器:Serial的多线程版本,采用复制算法,其他基本相同。是运行在server模式下的虚拟机首选的新生代收集器
Parallel Scavenge收集器:与其他收集器关注点在缩短用户线程停顿时间不同,它关注点是达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行代码时间+垃圾收集时间)如:JVM总运行100分钟,垃圾收集1分钟,那吞吐量=99%,如果新生代采用了此收集器,那老年代只能使用Serial Old收集器
Serial Old收集器:Serial收集器的老年代版本,同样单线程,采用标记-整理算法,存在意义是给Client客户端JVM使用
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,采用多线程和标记-整理算法
CMS收集器(Concurrent Mark Sweep):一种以获取最短回收停顿时间为目标的收集器,基于标记- 清除算法实现。(只会收集老年代和永久代,1.8后改为元空间(需要设置 CMSClassUnloadingEnabled)),不会收集年轻代
1、初始标记 标记GCRoots直接关联的对象
2、并发标记 往下跟踪标记所有与GCRoots有引用链可达的对象 (可与用户线程同时工作),就是进行GCROOTS Tracing的过程
3、重新标记 修正并发标记期间因用户线程运行而导致的标记变动的一部分对象
4、并发清除 清除未标记的对象 (可与用户线程同时工作)
缺点:
1、虽然在并发阶段可与用户线程同时工作,但是会占用CPU资源,导致应用程序变慢,总吞吐量会降低
2、无法处理浮动垃圾,即在并发清除阶段新产生的垃圾,只有留待下一次GC时再清理掉
3、使用标记-清除算法,会有大量内存碎片产生
G1收集器(Garbage-First):特点:
1、并行与并发:充分利用多CPU,多核环境的硬件优势,来缩短停顿时间,在GC期间可通过并发的方式让Java程序继续执行
2、分代收集:采用不同的算法去收集刚创建的对象,存活了一段时间的对象和熬过多次GC的对象,以获取更好的收集效果
3、空间整合:整体基于标记-整理算法,内部region之间采用复制算法,都不会产生内存空间碎片
4、可预测的停顿:除了追求短时间停顿外,还建立了可预测停顿模型,使在M毫秒内,在垃圾收集上的时间不超过N毫秒
G1逻辑上将整个Java堆划分为多个大小相等的独立区域(region)。仍保留新生代和老年代的概念,但它们之间不是物理隔离了,新生代和老年代都是一部分region的集合了。G1之所以
能建立可预测停顿模型,因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的最优),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region,这也是Garbage-First的由来。这种使用region划分内存空间以及有优先级的区域回收方式保证了G1在有限的时间内可获取尽可能高的收集效率。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,
虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象
引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remember Set即可保
证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
1、初始标记
2、并发标记
3、最终标记
4、筛选回收
内存分配与回收策略
Minor GC 新生代GC:一般比较频繁,回收速度也比较快
Major GC/Full GC 老年代GC:调用System.gc() 强制执行GC为Full GC
(Full GC停顿时间比Minor GC高几个量级,一般为50倍以上)
对象优先在Eden区上分配,Eden区上没有足够的空间分配时,触发一次Minor GC(新生代GC),将存活的对象复制进Survivor to区,若Survivor to区没有足够的空间存放,则通过分配担保机制将对象转移到老年代中。同时,经过一次Minor GC进入到survivor to区的对象,年龄计数器设为1,在Survivor from区的对象每经过一次Minor GC,年龄加1,当年龄增加到 -XX:MaxTenuringThreshold 设定的阀值(默认15)或者在Survivor区中有相同年龄的所有对象大小总和大于Survivor区大小的一半,那么大于这个年龄的对象,将会被移动到老年代中。
每进行Minor GC之前,在允许担保失败的情况下,JVM将查看老年代中最大可用连续空间是否大于历次minor GC晋升到老年代的对象的平均大小,如果大于,将进行一次Minor GC;如果minor GC后老年代空间不足,则紧接着触发Full GC,如果小于,则直接触发Full GC。(新生代和老年代的比例默认是1:2)
(HandlePromotionFailure设置是否允许担保失败(默认允许),如果不允许担保失败,那么每次Minor GC前JVM查看老年代中最大的连续空间是否大于新生代所有对象的大小总和,如果小于,则直接触发Full GC)
大的对象可能直接进入老年代,避免在Eden区和两个Survivor区之间发生大量的内存复制。典型的大对象是那种很长的字符串对象或者数组。超过 -XX:PretenureSizeThreshold参数配置的大小的对象直接在老年代分配内存。
JVM性能监控和故障处理
通过工具导出和处理分析 运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(headdump/hprof文件)等
jps:JVM process status tool,显示指定系统内所有的HotSpot虚拟机进程
jstat:JVM statistics Monitoring Tool,收集HotSpot虚拟机各方面的运行数据
jinfo:Configuration Info for java,显示虚拟机配置信息
jmap:Memory map for Java,生成虚拟机的内存转储快照(heapdump文件)
jhat:JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个http/html服务器,让用户可以在浏览器上查看分析结果
jstack:Stack trace for Java,显示虚拟机的线程快照
jps:和linux中ps命令相似,可列出正在运行的虚拟机进程,并显示虚拟机执行主类和这些进程的唯一ID(Local Virtual Machine Identifier LVMID)
格式:jps [option] [hostid(主机名)]
参数:-l 输出主类的全路径 -v 输出虚拟机进程启动是的JVM参数
jstat:用于监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、JIT编译等运行数据,是定位虚拟机性能问题的首选工具。
格式:jstat [ option vmid [interval] [count] ]
interval和count是查询间隔和次数,如果忽略这两个参数则只查询一次
选项:-class 监视类装载、卸载数量、总空间以及类装载消耗时间
-gc 监视Java堆状况,包含Eden区,两个Survivor区、老年代、永久代等的容量,已用空间,GC时间等信息。
jmap:Java内存映射工具,用于生成堆存储快照heapdump,生成堆快照还可以通过设置参数使在OOM异常之后自动生成堆dump文件。
jmap还可查询finalize执行队列、Java堆和永久代的详细信息(空间使用率,使用哪种收集器等)。
格式:jmap [option] vmid
option参数
-dump:format=b,file=[filename] 生成堆转储快照
-heap 显示Java堆详细信息,如使用哪种收集器、参数配置、分代状况等。
jhat:与jmap搭配使用,分析jmap生成的堆转储快照文件。一般不会直接使用jhat命令分析dump文件,一是不会直接在应用服务器上分析dump文件,因为分析耗时耗资源,二是jhat分析结果比较简陋,可用VisualVM,MAT等工具
jstack:Java堆栈跟踪工具,用户生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成快照的主要目的是定位线程出现长时间等待的原因,如线程间死锁、死循环、请求外部资源(如sql)导致的长时间等待等。
线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情或者等待什么资源
格式:jstack [option] vmid
option参数,-F:强制输出线程堆栈
-l:除堆栈外显示关于锁的附加信息
-m:可显示调用本地方法的堆栈
JDK1.5之后的Thread类新增了getAllStackTraces()方法用户获取虚拟机中所有线程的StackTraceElement对象。和jstack功能类似
public static Map<Thread,StackTraceElement[]> getAllStackTraces() 返回从 Thread 到 StackTraceElement 数组的一个 Map,代表相应线程的堆栈跟踪。 、
总结如果要定位OOM问题使用jps和jmap组合命令,先用jps或者linux的ps命令查看虚拟机进程的vmid,然后用jamp命令生成堆快照文件,最后使用工具分析dump文件定位问题。
如果要定位线程响应时间过长的问题,使用jps和jstack命令,先用jps或者linux的ps命令查看虚拟机进程的vmid,然后用jstack命令查看线程堆栈信息
JVM调优
JVM调优是通过分析GC日志等来分析java内存和垃圾回收的情况,来调整各内存区域内存占比和垃圾回收策略。
充分使用系统资源,减少GC停顿时间和停顿次数,由于Full GC的停顿时间远比Minor GC的停顿时间长,因此要控制Full GC的频率。
控制Full GC的频率的关键是看应用中的绝大多数对象是否符合“朝生夕灭”的原则,即大多数的对象的生存时间都不应太长,尤其是不能有成批量的、长时间存活的对象产生,这样这些对象在Minor GC就会被回收,不会进入老年代,这样才能保证老年代的稳定。
比如对于十几小时乃至一天才出现一次Full GC的系统可以通过定时任务的方式在夜间触发Full GC。
如果FullGC次数过多可能是下面的原因:
1、内存占用高:代码中创建了大量的对象导致内存泄漏,不能回收内存,创建新对象导致空间不足触发fullGC
2、内存占用不高:可能是显示的调用System.gc()次数太多导致的fullGC,可以通过添加-XX:+DisableExplicitGC来禁用JVM对显式GC的响应