JVM详解
JVM JAVA 虚拟机
对象头
包括两部分信息
第一部分:对象自身运行时数据
哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。【Mark Word】
32位虚拟机为32bit,64位虚拟机为64bit
例如在32位的虚拟机中未被锁定的情况下:
25bit记录存储对象哈希码、4bit用于存储对象分代年龄、2bit用于存储锁标志位、1bit固定为0
| Mark Word | ||
|---|---|---|
| 存储内容 | 标志位 | 状态 |
| 对象哈希码、对象分代年龄 | 01 | 未锁定 |
| 指向锁记录的指针 | 00 | 轻量级锁定 |
| 指向重量级锁的指针 | 10 | 膨胀(重量级锁) |
| 空、不需要记录信息 | 11 | GC标记 |
| 偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
第二部分是类型指针、即对象指向它的类元数据的指针、虚拟机通过这个指针来确定这个对象的哪个类的实例
第三部分是对齐填充,并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。是由于虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。当对象是例没有对齐的时,就需要通过对齐填充来补全
Jvm 结构

对象访问定位
栈中保存的是reference,java程序需要通过栈上的reference来操作堆上的具体对象。
1.句柄访问

2.直接指针访问

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动( 垃圾收集时移动对象是非常普遍的行为 )时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就本书讨论的主要虚拟机Sun HotSpot而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
OutOfMemoryError 异常
在Java虚拟机规范中,除程序计数器外,虚拟机内存的其他运行区域都可能出现OOM异常。
Java堆内存溢出
通过内存映像分析工具对堆转储【Dump】进行快照分析,【重点】确认内存中的对象是否是必要的,需要先分清楚到底是内存泄露还是内存溢出
内存泄露
内存中的存在对象不再被程序需要,但垃圾收集器无法自动回收。【排查】可通过排查工具查询泄露对象GC Roots的引用链。
内存溢出
内存中的对象都是程序所需要。【排查】1、检查虚拟机堆的参数(-Xms和-Xmx)2、检查是否存在对象的生命周期过长、持有状态时间过长
Java虚拟机栈或本地方法栈OOM(-Xxs)
例:32window虚拟机的内存为2G、虚拟机通过了参数控制Java堆和方法区两个内存的最大值。
剩余内存 = 虚拟机内存 - (Java堆内存最大容量(Xmx) + 最大方法区容量(MaxPermSize))
剩余内存 = (程序计数器 + 本地方法栈 + 虚拟机栈) * 线程数
由以上公式可得每一个线程所分配的栈容量越大、可以建立线程的线程数越少。
如果是建立过多的线程导致程序OOM,在无法减少线程数和更换虚拟机的情况下,只能通过减少堆的最大内存、最大方法区内存或减少栈容量。
方法区和运行时常量池溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描过等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类( 如反射时的GeneratedConstructorAccessor和动代理等 ),但在本次实验中操作起来比较麻烦。在代码清单2-8中,笔者借助CGLib“直接操作字节码运行时生成了大量的动态类。
当前的很多主流框架,如Spring、 Hibernate,在对类进行增强时都会使用到(CGLib/动态语言Groovv等)这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。
【注意】CGlib的使用
本机直接内存溢出
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。
| JVM参数(JAVA堆) | ||
|---|---|---|
| 参数 | 说明 | 备注 |
| -Xmx3550m | 设置JVM最大可用内存为3550 | |
| -Xms3550m | 设置JVM最大小用内存为3550 | 此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存 |
| -Xmn2g | 设置年轻代大小为2G | 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 |
| -Xss128k | 设置每个线程的堆栈大小 | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 |
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
①、-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代).设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
②、-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值.设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
③、-XX:MaxPermSize=16m:设置持久代大小为16m.
④、-XX:MaxTenuringThreshold=0:设置垃圾最大年龄.如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概论.
可达性算法
在java中,可作为GC Roots的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量)中引用的对象
- 方法中的类静态属性引用的对象
- 方法中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
问题:为何gc root期间需要stop the word?
GCRoots(根节点枚举)必须保障一致性快照才得以进行。其中“一致性”的意思指在整个枚举期间执行子系统,看起来就像被冻结在某个时间点上。不会出现分析过程中,根节点集合的对象引用关系还在不断变化 的情况,若这点不能满足的话,分析结果准确性也就无法保证。
扩展:“被冻结在某个时间点”就是后面提到的*主动式中断*
而随着程序的不断扩展gc root程序会出现长时间停止的灾难。
- 根节点枚举时间会随着方法区和栈区的大小成正比
- 根节点枚举期间会stop the world
目前主流Java虚拟机使用的都是准确式垃圾收集,而可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发。
保守式 GC: 遍历方法区和栈区查找 ;
准确式 GC: 通过后文提到的称之为 OopMap 的数据结构来记录 GC Roots 的位置
半保守 GC:
所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpo的实现中,是使用一组称为OopMap的数据结构来达到这个目的的。
在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

安全点(Safepoint)
如果为每一条指合都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。所以只在“特定的位置”记录和生产OopMap,这些位置称为安全点(Safepoint)。安全点的选择是有策略的,需要具备“是否具有长时间运行指令”的特征。我们知道,CPU资源是时间片段,如果在占用cpu时间比较小的指令位置设置安全点,线程的中断操作导致的全局暂停会效果会被放大;反之,在需要长时间运行的指令位置进行中断操作,延迟效果会被覆盖。
而“长时间执行”的最明显特征就是指今序列复用。例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指合才会产生Safepoint
对于Safepoint,另一个需要考虑的问题是如何在GC的时候让所有的线程都跑到“最安全点”在停下来。这里有两种方案可供选择:抢断式中断和主动式中断。
抢先式中断
抢先式中断不需要用户线程配合,在进行GC时,JVM会中断挂起所有用户线程;如果有些线程不在安全点,过段时间再尝试,直到线程在安全点的位置成功中断。抢先式中断的最大问题是时间成本的不可控,进而导致性能不稳定和吞吐量的波动,特别是在高并发场景下这是非常致命的。目前没有虚拟机采用这种中断策略。
主动式中断
主动式中断不会直接中断线程,而是全局设置一个标志位,用户线程会不断的轮询这个标志位;当发现标志位为真时,线程会在最近的一个安全点主动中断挂起。
安全区域
使用Safepoint似乎已经完美地解决了如何进入GC的问题,但实际情况却并不一定Safepoint机制保证了程序执行时,在不太长的时间内就进入了Safapoint。但是程序“不执行”的时候呢 ? 程序不执行的时候就无法进入Safepoint。比如线程进入Sleep或Block状态下。这个时候线程无法处理JVM的中断请求找到附近的Safepoint。JVM也无法分配时间让这些线程继续执行。这种情况下我们就需要安全区域(Safe Region)
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
Jvm中对Sleep和Block线程的处理:
在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举( 或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开SafeRegion的信号为止。
关于对象引用
强引用【Strong Reference】:Object o = new Object(); 只要是当前引用还在,垃圾收集器永远不会回收掉被引用的对象
软引用【Soft Reference】:描述一些还有用但非必须的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。回收完成之后内存还是不足的时候才会抛出内存溢出
弱引用【Weak Reference】:描述非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会被收掉只被弱引用关联的对象。
虚引用【Phantom Reference】:它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存周期构成影响,也无法通过虚引用来取得一个对象实例。
对象finalize【过程】

回收方法区【永久代】
永久代垃圾收集器主要回收两个部分为“废弃常量”、“无用的类”
废弃常量:系统中没有任何地方引用了这个常量
无用的类:(1)该类所有的实例都已经被回收、也就是java堆中不存在该类的任何实例。(2)加载该类的classLoader已经被回收。(3)该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
标记-清除
执行过程:当堆中有效的内存被耗尽或是当前的内存不满足待分配的内存容量、会停止整个程序,即“Stop the world(STW)”。然后开始两个工作,分别是标记和清除。
标记:Collector从引用的根节点开始遍历递归、标记所有被引用的对象、在对象的Header中记录为可达对象。
清除:Collector从引用的根节点进行线性遍历,如果发现某个对象在Header中没有被标记为可达对象,则将其回收【回收见:finalize执行过程】。
不足:
- 效率一般,因为标记和清除都需要遍历,所有效率较慢。
- GC的时候会触发STW,用户体验差。
- 这种方式清除来的内存是不连续的,产生内存碎片,需要维护一个空闲列表(空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。)。
复制算法
复制算法主要为了解决标记-清除算法的弊端。其核心思想是将活着的内存空间分成两块,每次只使用一块,在垃圾回收时将正在使用的内存中存活的对象复制到未被使用的内存块中,最后清除正在使用的内存块,交换两个内存的角色,最后完成垃圾回收。
优点:
- 没有标记和清除的过程,实现简单,运行高效。
- 复制过去的内存保证空间连续性,不会有碎片问题。
不足
- 需要两倍的内存空间。
- 如果对象移动,那么需要修改栈空间的引用地址。参考:对象访问定位
注意:如果系统中的存活对象多,而复制算法复制的存活对象数量并不会太大,所以效率会非常低。而在新生代的Survivor适合复制算法。
标记-压缩(整理)
复制算法的高效性是在存活对象少、垃圾对象多的前提下,这种情况经常在新生代发生,但是老年代恰恰相反。此时再使用复制算法,回收的成本将很高。因此基于老年代的垃圾回收特性,需要使用其他算法。标记清除算法虽然可以使用,但是执行完回收后会产生内存碎片,导致大对象无法存入,所以在此基础上改进,出现了标记-压缩算法。
执行过程:
第一阶段和标记-清除算法相同,在第二阶段将所有的存活对象压缩到内存的一侧,按顺序排放,然后清理之外的空间。
标记-压缩算法就等同于标记-清除算法执行完成后再执行一次内存碎片整理,因此标记-压缩算法也可以称为标记-清除-压缩算法。
优点:
- 解决了标记-清除算法和复制算法的缺点。
缺点:
- 效率上标记-压缩算法不如复制算法。
- 移动对象的同时,如果被其他对象引用,则还需要修改引用地址。
- 移动的过程中需要停止用户线程(STW)。
分代收集
不同对象的生命周期不同,所以根据不同的生命周期可以采用不同的收集算法。Java堆分为老年代和新生代,这样就可以根据各个分代的特点使用不同的回收算法,以便提高回收效率。
各个分代使用的回收算法和特点:
新生代:区域相对老年代较小,对象生命周期较短、存活率低,回收频繁。针对这种情况使用复制算法,速度是最快的。并且复制算法的效率和存活对象的数量有关,所以非常适合复制算法,使用两个Survivor的设计缓解复制算法内存利用率不高的问题(默认情况下只会空闲新生代中的十分之一的空间)。
老年代:区域比较大,对象生命周期较长、存活率高,回收频率不高。针对这种存在大量存活对象的空间,复制算法明显不合适。一般使用标记-清除算法或者标记-清除和标记-压缩算法的混合实现。目前Hotspot虚拟机采用的CMS回收器,基于这两种算法实现的。
- 标记(Mark)阶段的开销是和存活对象成正比的。(存活的对象多遍历的时间越长)
- 清除(Sweep)阶段的开销是与所管理区域的大小成正比的。(线性遍历)
- 压缩(Compact)阶段的开销与存活对象的数据成正比。(存活对象越多需要整理的也越多)
垃圾收集算法汇总
| 标记-清除 | 标记-整理(压缩) | 复制算法 | |
|---|---|---|---|
| 效率 | 中 | 最慢 | 最快 |
| 空间开销 | 少【产生空间碎片】 | 少【不会产生空间碎片】 | 多【不会产生空间碎片】 |
| 是否移动对象 | 否 | 是 | 是 |
垃圾收集器

如果两个收集器之间存在连线,说明它们可以搭配使用。
Serial收集器
Serial收集器是一个单线程收集器,它不仅仅只使用一个CPU或一条线程去完成垃圾收集工作,更重要的是他在进行它在进行垃圾收集的时候,必须暂停其他线程的工作【STW】,直到它收集结束。
以下是Serial和Serial Old执行过程:
新生代采取复制算法,并暂停所有用户操作。
老年代采取标记-整理,并暂停所有用户操作。
优点:
- 简单和高效(与其他单线程相比),对于单CPU的环境来说,Serial没有线程交互的开销,所以是单线程垃圾收集器中可以达到最高效率。
- 在虚拟机分配的内存不会很大的情况下,例如桌面应用。Serial收集几十兆或是上百兆的内存,停顿时间完全可以控制在几十毫秒或最多一百豪秒以内。所以,Serial收集器对于Client模式的虚拟机来是很好的选择。
ParNew收集器
ParNew收集器就是Serial垃圾收集器的多线程版本,与Serial垃圾级收集器没有太大创新之处,但它却是许多运行在Server模式下的虚拟机中的首选的新生代收集器。目前只有他可以和CMS配合工作。
以下是ParNew/Serial Old 执行过程:
新生代采取复制算法,并暂停所有的用户线程。【多线程处理】
老年代采取标记-整理算法,并暂停所有的用户线程。【单线程处理】
对于新生代,回收次数频繁,使用并行方式更高效。
对于老年代,回收次数少,使用串行的方式节省资源。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集器线程同时执行(但不一定是并行,可能是会交替执行),用户程序在继续运行,而垃圾收集运行于另外一个CPU上。
注:ParNew在单核CPU中不如Serial,甚至在双核CPU中效果也可能不如Serial。
Parallel Scavenge收集器
- Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法,又是并行的多线程收集器。
和 ParNew 不同,Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量,也被称为吞吐量优先的垃圾收集器。
- 自适应调节策略也是 Parallel Scavenge 与 ParNew 的一个重要区别。
高吞吐量则可以高效率的利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序
Parallel Scavenge 收集器在 JDK1.6 时提供了用于执行老年代垃圾收集的 Parallel Old 收集器,用来代替老年代的 Serail Old 收集器。Parallel Old 收集器采用标记-压缩算法,但同样也是基于并行回收和“Stop The World”机制。
参数配置相关
- -XX: +UseParallelGC 手动指定年轻代使用 Parallel Scavenge 并行收集器执行垃圾回收任务。
- -XX: +UseParalleloldGC 手动指定老年代使用 Parallel Old 并行收集器。
这两个参数分别适用于年轻代和老年代。默认 jdk8 是开启的。
这两个参数,默认开启一个,另一个也会被开启(互相激活)。
- -XX:ParallelGCThreads 设置年轻代并行收集的线程数。一般的,最好与CPU数量相等,以免过多的线程数影响垃圾收集性能,在默认情况下,当 CPU 数量小于 8 个,ParallelGCThreads 的值等于 CPU的数量。当 CPU 的数量大于8个时,ParallelGCThreads 的值等于 3 + [5 * CPU的个数 / 8]。
- -XX:MaxGCPauseMills 设置垃圾收集器最大的暂停时间(STW),单位是毫秒。为了尽可能把停顿时间控制在 MaxGCPauseMills 以内,收集器在工作时会调整堆大小或者其它一些参数。对于用户来说,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量,所以服务端适合 Parallel 垃圾收集器。
- -XX:GCTimeRatio 垃圾收集时间占总时间的比例( 为 1/(N+1) )。用于衡量吞吐量的大小,取值范围是(0,100)。默认是 99 ,也就是垃圾回收时间不超过1。与 MaxGCPauseMills 参数有一定的矛盾性。暂停时间越长,GCTimeRatio 越容易超过设定的比例。
- -XX:UseAdaptiveSizePolicy 设置 Parallel Scavenge 收集器具有自适应调节策略。这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象等参数会被自动调节,以达到在堆大小、吞吐量和停顿时间的平衡点。在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标吞吐量和停顿时间,让虚拟机自己完成调优工作。
Serial Old 收集器
Serial Old是Serial 老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要是给Cilent模式下的虚拟机使用。如果是在Server模式下,那么它还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用叫,另一种用途就是作为CMS收集器的后备预案,在并发收集发生ConcurrentMode Failure时使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直Scavenge收集器,老年代除了处于比较尴尬的状态。原因是,如果新生代选择了ParallelSerial Old( PS MarkSweep ) 收集器外别无选择。由于老年代SerialOld收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
以下是Parallel Scavenge和 Parallel Old执行过程
新生代采取复制算法,并暂停所有的用户线程。【多线程处理】
老年代采取标记-整理算法,并暂停所有的用户线程。【多线程处理】
CMS垃圾收集器【JDK1.8常用的老年代垃圾回收器】
CMS(concurrent Mark Sweep-并发标记扫描【标记-整理算法】)收集器是一种以获取最小停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者B/S服务上。这一类服务很重视服务的响应速度,希望系统的停顿时间最短。
CMS的执行过程包括四个步骤:
- 初始标记(CMS initial mark):STW 枚举GCRoots对象,暂停用户线程,扫描对象较少,Stop the world时间短。
- 并发标记(CMS concurrent mark) :GC线程,用户线程并发工作,耗时时间最久的一部分内容(80%GC时间),GC线程抢占CPU,对用户线程有影响。由于用户线程是并发执行的,用户线程会造成一些对象的引用改变,产生新的垃圾对象。
- 重新标记(CMS remark) :STW 短暂的stop the world,不会有对象的状态改变。并发标记产生的新的垃圾会在这阶段被标记出来。
- 并发清除(CMS concurrent sweep) :GC线程和用户线程并发执行,产生的浮动垃圾由下一次垃圾回收清理。 缺点:CMS进行垃圾收集的时候需要预留一定的内存空间给用户线程工作(默认68%,可设置),在内容空间不足时,会导致回收失败,此时会通过serialold进行垃圾回收,速度特别慢。

CMS垃圾收集器的优缺点
CMS垃圾收集器的主要有点为并发收集、并发清除、低停顿。相比较前几代的垃圾收集器,CMS垃圾收集器给用户的体验更好,因为它追求的是最短的回收停顿时间。
CMS垃圾回收器的缺点也比较明显:
- 对CPU资源十分敏感,因为并发标记和并发清除都是和程序同时运行,因此会占用CPU导致应用程序变慢。
- 无法处理浮动垃圾,浮动垃圾就是在并发清除过程中用户线程新生成的垃圾,这部分垃圾CMS无法在本次被清理,可能出现Concurrent Mode Failed报错,因此需要预留一定的内存空间,无法等到老年代快被占满时再清除。默认情况下,CMS在老年代使用了92%后就会被激活。可以设置-XX:CMSInitiatingOccupancyFraction设置这个值。如果真的出现了concurrent mode failed,说明已经没办法并发标记垃圾了,这时候就会使用serial old垃圾收集器来回收,也就是通过stop the world的方式。
- 产生空间碎片,由于采用的是标记-清除算法,那就无法避免会产生空间碎片的问题,这会给分配大对象带来困难。
CMS的相关参数
1 | |
G1垃圾回收器
垃圾收集器参数总结
| 参数 | 描述 |
|---|---|
| UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+SerialOld的收集器组合进行内存回收 |
| UseParNewGC | 打开此开关后,使用ParNew+Serialold的收集器组合进行内存回收 |
| UseConcMarkSweepGC | 打开此开关后,使用ParNew+CMS+SerialOld的收集器组合进行内存回收。SerialOld收集器将作为CMS收集器出现ConcurrentMode Failure失败后的后备收集器使用 |
| UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用ParallelScavenge+SerialOld(PSMarkSweep)的收集器组合进行内存回收 |
| UseParallelOldGC | 打开此开关后,使用ParallelScavenge+ParallelOld的收集器组合进行内存回收 |
| SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Survivor-8 :1 |
| PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
| MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代 |
| UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
| HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
| ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
| GCTimeRatio | GC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在使用ParallelScavenge收集器时生效 |
| MaxGCPauseMillis | 设置GC的最大停顿时间。仅在使用ParallelScavenge收集器时生效 |
| CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效 |
| UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效 |
| CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS收集器时生效 |
内存分配和回收策略【Serial/Serial Old 和ParNew/Serial Old】
内存分配
例:在一个方法中尝试一下分配3个2MB和一个4MB大小的对象,在运行的时候通过-Xms20M -Xmx20M -Xmn10M 这三个参数
限制了Java堆的大小为20MB,因为-Xms 与-Xmx中的参数一致,所以限制了JAVA堆的大小为20MB。其中Xmn10M给新生代分配10MB,剩下的老年代大小就剩10MB。-Xx:SurvivorRatio=8,决定了新生代中Eden:其中一个Survivor = 8:1。而新生代中存在两个Survivor和一个Eden,所以新生代中Eden为4MB,两个Survivor为1MB。**注:**新生代的可用空间为Eden+Survivor[一个]【9MB】
Minor GC:指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭,所以Minor GC 非常频繁。
Major GC/Full GC :指发生在老年代的GC,出现了Major GC经常会伴随着至少一次Minor GC 【有一些垃圾处理器的收集策略里就有直接Major GC的策略过程】。Major GC一般速度会比Minor GC慢10倍以上。
对象分配顺序
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态年龄判断
- 空间分配担保
对象优先在Eden分配/大对象进入老年代:大多数情况下,对象在新生代Eden区分配。当Eden没有足够的空间进行分配时,虚拟机将发起一次Minor GC。如果执行GC之后,还是不够内存分配给新对象的时候,只能通过分配担保机制提前转移到老年代去。设置参数PretenureSizeThreshold为3MB时,则4MB的新对象进入会被直接分配至老年代。注意PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。Parallel Scavenge收集器不认识这个参数。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。
长期存活的对象加入老年代:虚拟机给每一个对象定义了一个对象年龄(age)计数器,对象在Eden区发生一次Minor GC 并且可以被Survivor容纳的话,对象将被移动到Survivor区,并且年龄设置为1,并且后续发生Minor GC后,对象年龄+1。默认对象在到达15岁后面就会晋升到老年代。可通过次数-XX:MaxTenuringThershold设置。
动态年龄判断:Survivor空间中相同年龄所有对象大于Survivor空间的总合,年龄大于或等该年龄的对象可直接进入老年代。
空间担保:老年代最大可用连续空间是否大于新生代所有对象的总空间。
虚拟机性能监控与故障处理工具
JDK 监控和故障处理工具
| 名称 | 主要作用 |
|---|---|
| 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:虚拟机进程状态工具
jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名
jps [option]
option选项
| 选项 | 作用 |
|---|---|
| -q | 只输出LVMID,省略主类的名称 |
| -m | 输出虚拟机进程启动传给主类传给main()函数的参数 |
| -l | 输出主类的全名,如果进程执行的是jar包,则输出jar路径 |
| -v | 输出虚拟机进程启动时JVM 参数 |
jstat:虚拟机统计信息监视工具
jstat命令是用于监视虚拟机各种运行状态信息的命令工具。
jstat [option]
参数:interval表示间隔、count表示次数。
例:假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命命应当是:jstat -gc 2764 250 20
其中option选项见下表
| 选项 | 作用 |
|---|---|
| -class | 监视类装载、卸载数量、总空间以及类装载所耗费的时间 |
| -gc | 监视Java堆状况,包括Eden区、两个survivor区、老年代永久代等的容量、已用空间、GC时间合计等信息 |
| -gccapacity | 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间 |
| -gcutil | 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比 |
| -gccause | 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因 |
| -gcnew | 监视新生代 GC状况 |
| -gcnewcapacity | 监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间 |
| -gcold | 监视老年代GC状况 |
| -gcoldcapacity | 监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间 |
| -gcpermcapacity | 输出永久代使用到的最大、最小空间 |
| -compiler | 输出JIT编译器编译过的方法、耗时等信息 |
| -printcompilation | 输出已经被JIT编译的方法 |
jinfo:Java配置信息工具
查询进程的详细信息,包括 JVM 参数和系统属性
jinfo [option]
其中option选项合法项见下表:
| 选项 | 作用 |
|---|---|
| -properties | 进程的系统属性 |
| -cmd | 进程的命令行参数 |
| -set |
动态修改进程的 JVM 参数[ |
| -mem | 进程的内存状况 |
| -classpath | 进程的类路径 |
| -threads | 进程的线程状态 |
| -gc | 进程的垃圾回收情况 |
| -compilation | 进程的 JIT 编译情况 |
| -diagnostics | 进程的诊断信息 |
jmap:Java内存映像工具
jmap命令用于生成堆转储文件(headdump或dump)。
jmap [option]
其中option选项的合法项见下表:
| 选项 | 作用 |
|---|---|
| -dump | 生成Java堆转储快照。格式为:-dump:[live,]format-b,fle- |
| -finalizerinfo | 显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在Linux/Solaris平台下有效 |
| -heap | 显示 Java堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在Linux/Solaris平台下有效 |
| -histo | 显示堆中对象统计信息,包括类、实例数量、合计容量 |
| -permstat | 以ClassLoader为统计口径显示永久代内存状态。只在Linux/Solaris平台下有效 |
| -F | 当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在Linux/Solaris平台下有效 |
| -objects | 显示指定类加载器下的所有对象 |
jhat:虚拟机堆转储快照分析工具
jhat与jmap搭配使用,用于分析jmap生成的堆转储文件。由于分析dump文件比较消耗资源和时间。所以一般不会选择在服务器上直接使用jhat进行dump文件分析。另一个原因是有其他更好的分析工具。
jhat [option]
1 | |
option 选项
| 选项 | 作用 |
|---|---|
| -dump | 指定 JVM 堆内存快照文件 |
| -format | 设置输出格式,可选值有 text(默认)、html、csv |
| -heap | 打印堆内存使用情况 |
| -cpu | 打印 CPU 使用情况 |
| -thread | 打印线程信息 |
| -verbose | 打印详细信息 |
jstack:Java堆栈跟踪工具
jstack命令用于生成虚拟机当前时刻的线程快照(threaddump或javacore文件)。主要是用于定位线程长时间停顿的原因,如线程死锁、死循环、请求外部资源导致长时间等待等。
jstack [option]
1 | |
option 选项
| 选项 | 作用 |
|---|---|
| -l | 打印线程列表,同时显示线程 ID、线程名和线程状态。 |
| -f | 指定线程快照文件路径 |
| -p | 打印指定进程的线程快照 |
| -c | 打印类加载器信息 |
| -m | 打印方法名和字节码索引(bytecode index,bci) |
| -t | 打印线程栈跟踪 |
| -v | 打印详细信息 |