JVM内存问题分析

JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。
JVM运行时数据区:
1、方法区:类信息(类名,访问修饰符、字段描述、方法 描述等)、常量、静态变量、即时编译后的class文件等。在GC时用永久代来实现方法区
2、运行时常量池:是方法区的一部分,存放编译期生成的各种字面量和符号引用(字面量就是实际的值,如1,"abc",符号引用是不知道实际引用对象的实际地址而抽象出的一种引用)。
  字面量如:文本字符串,声明为final的常量值;
  符号引用包括了三种常量,分别是:类和接口的全限定名,字段的名称和描述符,方法的名称和修饰符
3、堆:存放对象的实例,数组内存在此分配(所有的对象实例和数组都在堆上分配),可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
4、栈:局部变量表(局部基本变量的值和引用类型在堆中的地址值,方法参数),方法的返回值等信息
 
问题:字符串在JVM中如何存放?
  1、使用字符串初始化的字符串对象,它的值存放在字符串常量池中
  2、使用字符串构造方法创建的字符串对象,它的值存放在堆内存中
java.lang.String.intern(),这个API可以手动将一个字符串对象的值转移到字符串常量池中
在1.7及之前,字符串常量池是在永久代(方法区),大小是固定的,也不能被垃圾回收器回收,如果有太多了字符换调用了intern方法的话,就有可能造成OOM。
1.8里,字符串常量池移到了堆内存中,可以被垃圾回收器回收降低了字符串常量池OOM的风险。
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;
 
 
直接内存:Direct Memory
Direct Memory容量可通过-XX:MaxDirectMemorySize指定,如果不指定则默认和Java堆的最大值-Xmx一样。
不受到Java堆内存大小的限制,只会受到计算机总内存(包括RAM以及SWAP或者分页文件)大小以及处理器寻址空间的限制,若直接内存和JVM各个区域占用总内存大小超过物理内存限制则会出现OutOfMemoryError。
NIO:JDK1.4引入的一种基于通道channel和缓冲区Buffer的IO方式;它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,
避免了在Java堆和Native堆中来回复制数据,因此能在一些场景中显著提高性能。
 
 
 
Java堆中对象分配,布局和访问过程
1、对象创建(普通Java对象,不包括数组和Class对象)
虚拟机遇到一条new指令时,首先去常量池中检查这个指令的参数能否定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过,如果没有先执行类的初始化过程。
 在类加载检查通过后,对象所需的内存的大小在类加载完成后便可完全确定,接下来虚拟机将为新生对象分配内存,即把一块确定大小的内存从堆中划分出来。
(若堆中内存是规整的,即用过的和空闲的各放在一边,指针指向分界点,内存分配即指针向空闲移动对象所需的内存大小。这时内存分配采用指针碰撞(bump the pointer)的方式
若堆中的内存是不规整的,虚拟机需维护一张列表记录哪些内存块是可用的,在分配的时候找到一块足够大的内存块划分给对象,并更新列表,这种方式称为空闲列表(free list))
因此使用哪种方式取决于堆内存是否规整,是否规整又取决于采用的垃圾收集器是否带有压缩整理功能决定。因此采用Serial、ParNew采用的是指针碰撞,采用CMS收集器使用的是空闲列表。
给对象分配内存需要考虑线程安全的问题,需要同步进行,避免两个对象分配一块内存;解决这个问题有两种方案:
1、对内存分配空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新的原子性
2、把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),线程先在TLAB上分配,只有TLAB用完分配新的TLAB时才需要同步锁定。
内存分配完成后JVM将分配到的内存空间都初始化为0值,然后进行必要的设置,类的元数据信息,对象的哈希码,对象的GC分代年龄都存在对象头中,最后进行init初始化。
 
 2、对象的内存布局
在HotSpot 虚拟机中,对象在内存中的存储被分成了3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头分成两部分:
1、用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
2、类型指针,即对象指向它的类元数据的指针。JVM通过这个指针来确定这个对象是哪个类的实例。如果是数组对象,对象头中还需记录数组的长度
实例数据:对象真正存储的有效信息,即是代码中定义的字段内容,包括从父类继承下来的和自身定义的
对齐填充:并不是必然存在的,仅仅起着占位符的作用。JVM要求对象的大小必须是8字节的整数倍。
 
3、对象的访问定位
建立对象是为了使用对象,Java程序通过栈上的reference数据来操作堆上的具体对象。reference只是指向对象的一个引用,如何访问定位对象在堆上的具体位置呢?
1、句柄:Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息。
  好处是稳定:如垃圾收集时若对象被移动,只需修改句柄中的实例数据指针,reference无需修改。
2、直接指针:reference中存放的直接就是对象地址。好处是速度快,节省了一次指针定位的开销,HotSpot就是通过这种方式访问的。
 
4、Java对象模型
 
 
线程共享:堆,方法区(常量池)
线程私有:栈,程序计数器,本地方法栈
 
 
 
OutOfMemoryError
除了程序计数器外,其余的几个运行数据区都有可能发生OutOfMemoryError(OOM)的可能。
因此在遇到OOM的问题时应能根据异常的信息快速定位到时哪个内存区域的内存溢出,知道什么样的代码会导致OOM,以及该如何处理。 
 
1、Java堆溢出
  Heap堆是OOM故障最主要的发源地,它存储着几乎所有的实例对象。在线上生产环境中,JVM的Xms和Xmx应设置成一样的大小 ,避免在GC后调整堆大小时带来额外的压力。
  不停的创建对象,并且GC Roots到对象之间有可达路径,超过堆的最大内存容量就会OOM
如,在while(true)循环中不停的创建对象并将对象add到List集合中
Java堆内存溢出时异常堆栈信息会在“java.lang.OutOfMemoryError”后提示Java heap space
如何定位?
设置参数-XX:+HeapDumpOnOutOfMemoryError 来配置当内存耗尽时记录下当时的内存快照,一般根据MAT分析具体原因是内存泄漏还是内存溢出
内存泄漏:根据工具查看泄漏对象到GC Roots的引用链,找到泄漏对象时通过怎么样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。根据对象类型和GC Roots引用链就可以准确定位到泄漏代码的位置。
内存溢出:检查虚拟机参数堆参数能否扩大,代码上检查是否存在某些对象生命周期过长、持有状态时间过长。
 
2、虚拟机栈和本地方法栈溢出
  HotSpot虚拟机不区分虚拟机栈和本地方法栈,因此设置本地方法栈的参数-Xoss实际上是无效的,栈的参数只能根据-Xss设定
如果线程请求的栈深度超过了JVM允许的最大深度,将抛出StackOverflowError   
如果虚拟机在扩展栈时无法申请到足够的内存将跑出OutOfMemoryError
1、StackOverflowError   容易定位,可能是递归没有出口,正常的方法调用深度在1000-2000之间完全没有问题
2、OOM可能是建立的线程数量非常多,每个线程瓜分栈内存,当线程都存活着没有足够的内存去分配给线程时会抛出OOM
 
3、方法区和运行时常量池溢出
  Java方法区和运行时常量池溢出异常堆栈信息会在“java.lang.OutOfMemoryError”后提示PermGen space
  如String字符串存放在常量池中
 
4、本机直接内存溢出
  Unsafe的allocateMemory可以申请分配内存(Unsafe实例可通过反射获取)
由DirectMemory导致的内存溢出在Heap Dump文件中看不出明显的异常,如果OOM之后的Dump文件很小,而程序中直接或者间接使用了NIO,那可能是这方面的原因。
DirectMemory-XX:MaxDirectMemory指定,如果不指定默认与Java堆内存最大值一样。
垃圾收集时,虚拟机 只有在Full GC时会顺便回收DirectMemory中废弃的对象。因此DirectMemory内存满了之后,只有等待系统的下一次Full GC,或者抛出内存异常在catch中调用System.gc()
 
 

一、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的对象有没有引用链相连

 

GCROOTS:
1.虚拟机栈中引用的对象
2.方法区静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中引用的对象(native方法)
 

即使在可达性分析法中不可达的对象,也至少要经历两次标记过程

第一次标记:可达性分析后无与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()   返回从 ThreadStackTraceElement 数组的一个 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的响应



posted @ 2019-04-05 23:52  杨岂  阅读(904)  评论(0编辑  收藏  举报