# 一.垃圾回收基础
# 1.如何判断对象是否存活的?
主要通过以下两种方法:
- 引用计数算法:无法解决相互引用问题
- 可达性分析算法
可达性分析算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
可达性分析算法是需要一个理论上的前提的:该算法的全过程都需要基于一个能保障一致性的快照中才能够分析,这意味着必须全程冻结用户线程的运行。
并发标记会产生浮动垃圾和出现对象消失的问题,都可以解决.
# 2.GC Roots 是什么?
在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:
- 在
虚拟机栈
中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 - 在
方法区
中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。 - 在
方法区
中常量引用的对象,譬如字符串常量池(String Table)里的引用。 - 在
本地方法栈
中 JNI(即通常所说的 Native 方法)引用的对象。
Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、Out Of MemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等
# 3.GC Roots 举例
a.java虚拟机栈中的引用的对象:
- 我们知道,每个方法执行的时候,jvm 都会创建一个相应的栈帧
- 栈帧包括(操作数栈、局部变量表、运行时常量池的引用)
- 栈帧中包含这个方法内部使用的所有对象的引用(这就是虚拟机栈中的引用对象)
- 一旦该方法执行完后,该栈帧就会从虚拟机栈中弹出,这样一来这些局部(临时)对象的引用也就不存在了,或者说没有任何 GCRoot 指向这些临时对象,所以这些对象在下一次 gc 时就会被回收掉
b.方法区中的类静态属性引用的对象(一般指被static修饰的对象,加载类的时候就加载到内存中。):
第一种写法:
方法区中的类静态属性引用的对象
- 因为该类是属于 JvmTest 类的全局属性(类属性),所以会存在于方法区中,每个线程共享
- 这种写法一般会用在单例模式的饿汉模式中
- 所以此类对象可以作为 GCRoot 对象
- 注意:不止这一种写法,还可以下面那样写
- private static User user = new User();
第二种写法:
方法区中的类静态属性引用的对象
- 这种写法一般会用在单例模式的懒汉模式中
- 所以此类对象也可以作为 GCRoot 对象
- private static User user1 ;
c.方法区中的常量引用的对象:
- 因为该类是属于 JvmTest 类的全局属性(类属性),所以会存在于方法区中,每个线程共享
- 该常量属性被 final 修饰,再第一次赋值之后不允许更改
- 所以此对象可以作为 GCRoot 对象
# 4.说说根节点枚举?
迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的。现在可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发,但根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况,若这点不能满足的话,分析结果准确性也就无法保证。这是导致垃圾收集过程必须停顿所有用户线程的其中一个重要原因,即使是号称停顿时间可控,或者(几乎)不会发生停顿的 CMS 、 G1 、ZGC 等收集器,枚举根节点时也是必须要停顿的。
由于目前主流 Java 虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机是有办法直接得到哪些地方存放着对象引用的。在 HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等 GC Roots 开始查找。
# 5.java 中对象的引用类型有什么?
在 JDK 1.2 版之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。
- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 版之后提供了 SoftReference 类来实现软引用。
- 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK 1.2 版之后提供了 WeakReference 类来实现弱引用。
- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 版之后提供了 PhantomReference 类来实现虚引用。
# 6.如果对象不可达,会立即回收吗?
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记
- 随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()方法。
- 假如对象没有覆盖 finalize()方法,或者 finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。会真正回收这个对象.
如果这个对象被判定为确有必要执行 finalize()方法,那么该对象将会被放置在一个名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize()方法。
# 7.方法区有垃圾回收吗?
在《Java 虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,方法区垃圾收集的“性价比”通常也是比较低的:在 Java 堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收 70%至 99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型(类型卸载)。
# 8.什么是无用的类?
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
# 9.垃圾收集的算法有哪几种?
标记-清除算法:
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。
它的主要缺点有两个:第一个是执行效率不稳定,如果 Java 堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记-复制算法:
常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,也称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费。
HotSpot 虚拟机的 Serial 、ParNew 等新生代收集器均采用了这种策略来设计新生代的内存布局。具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor 。发生垃圾搜集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。 HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8 ∶1
标记-整理算法:
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费 50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
针对老年代对象的存亡特征,1974 年 Edward Lueders 提出了另外一种有针对性的“标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存.
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。
分代收集算法:
分为新生代和老年代,新生代使用复制算法,老年代使用标记整理算法.
# 10.什么是 jvm 的安全点?
JVM 的安全点(Safe Point)是指在程序执行过程中,JVM 会选取一些特定位置作为安全点,当线程到达这些位置时,JVM 会确保线程的栈和堆处于稳定状态,使得线程能够被安全地挂起。安全点的存在是为了支持 Java 的线程安全机制和垃圾回收。
在 JVM 中,线程在运行时,可能存在以下情况:
- 垃圾回收:JVM 的垃圾回收器需要检查和清理不再使用的对象。在执行垃圾回收时,需要暂停正在运行的线程,否则可能会导致垃圾回收器的工作出现问题。
- 线程安全机制:Java 中的线程安全机制涉及到一些原子操作、锁机制和同步操作。当线程进行这些操作时,需要保证线程处于稳定状态,防止数据不一致或竞态条件等问题。
为了实现以上功能,JVM 会在适当的时机,将线程挂起到安全点。JVM 会将安全点放在一些特定位置,例如:
- 方法调用:在方法调用开始和结束时。
- 循环跳转:在循环的跳转点。
- 同步点:在进入和退出同步块时。
- 异常抛出:在抛出异常时。
当线程到达安全点时,JVM 会暂停线程的执行,并确保线程的栈和堆处于一致状态,以便进行垃圾回收或执行线程安全机制。一旦操作完成,线程会被唤醒,继续执行。
# 11.如何让所有线程都跑到最近的安全点?
抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)是两种不同的线程调度方式,用于控制线程在执行过程中的暂停和恢复。
抢先式中断(Preemptive Suspension):
- 抢先式中断是由操作系统或线程调度器来决定何时暂停当前线程的执行,并切换到另一个线程执行。
- 在抢先式中断中,线程没有主动权,线程的暂停和恢复是由外部控制的。当一个线程运行时间片用完、有更高优先级的线程进入、或者出现了 I/O 等待等情况时,操作系统会强制中断当前线程的执行,将 CPU 资源分配给其他线程。
- 抢先式中断保证了高优先级的任务优先执行,但也可能导致上下文切换的开销。
主动式中断(Voluntary Suspension):
- 主动式中断是由线程自己决定何时暂停自己的执行,并让出 CPU 资源给其他线程执行。
- 在主动式中断中,线程具有主动权,线程可以根据自己的需要选择在何时主动挂起,并通过相应的方式(如
Thread.sleep()
、Object.wait()
等)暂停自己的执行。 - 主动式中断适用于一些需要控制自己执行频率或让出 CPU 资源给其他线程的情况,能够减少不必要的上下文切换。
总结:
- 抢先式中断是由操作系统或线程调度器决定线程何时暂停执行,线程没有主动权,通常用于保证高优先级任务优先执行。
- 主动式中断是由线程自己决定何时暂停执行,线程具有主动权,通常用于线程之间的协调和资源控制。
# 12.程序不执行的时候如何到达安全点?
安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于 Sleep 状态或者 Blocked 状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于这种情况,就必须引入安全区域(Safe Region)来解决。
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
# 13.垃圾回收是如何处理跨代引用问题的?
跨代引用举例:假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的 GC Roots 之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。
并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的垃圾收集器,典型的如 G1 、ZGC 收集器,都会面临相同的问题, JVM 为了用尽量少的资源消耗解决跨代引用下的垃圾回收问题,引入了记忆集
。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
。
在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。目前最常用的一种记忆集实现形式种称为“卡表”
,卡表中的每个记录精确到一块内存区域(每块内存区域称之为卡页),该区域内有对象含有跨代指针。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为 1,称为这个元素变脏(Dirty),没有则标识为 0 。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入 GC Roots 中一并扫描。
# 14.说说垃圾回收的三色标记理论?
标记阶段是所有追踪式垃圾收集算法的共同特征,如果这个阶段会随着堆变大而等比例增加停顿时间,其影响就会波及几乎所有的垃圾收集器。
白色:
表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。黑色:
表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。灰色:
表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
# 15.说说什么是对象消失?
对象消失的问题,即原本应该是黑色的对象被误标为白色
当且仅当以下两个条件同时满足时,会产生“对象消失”的问题:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)
增量更新:
要破坏的是第一个条件,当黑色对象
插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了(因为新加入的白色节点未被扫描过)。
原始快照:
要破坏的是第二个条件,当灰色对象
要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照
来进行搜索。
以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在 HotSpot 虚拟机中,增量更新和原始快照这两种解决方案都有实际应用,譬如, CMS 是基于增量更新来做并发标记的, G1 则是用原始快照实现的.
增量更新用的是写后屏障(Post-Write Barrier),记录了所有新增的引用关系。
原始快照用的是写前屏障(Pre-Write Barrier),将所有即将被删除的引用关系的旧引用记录下来。
# 16.为什么要赋值为 null?
变量 d 重用了变量 a 的 Slot,这样就节约了内存空间。
placeHolder
没有被回收的原因:System.gc();触发 GC 时,main()方法的运行时栈中,还存在有对args
和placeHolder
的引用,GC 判断这两个对象都是存活的,不进行回收。 也就是说,代码在离开 if 后,虽然已经离开了placeHolder
的作用域,但在此之后,没有任何对运行时栈的读写,placeHolder
所在的索引
还没有被其他变量重用
,所以GC
判断其为存活。
public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
placeHolder = null;//关键
}
System.gc();
}
2
3
4
5
6
7
8
# 二.垃圾回收器
# 1.说说垃圾回收器的种类?
图中展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器.
# 2.serial 的特点和使用场景?
该收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
迄今为止,它依然是 HotSpot 虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说, Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以, Serial 收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
# 3.ParNew 是如何进行垃圾回收的?
ParNew 收集器实质上是 Serial 收集器的多线程并行版本
,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World 、对象分配规则、回收策略等都与 Serial 收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
ParNew 收集器除了支持多线程并行收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的 HotSpot 虚拟机,尤其是 JDK 7 之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
在 JDK 5 发布时,HotSpot 推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS 收集器。这款收集器是 HotSpot 虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
遗憾的是, CMS 作为老年代的收集器,却无法与 JDK 1.4.0 中已经存在的新生代收集器 Parallel Scavenge 配合工作,所以在 JDK 5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个。 ParNew 收集器是激活 CMS 后(使用-XX:+UseConcMarkSweepGC 选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC 选项来强制指定或者禁用它。
ParNew 收集器在单核心处理器的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越 Serial 收集器。当然,随着可以被使用的处理器核心数量的增加,ParNew 对于垃圾收集时系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多(譬如 32 个,现在 CPU 都是多核加超线程设计,服务器达到或超过 32 个逻辑核心的情况非常普遍)的环境中,可以使用-XX: ParallelGCThreads 参数来限制垃圾收集的线程数。
# 4.Parallel Scavenge 收集器?
Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器.Parallel Scavenge 的诸多特性从表面上看和 ParNew 非常相似,那它有什么特别之处呢?
Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同, CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)
。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
吞吐量=(运行用户代码时间)/(运行用户代码时间+垃圾收集时间)
Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间
的-XX: MaxGCPauseMillis 参数以及直接设置吞吐量大小
的-XX: GCTimeRatio 参数。
-XX:MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集 300MB 新生代肯定比收集 500MB 快,但这也直接导致垃圾收集发生得更频繁,原来 10 秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio 参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为 19,那允许的最大垃圾收集时间就占总时间的 5%(即 1/(1+19)),默认值为 99,即允许最大 1%(即 1/(1+99))的垃圾收集时间。
由于与吞吐量关系密切,Parallel Scavenge 收集器也经常被称作“吞吐量优先收集器”。除上述两个参数之外,Parallel Scavenge 收集器还有一个参数-XX:+UseAdaptiveSizePolicy 值得我们关注。这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、 Eden 与 Survivor 区的比例(-XX: SurvivorRatio)、晋升老年代对象大小(-XX: PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)
# 5.Serial Old 收集器的特点?
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的 HotSpot 虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,另外一种就是作为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
需要说明一下,Parallel Scavenge 收集器架构中本身有 PS MarkSweep 收集器来进行老年代收集,并非直接调用 Serial Old 收集器,但是这个PS MarkSweep 收集器与Serial Old的实现几乎是一样的,所以在官方的许多资料中都是直接以 Serial Old 代替 PS MarkSweep 进行讲解
# 6.Parallel old 收集器的特点?
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-整理算法
实现。这个收集器是直到 JDK 6 时才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于相当尴尬的状态,原因是如果新生代选择了 Parallel Scavenge 收集器,老年代除了 Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如 CMS 无法与它配合工作。由于老年代 Serial Old 收集器在服务端应用性能上的“拖累”,使用 Parallel Scavenge 收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比 ParNew 加 CMS 的组合来得优秀。
直到 Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器这个组合。
# 7.说说 CMS 收集器的特点?
CMS (Concurrent Mark Sweep):
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网网站或者基于浏览器的 B/S 系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。 CMS 收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出 CMS 收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”
。初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快;并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象
,所以这个阶段也是可以与用户线程同时并发的。标记清除算法.
# 8.CMS 收集器的缺点?
CMS 是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿
,一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。 CMS 收集器是 HotSpot 虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:
首先, CMS收集器对处理器资源非常敏感
。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。 CMS 默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过 25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS 对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。为了缓解这种情况,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的 CMS 收集器变种,所做的事情和以前单核处理器年代 PC 机操作系统靠抢占式多任务来模拟多核并行多任务的思想一样,是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变慢的时间更多了,但速度下降幅度就没有那么明显。实践证明增量式的 CMS 收集器效果很一般,从 JDK 7 开始,i-CMS 模式已经被声明为“deprecated”,即已过时不再提倡用户使用,到 JDK 9 发布后 i-CMS 模式被完全废弃。
然后,由于CMS收集器无法处理“浮动垃圾”(Floating Garbage)
,有可能出现“Concurrent Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC 的产生。在 CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此 CMS 收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在 JDK 5 的默认设置下, CMS 收集器当老年代使用了 68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX: CMSInitiatingOccu-pancyFraction 的值来提高 CMS 的触发百分比,降低内存回收频率,获取更好的性能。到了 JDK 6 时, CMS 收集器的启动阈值就已经默认提升至 92%。但这又会更容易面临另一种风险:要是 CMS 运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction 设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。
还有最后一个缺点, CMS是一款基于“标记-清除”算法实现的收集器
,如果对前面对垃圾回收器算法讲解还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。为了解决这个问题, CMS 收集器提供了一个-XX:+UseCMS-CompactAtFullCollection 开关参数(默认是开启的,此参数从 JDK 9 开始废弃),用于在 CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX: CMSFullGCsBeforeCompaction(此参数从 JDK 9 开始废弃),这个参数的作用是要求 CMS 收集器在执行过若干次(数量由参数值决定)不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为 0,表示每次进入 Full GC 时都进行碎片整理)。只能预防,不能根治。
# 9.CMS GC 失败后的预案
如果 CMS 运行期间预留的内存无法满足程序需要,那么就会出现一次“Concurrent Mode Failure”失败,此时 JVM 将启动后备预案: 使用 Serial Old 收集器重新对老年代进行 gc,这样一来,停顿时间就会很长。
# 10.CMS GC 的两种模式
在 Java 的 CMS(Concurrent Mark-Sweep)垃圾回收器中,存在两种不同的模式:后台模式(Background Mode)和前台模式(Foreground Mode),用于不同阶段的垃圾回收操作。
后台模式(Background Mode):
- 后台模式是 CMS 垃圾回收器的默认模式,也称为并发模式(Concurrent Mode)。
- 在后台模式下,垃圾回收器会尽量减少对应用程序线程的影响,通过在大部分 GC 过程中与应用程序并发执行,以减少停顿时间。
- 后台模式中的垃圾回收包括:初始标记、并发标记、重新标记和并发清理等阶段。其中,初始标记和重新标记需要暂停应用程序线程,但停顿时间较短;而并发标记和并发清理阶段会与应用程序线程并发执行,不会暂停应用程序的执行。
前台模式(Foreground Mode):
- 前台模式也称为 STW(Stop-The-World)模式,是 CMS 垃圾回收器的一部分,用于在并发标记阶段完成之后进一步完成 GC 过程。
- 前台模式会在执行并发标记后,暂停所有应用程序线程,进行重新标记和并发清理的最后阶段,以确保回收所有被标记为垃圾的对象。
- 前台模式的停顿时间通常比后台模式中的并发标记和并发清理阶段的停顿时间更长,因为在前台模式下,应用程序线程必须等待垃圾回收完成。
# 11.说说你对 G1 收集器的了解?
Garbage First(简称 G1)收集器面向局部收集的设计思路和基于 Region 的内存布局形式。
G1(Garbage-First)垃圾回收器是 Java HotSpot VM 的一种垃圾回收器,它于 Java 7u4 版本引入,并在 Java 9 及之后成为默认的垃圾回收器。G1 收集器是一种面向服务端应用的垃圾回收器,主要目标是提供更低的停顿时间和更高的吞吐量,以满足大内存堆的需求。
以下是 G1 垃圾回收器的主要特点和工作原理:
- 分代收集器:
- G1 回收器也是一种分代垃圾回收器,将堆内存划分为新生代、老年代和永久代(或元空间)。
- G1 主要关注于老年代的回收,而新生代的回收使用类似于其他收集器的方式,使用类似于 CMS 的并发标记-复制算法。
- 区域化的堆内存管理:
- G1 将整个堆划分成大小相等的多个区域(Region),每个区域的大小可以配置。
- 区域化的管理使得 G1 能够更加灵活地控制哪些区域进行垃圾回收,以便更快速地实现可预测的停顿时间。
- 并发标记-整理阶段:
- G1 采用了并发标记-整理算法,在并发标记阶段,应用程序线程与垃圾回收线程并发执行,标记所有存活对象。
- 在并发整理阶段,G1 将所有存活对象整理到一起,从而实现堆的整理和碎片的消除。
- 混合模式:
- G1 采用混合模式(Mixed Mode),它允许在并发标记阶段选择部分区域进行标记,而在后续阶段只回收部分区域,从而控制回收的时间和延迟。
- 可预测的停顿时间:
- G1 致力于实现可预测的停顿时间,通过控制回收的区域和时间划分,以及一些智能的启发式算法,使得 G1 的停顿时间相对稳定和可控。
- 大堆支持:
- G1 适用于大内存堆,其优势在于更好的利用多核 CPU 和更少的 GC 暂停时间。
# 12.Region 堆内存布局的原理?
虽然 G1 也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异: G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region)
,每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region 中还有一类特殊的 Humongous 区域
,专门用来存储大对象。 G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX: G1HeapRegionSize 设定,取值范围为 1MB ~ 32MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中, G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待。
虽然 G1 仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。
# 13.G1 收集器可预测停顿时间模型?
G1 收集器之所以能建立可预测的停顿时间模型,是因为它将 Region 作为单次回收的最小单元
,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。
更具体的处理思路是让 G1 收集器去跟踪各个 Region 里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX: MaxGCPauseMillis 指定,默认值是 200 毫秒),优先处理回收价值收益最大的那些 Region,这也就是“Garbage First”名字的由来。这种使用 Region 划分内存空间,以及具有优先级的区域回收方式,保证了 G1 收集器在有限的时间内获取尽可能高的收集效率。
# 14.跨 Region 引用问题,如何解决?
使用记忆集避免全堆作为 GC Roots 扫描,但在 G1 收集器上记忆集的应用其实要复杂很多,它的每个 Region 都维护有自己的记忆集
,这些记忆集会记录下别的 Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。 G1 的记忆集在存储结构的本质上是一种哈希表, Key 是别的 Region 的起始地址, Value 是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于 Region 数量比传统收集器的分代数量明显要多得多,因此 G1 收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验, G1 至少要耗费大约相当于 Java 堆容量 10%至 20%的额外内存来维持收集器工作。
# 15.G1 收集器并发标记阶段?
如何保证用户线程和垃圾回收线程互不影响?
CMS 收集器采用增量更新算法实现
,而 G1 收集器则是通过原始快照(SATB)算法
来实现的。此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建, G1 为每一个 Region 设计了两个名为 TAMS (Top at Mark Start)的指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。 G1 收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与 CMS 中的“Concurrent Mode Failure”失败会导致 Full GC 类似,如果内存回收的速度赶不上内存分配的速度, G1 收集器也要被迫冻结用户线程执行,导致 Full GC 而产生长时间“Stop The World”。
# 16.G1 收集器垃圾回收的步骤?
初始标记(Initial Marking):
仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。并发标记(Concurrent Marking):
从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。最终标记(Final Marking):
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。筛选回收(Live Data Counting and Evacuation):
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
从上述阶段的描述可以看出, G1 收集器除了并发标记
外,其余阶段也是要完全暂停用户线程的
。
# 17.jvm 堆内存结构?
java 堆分区新生代和老年代,新生代和老年代的比例为 新生代:老年代=1:2
新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor 。发生垃圾搜集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。 HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8 ∶1。
# 18.大对象直接分配到老年代吗?
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。
在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。 HotSpot 虚拟机提供了-XX: PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。
# 19.老年代存放的都是什么对象?
HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
对象通常在 Eden 区里诞生,如果经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,该对象会被移动到 Survivor 空间中,并且将其对象年龄设为 1 岁。对象在 Survivor 区中每熬过一次 Minor GC
,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold 设置。
# 20.年龄必须达到阈值才能晋升老年代?
为了能更好地适应不同程序的内存状况, HotSpot 虚拟机并不是永远要求对象的年龄必须达到-XX: MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold 中要求的年龄。
一共有四种情况:
- Survivor 空间不足以装下对象
- 大对象直接进入老年代
- 长期存活的对象-15(对象头 4 字节)
- 动态对象年龄判定(相同年龄的对象占用超过 Survivor 存储空间的一半)
# 21.什么是空间分配担保?
在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看-XX: HandlePromotionFailure 参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;不够的话继续进行一次 Full GC. 如果小于,或者-XX: HandlePromotionFailure 设置不允许冒险,那这时就要改为进行一次 Full GC。
# 22.JVM 运行时 GC 相关的参数
-Xms4096m //初始堆大小
-Xmx4096m //最大堆大小
-Xmn1536m //新生代大小 eden + from + to
-Xss512K //线程大小
-XX:NewRatio=2 //新生代和老年代的比例
-XX:MaxPermSize=64m //持久代最大值
-XX:PermSize=16m //持久代初始值
-XX:SurvivorRatio=8 // eden 区和survivor区的比例
-verbose:gc
-Xloggc:gc.log //输出gc日志文件
-XX:+UseGCLogFileRotation //使用log文件循环输出
-XX:NumberOfGCLogFiles=1 //循环输出文件数量
-XX:GCLogFileSize=8k //日志文件大小限制
-XX:+PrintGCDateStamps //gc日志打印时间
-XX:+PrintTenuringDistribution //查看每次minor GC后新的存活周期的阈值
-XX:+PrintGCDetails //输出gc明细
-XX:+PrintGCApplicationStoppedTime //输出gc造成应用停顿的时间
-XX:+PrintReferenceGC //输出堆内对象引用收集时间
-XX:+PrintHeapAtGC //输出gc前后堆占用情况
-XX:+UseParallelGC //年轻代并行GC,标记-清除
-XX:+UseParallelOldGC //老年代并行GC,标记-清除
-XX:ParallelGCThreads=23 //并行GC线程数, cpu<=8?cpu:5*cpu/8+3
-XX:+UseAdaptiveSizePolicy //默认,自动调整年轻代各区大小及晋升年龄
-XX:MaxGCPauseMillis=15 //每次GC最大停顿时间,单位为毫秒
-XX:+UseParNewGC //Serial多线程版
-XX:+UseConcMarkSweepGC //CMS old gc
-XX:+UseCMSCompactAtFullCollection //FullGC后进行内存碎片整理压缩
-XX:CMSFullGCsBeforeCompaction=n //n次FullGC后执行内存整理
-XX:+CMSParallelRemarkEnabled //启用并行重新标记,只适用ParNewGC
-XX:CMSInitiatingOccupancyFraction=80 //cms作为垃圾回收是,回收比例80%
-XX:ParallelGCThreads=23 //并行GC线程数,cpu<=8?cpu:5*cpu/8+3
-XX:-UseSerialGC //默认不启用,client使用时启用
-XX:+UseG1GC //启用G1收集器
-XX:-UseAdaptiveSizePolicy //默认,不自动调整各区大小及晋升年龄
-XX:PretenureSizeThreshold=2097152 //直接晋升到老年代的对象大小
-XX:MaxTenuringThreshold=15(default) //晋升到老年代的对象年龄,PSGen无效
-XX:-DisableExplicitGC //禁止在运行期显式地调用?System.gc()
-XX:+HeapDumpOnOutOfMemoryError //在OOM时输出堆内存快照
-XX:HeapDumpPath=./java_pid<pid>.hprof //堆内存快照的存储路径
-XX:+CMSScavengeBeforeRemark //执行CMS重新标记之前,尝试执行一此MinorGC
-XX:+CMSPermGenSweepingEnabled //开启永久代的并发垃圾收集
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 23.触发 GC 的类型?
Java 虚拟机会把每次触发 GC 的信息打印出来,可以根据日志来分析触发 GC 的原因。
- GC_FOR_MALLOC:表示是在堆上分配对象时内存不足触发的 GC。
- GC_CONCURRENT:当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发 GC 操作来释放内存。
- GC_EXPLICIT:表示是应用程序调用 System.gc、VMRuntime.gc 接口或者收到 SIGUSR1 信号时触发的 GC。
- GC_BEFORE_OOM:表示是在准备抛 OOM 异常之前进行的最后努力而触发的 GC。
# 24.查看当前使用的垃圾收集器?
java -XX:+PrintCommandLineFlags -version
# 25.垃圾收集器比较?
# 26.Minor GC 与 Full GC
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢 10 倍以上。
Minor GC 与 Full GC 触发条件:
当 Eden 区没有足够的空间进行分配时
老年代最大可用连续空间大于Minor GC 历次晋升到老年代对象的平均大小
Full GC 触发条件:
调用 System.gc()时(系统建议执行 Full GC,但是不必然执行)
老年代空间不足时
方法区空间不足时
老年代最大可用连续空间小于Minor GC 历次晋升到老年代对象的平均大小
CMS GC 在垃圾回收的时候,当对象从 Eden 区进入 Survivor 区,Survivor 区空间不足需要放入老年代,而老年代空间也不足时
# 27.为什么 GC 分代年龄为 15?
因为对象头中的 Mark Word 采用 4 个 bit 位来保存年龄,4 个 bit 位能表示的最大数就是 15
对象头包含两类信息:
一种是 Mark Word:
用于存储对象自身的运行时数据,如 HashCode,GC 的分代年龄,锁状态标志,线程持有的锁,偏向线程 ID,偏向时间戳等。这部数据的长度在 32 位和 64 位的虚拟机中分别为 32 比特和 64 比特。(Mark Word 的 32 个比特存储空间中的 25 个比特用于存储对象的 HashCode,4 个比特存储对象分代年龄,2 个比特存储锁标志位,一个比特固定为 0)
另一种是 Klass Pointer 类型指针:
即对象指向它的类型元数据的指针,Java 通过这个指针确定该对象是哪个类的实例。但是并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息不一定要经过对象本身。
MetaSpace 主要由 Klass Metaspace 和 NoKlass Metaspace 两大部分组成。
- Klass MetaSpace: 就是用来存 Klass 的,就是 Class 文件在 JVM 里的运行时数据结构,这部分默认放在 Compressed Class Pointer Space 中,是一块连续的内存区域,紧接着 Heap。Compressed Class Pointer Space 不是必须有的,如果设置了
-XX:-UseCompressedClassPointers
,或者-Xmx
设置大于 32 G,就不会有这块内存,这种情况下 Klass 都会存在 NoKlass Metaspace 里。 - NoKlass MetaSpace: 专门来存 Klass 相关的其他的内容,比如 Method,ConstantPool 等,可以由多块不连续的内存组成。虽然叫做 NoKlass Metaspace,但是也其实可以存 Klass 的内容.
# 28.说说 ZGC?
- 在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停颇时间限制在十毫秒以内的低延迟。
- ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器。
- ZGC 的工作过程可以分为 4 个阶段:并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射等。
- ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的。所以停顿时间几乎就耗费在初始标记上,这部分的实际时间是非常少的。
# 三.JVM 调优
# 1. jps 作用与使用?
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类 Main Class, main()函数所在的类名称以及这些进程的本地虚拟机唯一 ID (LVMID, Local Virtual Machine Identifier)。虽然功能比较单一,但它绝对是使用频率最高的 JDK 命令行工具,因为其他的 JDK 工具大多需要输入它查询到的 LVMID 来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说, LVMID 与操作系统的进程 ID (PID, Process Identifier)是一致的,使用 Windows 的任务管理器或者 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就必须依赖 jps 命令显示主类的功能才能区分了。
- -q:只输出进程 id
- -m:输出虚拟机启动时传递给 main()方法的参数
- -l:输出主类的全名,如果执行的是 jar 包,输出 jar 包的路径
- -v:输出启动虚拟机的参数
# 2.jstat 作用与使用?
jstat (JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有 GUI 图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。
#参数interval和count代表查询间隔和次数,如果省略这2个参数,说明只查询一次。假设需要每1000毫秒查询一次进程42339垃圾收集状况,一共查询10次,那命令应当是:
jstat -gc 42339 1000 10
2
上图显示了各个区以及垃圾回收的情况,具体代表含义如下(C 代表 Capacity,U 代表 Used 已使用大小)
- S0C 和 S1C 代表 Survivor 区的 S0 和 S1 的大小
- S0U 和 S1U 表示已使用空间
- EC 表示 Eden 区大小,EU 表示 Eden 区已使用容量
- OC 表示老年代大小,OU 代表老年代已使用容量
- MC 表示方法区大小,MU 表示方法区已使用容量
- CCSC 表示压缩类空间大小,CCSU 表示压缩类空间已使用大小
- YGC 表示新生代 GC 次数,YGT 表示新生代 GC 总耗时
- FGC 表示 Full GC 次数,FGCT 表示 FULL GC 总耗时
- GCT 表示 GC 总耗时时间
选项 option 代表用户希望查询的虚拟机信息,主要分为三类:类加载、垃圾收集、运行期编译状况。
# 3.jstack 作用?
jstack (Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为 threaddump 或者 javacore 文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过 jstack 来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack 进程号 > stack1.log
option 选项的合法值与具体含义下图所示。
从 JDK 5 起, java.lang.Thread 类新增了一个 getAllStackTraces()方法用于获取虚拟机中所有线程的 StackTraceElement 对象。使用这个方法可以通过简单的几行代码完成 jstack 的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈
# 4.jinfo 作用与使用?
jinfo (Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。注意,如果是修改,参数类型是 manageable 类型才能修改。
#查看某个java进程的name属性的值
jinfo -flag name PID
2
# 5.jmap 作用与使用?
jmap (Memory Map for Java)命令用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。
jmap 的作用并不仅仅是为了获取堆转储快照,它还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
#打印出堆内存相关信息
jmap -heap PID
#生成dump文件
jmap -dump:format=b,file=/usr/heap.hprof pid
#OOM时自动生成dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
2
3
4
5
6
7
8
jmap 可以添加的参数如下:
# 6.jhat 作用与使用?
JDK 提供 jhat (JVM Heap Analysis Tool)命令与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。
jhat heap.hprof
然后访问地址 http://localhoust:7000/ 可以看到这款工具展示的信息比较简单。
生产上应该结合 dump 文件分析工具 MAT 或者 VisualVM 工具进行分析使用
# 7.jvm 调优实用工具?
jconsole 工具
VisualVM 工具
监控应用程序的 CPU、GC、堆。方法区和线程信息(jstack 和 jstat 的功能)
dump 文件以及分析(jmap 和 jhat 的功能)
方法级的程序性能分析,可以找出被调用最多,运行时间最长的方法
离线程序快照:收集程序运行时配置、线程 dump。内存 dump 等信息建立一个快照,并可以将快照发送给开发者进行 bug 反馈。
插件化处理,有无限扩展可能
# 8.打印 gc 日志的命令?
#打印gc日志的命令
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/Users/lizhengqiang/Documents/gc.log
2
找到 gc.log 文件,刚开始没有发生 GC,所以文件是空的,等到发生 GC 后打开
# 9.内存泄漏和内存溢出?
内存泄漏:是指创建的对象已经没有用处,正常情况下应该会被垃圾收集器回收,但是由于该对象仍然被其他对象进行了无效引用,导致不能够被垃圾收集器及时清理,这种现象称之为内存泄漏。内存泄漏会导致内存堆积,最终发生内存溢出,导致 OOM。
内存溢出:java 堆用于存储对象实例,只要不断的创建实例,并保证 GC roots 到对象是可达的,避免被回收,对象数量达到堆的最大容量时就会出现内存溢出异常.
出现 Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
//设置jvm参数 VM Args:-Xms20m-Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class Jvm_01_HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
内存溢出分三种情况
OutOfMemoryError: PermGen space
Permanent Generation space 这个区域主要用来保存加来的 Class 的一些信息,在程序运行期间属于永久占用的,Java 的 GC 不会对他进行释放,所以如果启动的程序加载的信息比较大,超出了这个空间的大小,就会发生溢出错误;
解决的办法:
增加空间分配——增加 java 虚拟机中的 XX:PermSize 和 XX:MaxPermSize 参数的大小,其中 XX:PermSize 是初始永久保存区域大小,XX:MaxPermSize 是最大永久保存区域大小。
OutOfMemoryError:Java heap space
heap 是 Java 内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC 又来不及释放的时候,就会发生溢出错误。Java 中对象的创建是可控的,但是对象的回收是由 GC 自动的,一般来说,当已存在对象没有引用(即不可达)的时候,GC 就会定时的来回收对象,释放空间。但是因为程序的设计问题,导致对象可达但是又没有用(即前文提到的内存泄露),当这种情况越来越多的时候,问题就来了。 针对这个问题,我们需要做一下两点:
1、检查程序,减少大量重复创建对象的死循环,减少内存泄露。 2、增加 Java 虚拟机中 Xms(初始堆大小)和 Xmx(最大堆大小)参数的大小。
StackOverFlowError
stack 是 Java 内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。针对这个问题,除了修改配置参数-Xss 参数增加线程栈大小之外,优化程序是尤其重要。
# 10.什么是对象逃逸?
什么是对象逃逸?对象逃逸优化有哪几种?
逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
#在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析。
2
3
优化有三种:栈上分配;标量替换;锁消除(或称同步消除)。
栈上分配(Stack Allocations):在 Java 虚拟机中, Java 堆上分配创建对象的内存空间几乎是 Java 程序员都知道的常识, Java 堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。
标量替换(Scalar Replacement):若一个数据已经无法再分解成更小的数据来表示了, Java 虚拟机中的原始数据类型(int 、 long 等数值类型及 reference 类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate), Java 中的对象就是典型的聚合量。如果把一个 Java 对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。
同步消除(Synchronization Elimination):线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉。
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
2
3
4
5
6
7
每个 StringBuffer.append()方法中都有一个同步块,锁就是 sb 对象在 concatString()方法内部。也就是 sb 的所有引用都永远不会逃逸到 concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地消除掉 。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略。虚拟机观察变量 sb,经过逃逸分析后会发现它的动态作用域被限制所有的同步措施而直接执行。
# 11.什么是锁粗化?
# 12.有过 jvm 调优经验吗?
JVM 调优情况十分复杂,各种情况都可能导致垃圾回收不能够达到预想的效果。对于场景问题,可以从如下几个大方向进行设计:
MinorGC
频繁
- MinorGC 是针对新生代进行回收的,每次在 MGC 存活下来的对象,会移动到 Survivor1 区。
- 大访问压力下, MGC 频繁一些是正常的,只要 MGC 延迟不导致停顿时间过长或者引发 FGC
- 可以适当的增大 Eden 空间大小,降低频繁程度,同时要保证,空间增大对垃圾回收时间产生的停顿时间增长也是可以接受的。
Full GC
- 如果 MinorGC 频繁,且容易引发 Full GC 。
- 每次 MGC 存活的对象的大小,是否能够全部移动到 S1 区,如果 S1 区大小< MGC 存活的对象大小,这批对象会直接进入老年代。
- 这批对象的年龄才 1 岁,很有可能再多等 1 次 MGC 就能被回收了,可是却进入了老年代,只能等到 Full GC 进行回收,很可怕。这种情况下,应该在系统压测的情况下,实时监控 MGC 存活的对象大小,并合理调整 eden 和 s 区的大小以及比例。
- 还有一种情况会导致对象在未达到 15 岁之前,直接进入老年代,就是 S1 区的对象,相同年龄的对象所占总空间大小>s1 区空间大小的一半,所以为了应对这种情况,对于 S 区的大小的调整就要考虑:尽量保证峰值状态下, S1 区的对象所占空间能够在 MGC 的过程中,相同对象年龄所占空间不大于 S1 区空间的一半,因此对于 S1 空间大小的调整,也是十分重要的。
大对象
创建频繁,导致 Full GC 频繁。- 对于大对象, JVM 专门有参数进行控制,-XX: PretenureSizeThreshold 。超过这个参数值的对象,会直接进入老年代,只能等到 full GC 进行回收,所以在系统压测过程中,要重点监测大对象的产生。
- 如果能够优化对象大小,则进行代码层面的优化,优化如:根据业务需求看是否可以将该大对象设置为单例模式下的对象,或者该大对象是否可以进行拆分使用,或者如果大对象确定使用完成后,将该对象赋值为 null,方便垃圾回收。
- 如果代码层面无法优化,则需要考虑
- 调高-XX: PretenureSizeThreshold 参数的大小,使对象有机会在 eden 区创建,有机会经历 MGC 以被回收。但是这个参数的调整要结合 MGC 过程中 Eden 区的大小是否能够承载,包括 S1 区的大小承载问题。
- 这是最不希望发生的情况,如果必须要进入老年代,也要尽量保证,该对象确实是长时间使用的对象,放入老年代的总对象创建量不会造成老年代的内存空间迅速长满发生 Full GC,在这种情况下,可以通过定时脚本,在业务系统不繁忙情况下,主动触发 full gc。
- 内存泄漏导致的 MGC 和 FGC 频繁,最终引发 oom 。
- 纯代码级别导致的 MGC 和 FGC 频繁。
- 如果是这种情况,那就只能对代码进行大范围的调整,这种情况就非常多了,而且会很糟糕。
- 如大循环体中的 new 对象,未使用合理容器进行对象托管导致对象创建频繁,不合理的数据结构使用等等。
MGC 与 FGC 停顿时间长导致影响用户体验。其实对于停顿时间长的问题无非就两种情况:
- a:gc 真实回收过程时间长,即 realtime 时间长。这种时间长大部分是因为内存过大导致,导致从标记到清理的过程中需要对很大的空间进行操作,导致停顿时间长。
- b:gc 真实回收时间 real time 并不长,但是 user time(用户态执行时间)和 systime (核心态执行时间)时间长,导致从客户角度来看,停顿时间过长。
对于 a 情况,要考虑减少堆内存大小,包括新生代和老年代,比如之前使用 16G 的堆内存,可以考虑将 16G 内存拆分为 4 个 4G 的内存区域,可以单台机器部署 JVM 逻辑集群,也可以为了降低 GC 回收时间进行 4 节点的分布式部署,这里的分布式部署是为了降低 GC 垃圾回收时间。
对于 b 情况,要考虑线程是否及时达到了安全点,通过-XX:+PrintSafepointStatistics 和-XX: PrintSafepointStatisticsCount=1 去查看安全点日志,如果有长时间未达到安全点的线程,再通过参数-XX:+SafepointTimeout 和-XX: SafepointTimeoutDelay=2000 两个参数来找到大于 2000ms 到达安全点的线程,这里的 2000ms 可以根据情况自己设置,然后对代码进行针对的调整。除了安全点问题,也有可能是操作系统本身负载比较高,导致处理速度过慢,线程达到安全点时间长,因此需要同时检测操作系统自身的运行情况。
# 13.JVM 两种常见异常?
StackOverFlowError
: 如果 Java 虚拟机栈容量不能动态扩展,而此时线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError
: 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展的时候,无法申请到足够的内存(Java 虚拟机堆中没有空闲内存,垃圾回收器也没办法提供更多内存)
# 14.堆栈溢出异常?
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemorvError 异常。
单线程 用-Xss 减少栈内存容量,来模拟 StackOverflowError 异常.
多线程,-Xss 设置大一些,模拟 OutOfMemoryError 异常.
# 15.方法区运行时常量池异常?
String.intern()是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。在 JDK1.6 及之前的版本中,由于常量池分配在永久代内,我们可以通过-Xx:PermSize 和- XX:MaxPermSize 限制方法区大小,从而间接限制其中常量池的容量.
public class Jvm_04_RuntimeConstantPooloOM {
public static void main(String[] args) {
//使用List保持着常量池引用,避免FullGC回收常量池行为
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer范围内足够产生00M了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
2
3
4
5
6
7
8
9
10
11
OutOfMemoryError 后面跟随的提示信息是“Perm Gen space”
# 16.本机直接内存异常?
DirectMemory 容量可通过-XX:MaxDirectMemorySize 指定,如果不指定,则默认与 Java 堆最大值(-Xmx 指定)一样,直接通过反射获取 Unsafe 实例进行内存分配(Unsafe 类的 getUnsafe))方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有 rtjar 中的类才能使用 Unsafe 的功能)。因为,虽然使用 DirectByteBuffer 分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是 unsafeallocateMemorv)。
public class Jvm_05_DirectMemory0OM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 17.CPU 过高问题排查?
1.top 命令
使用 top 查询到 cup 过高的进程 PID
top
2.查线程
#根据进程id查询占用高的线程id
ps H -eo pid,tid,%cpu | grep 19235
#查看十六进制的异常线程PID
printf "%x\n" 线程PId
#生成stack文件
jstack -l 3033 > ./3033.stack
#查看线程信息
cat 3033.stack |grep 'xxxx'
#查看使用最耗费cpu的线程堆栈信息
cat stack | grep -i 34670 -C10 --color
2
3
4
5
6
7
8
9
10
11
12
13
14
3.异常信息
#使用jstack查看进程,并查找指定线程的前100行信息
jstack pid |grep 线程id -A100
2
4.查看gc信息
jstat -gc pid 1000 100 > gctimes.log
5.使用脚本
sudo sh show-busy-java-threads -p 19235
# 18.生产环境访问慢?
1.查看错误日志
#最后1000行
tail -1000 xxx.log
2
如果看到 OOM
2.进容器排查
#获取容器id,并查看容器运行状态
docker stats
#进入容器
docker exec -it 47863d1021e9 /bin/bash
#十进制转十六进制
printf "%x\n" 24306
#找到进程pid
ps -ef |grep java
#查看gc情况,1000ms执行一次,一共执行10次
jstat -gc 进程pid 1000 10
#线程快照,用于排查线程间死锁、死循环、请求外部资源导致的长时间挂起等
jstack 进程pid
#打印出堆内存相关信息
jmap -heap 进程pid
#生成dump文件
jmap -dump:format=b,file=/home/heap.hprof 进程pid
#图形化展示
jhat heap.hprof
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
3.OOM 问题定位
#可以查看新生代,老年代堆内存的分配大小以及使用情况,看是否本身分配过小
jmap -heap pid
#结果以表格的形式显示存活对象的信息,并按照所占内存大小排序,找到最耗内存的对象
jmap -histo:live 进程pid | more
2
3
4
5
# 19.visualVM
在服务器上使用命令导出文件
#检测容器占用的内存和cpu,以及IO情况
docker stats
#进入容器
docker exec -it 容器id bash
#进入home目录
cd /home
#查找进程id
ps -ef|grep java
#生成dump文件
jmap -dump:format=b,file=/home/heap-$(date +%F%n).hprof 进程id
#拷贝文件到宿主机
docker cp ce4830fa74f7:/home/heap-2023-03-23.hprof /home/app/deepexi-dsc-belle-insight-command/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
打开 visualVM 软件,导入生成的 dump 文件,主要观测概览,对象,线程三个主要信息
summary:概览
objects:
对象信息
线程:
观测可能出现的死锁信息
在 idea 中使用,需要使用 visualVM 启动应用
# 20.jvm 调优如何提前调参?
JVM(Java 虚拟机)调优是优化 Java 应用程序性能的关键步骤之一。在实际应用中,提前调整 JVM 参数可以在一开始就为应用程序的性能和稳定性奠定良好的基础。以下是在提前调优 JVM 时应该考虑的一些关键方面:
了解应用需求: 在开始调优之前,了解应用程序的性能需求、资源使用情况以及预期的负载情况。这将有助于您根据实际情况选择合适的调优策略。
硬件资源: 考虑应用程序运行所需的硬件资源,例如内存、CPU 核数等。这有助于您设置合适的 JVM 参数,以充分利用可用资源。
内存设置: 调整堆内存大小是 JVM 调优的一个关键因素。您可以使用
-Xms
(初始堆大小)和-Xmx
(最大堆大小)参数来配置。合理地设置堆内存可以避免频繁的垃圾回收,从而提高性能。垃圾回收: 选择适当的垃圾回收器和调整相关参数是重要的。不同的应用场景可能需要不同的垃圾回收策略,例如 CMS、G1、Parallel 等。
线程设置: 根据应用程序的并发需求,调整与线程相关的参数,如线程池大小、并发线程数等。
GC 日志和监控: 启用 GC 日志记录以及相关监控工具(如 VisualVM、JConsole)有助于您分析垃圾回收性能,以及发现内存泄漏等问题。
PermGen/Metaspace 调整: 对于较老的 JVM 版本,考虑调整 PermGen 空间大小(使用
-XX:PermSize
和-XX:MaxPermSize
参数)。在 Java 8+中,PermGen 已被 Metaspace 取代。健康监控: 配置应用程序的健康监控,以及性能指标采集和报警机制,以便在出现问题时及时做出反应。
压力测试: 使用压力测试工具模拟实际负载,以观察应用程序在不同负载下的表现,从而根据测试结果进行调优。
逐步调整: 调优过程中建议逐步更改参数,然后进行测试和评估,避免一次性更改过多参数造成不可预测的结果。
调优是一个实验和优化的过程。根据应用程序的性质和需求,您可能需要尝试不同的参数设置并进行测试,以找到最佳的配置。同时,时刻关注最新的 JVM 版本和最佳实践,以确保您的应用程序在性能和稳定性方面保持在最佳状态。
# 21.如何做 JVM 预热?
通过流量控制来进行预热:
- 利用网关的流量控制功能,根据新服务上线的时间,给予不同的访问权重。这样,服务能够逐步达到正常访问的热度,避免因为流量过大导致服务崩溃。
- 使用 sentinel 等组件进行 warmup 限流,在服务刚上线时,将过高的流量直接拦截,防止对服务造成过大的压力,确保服务的稳定运行。
- spring 的 ribbon 组件策略改造,使其流量控制策略与网关的流量控制策略保持一致。这样,可以更好地协调各个组件之间的流量控制,提高服务的预热效果。
对外服务之前,通过合适的手段提前预热
- 服务开发者可以在编写代码时,设计一个初始化预热模块,该模块在服务启动后会自动执行。
- 在这个初始化模块中,可以编写逻辑来遍历所有的重要访问接口,这样在服务启动后,就能对这些接口进行预热。
- 这种方式能够确保服务在启动后的早期阶段,就对重要的访问接口进行了遍历,提高了服务的响应速度和稳定性。
# 22.内存溢出查看
docker stats
是 Docker 命令行工具的一部分,用于显示关于运行中容器的实时资源使用情况的统计信息。下面是对 docker stats
命令的中文详细解释:
#查看状态
docker stats
#显示指定容器
docker stats container_name
#显示所有容器(包括停止的)
docker stats --all
#指定格式
docker stats --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"
2
3
4
5
6
7
8
9
10
11
docker stats
输出的信息包括以下列:
- CONTAINER:容器的名称或 ID。
- CPU %:CPU 使用率。
- MEM USAGE / LIMIT:内存使用量和限制。
- MEM %:内存使用率。
- NET I/O:网络输入/输出。
- BLOCK I/O:块设备输入/输出。
- PIDS:进程 ID 数量。
如果机器内存充足的情况下,可以适当调大堆内存的大小:
Xmx 不能大于总内存的 3/4
ENV JAVA_OPTS="\
-Xms4g \
-Xmx4g \
-Xmn2g \
-Xss1m \
-XX:SurvivorRatio=8 \
-XX:MaxTenuringThreshold=10 \
-XX:+UseConcMarkSweepGC \
-XX:CMSInitiatingOccupancyFraction=70 \
-XX:+UseCMSInitiatingOccupancyOnly \
-XX:+AlwaysPreTouch \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=:./logs/gc \
-verbose:gc \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintGCTimeStamps \
-Xloggc:./logs/gc/gc.log \
"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 23.如何导出堆内存文件
非容器启动
#第一步:通过jps命令确认jvm进程号
[root@dataprocess-server]# jps -l
19570 customer-datap-1.3.2.jar
10589 sun.tools.jps.Jps
#第二步:通过jmap命令dump堆内存文件到指定目录
[root@dataprocess-server]# jmap -dump:format=b,file=/temp/dump.thprof 19570
Dumping heap to /temp/dump.thprof ...
Heap dump file created
2
3
4
5
6
7
8
9
配置OOM自动生成dump文件:
在 Java 虚拟机(JVM)启动时,可以通过设置一些参数来配置 OutOfMemoryError(OOM)时自动生成 Dump 文件。Dump 文件是 JVM 在遇到 OOM 时生成的一种内存转储文件,它包含了 JVM 堆内存的快照,有助于诊断内存溢出问题。
#JVM在发生OutOfMemoryError时生成Heap Dump文件
java -XX:+HeapDumpOnOutOfMemoryError -jar your_application.jar
#指定Heap Dump文件的输出路径
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/files -jar your_application.jar
#当发生OutOfMemoryError时,执行指定的命令
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/files -XX:OnOutOfMemoryError="kill -9 %p" -jar your_application.jar
2
3
4
5
6
7
8
容器启动:
#docker容器的基本信息
docker stats
#这里的PIDS是容器内的PID,基本上没什么用
2
3
Dockerfile 配置:
FROM openjdk:latest
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
WORKDIR /home
USER root
ENV PROFILE="dev"
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV JAVA_OPTS="\
-Xms4g \
-Xmx4g \
-Xmn2g \
-Xss1m \
-XX:SurvivorRatio=8 \
-XX:MaxTenuringThreshold=10 \
-XX:+UseConcMarkSweepGC \
-XX:CMSInitiatingOccupancyFraction=70 \
-XX:+UseCMSInitiatingOccupancyOnly \
-XX:+AlwaysPreTouch \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=:./logs/gc \
-verbose:gc \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintGCTimeStamps \
-Xloggc:./logs/gc/gc.log \
"
ENV PARAMS=""
COPY ./insight-provider/target/*.jar /home/app.jar
EXPOSE 80
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
RUN echo -e 'mkdir -p ./logs/gc && java $JAVA_OPTS -jar ./app.jar --spring.profiles.active=$PROFILE $PARAMS' > entrypoint.sh
ENTRYPOINT ["sh", "entrypoint.sh"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
启动脚本:
#!/usr/bin/env bash
CONTAINER_NAME=insight-command
IMAGE_NAME=xxx.xxx.com/xxx-uat/xxx-xx-xx-insight-command:$1
docker rm -f ${CONTAINER_NAME}
docker rmi ${IMAGE_NAME}
docker pull ${IMAGE_NAME}
docker run -d --name ${CONTAINER_NAME} \
--privileged=true \
-e PROFILE=uat \
-e PARAMS="--logging.level.root=info" \
-w /home \
-p 8090:80 \
-v $PWD/logs:/home/logs \
-v /home/uploads:/home/uploads \
--restart=always ${IMAGE_NAME}
docker logs -f --tail 500 ${CONTAINER_NAME}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 四.JVM 参数
# 1.堆参数
- –Xms:JVM 初始分配的堆内存,默认是物理内存的 1/64
- –Xmx:JVM 最大允许分配的堆内存,默认是物理内存的 1/4
建议将 Xms 和 Xmx 设为一样的值,避免每次垃圾回收完成后 JVM 重新分配内存。如果虚拟机启动时设置的 Xms 比较小,这个时候又需要初始化很多对象,虚拟机会不断地增加内存。
#设置堆的初始和最大都为1M
-Xms1024m -Xmx1024m
#设置堆的初始和最大都为3g
-Xms3g -Xmx3g
2
3
4
5
# 2.JVM 参数
所谓 JVM 调优就是设置一个合理的 JVM 参数,适合当前系统运行。
JVM 参数分为三类:
- 标准参数
- -X 参数
- -XX 参数
# 3.标准参数
以"-"开头的参数称为标准参数,是任何一个 JDK 版本都支持的,比较稳定,不会随 jdk 版本更新和改变。例如-version,-help,-server。
# 4.-X 参数
以-X 开头的参数是在特定版本 HotSpot 支持的命令,jdk 版本变化之后,参数可能变化,这个参数用的较少。
# 5.-XX 参数
-XX 是不稳定的参数,也是主要参数,分为 Boolean 类型和非 Boolean 类型。
Boolean 型
Boolean 型的-XX 参数使用格式为:
#语法
-XX:[+-]<name>
2
例如:
#表示启用CMS垃圾收集器
-XX:+UseConcMarkSweepGC
#表示启用G1垃圾收集器
-XX:+UseG1GC
#表示打印出所有的JVM参数信息
-XX:+PrintFlagsFinal
2
3
4
5
6
7
8
非 Boolean 型
非 Boolean 型的-XX 参数的使用格式为:
#name表示属性,value表示属性对应的值
-XX:<name>=<value>
2
例如:
#设置最大永久代空间大小为5M
-XX:MaxMetaspaceSize=5M
2
# 6.堆栈参数
还有一些非常有用的参数,比如-Xms,-Xmx,-Xss,实际上这几种参数也是属于-XX 参数,只是简写了。
#堆最大为1000M设置方式
-Xms1000M
-XX:InitialHeapSize=1000M
#堆初始
-Xmx1000M
-XX:MaxHeapSize=1000M
#栈最大
-Xss1000M
-XX:ThreadStackSize=1000M
2
3
4
5
6
7
8
9
10
11
# 7.常用 JVM 参数
设置 | 说明 |
---|---|
-XX:ClCompilerCount=3 | 最大并行编译数,大于 1 时可以提高编译速度,但会影响系统稳定性 |
-XX:InitialHeapSize=100m | 初始堆大小,可以简写为-Xms100 |
-XX:MaxHeapSize | 最大堆大小,可以简写为-Xmx100 |
-XX:NewSize=20m | 设置年轻代大小 |
-XX:MaxNewSize | 设置年轻代最大值 |
-XX:OldSize=50m | 设置老年代大小 |
-XX:MetaspaceSize=50m | 设置方法区大小,jdk1.8 才有,用元空间代替方法区 |
-XX:+UseParallelGC | 设置 Parallel Scanvage 作为新生代收集器,系统默认会选择 Parallel Old 作为老年代收集器 |
-XX:NewRatio | 新生代和老年代的比值,比如 -XX:NewRatio=4 表示新生代:老年代=1:4 |
-XX:SurvivorRatio | 表示两个 S 区和 Eden 区的比值,比如-XX:SurvivorRatio=8 表示(S0+S1):Eden=2:8 |
# 8.如何查看 GC 日志?
查看 JVM 的垃圾回收(GC)日志并通过日志来评估 GC 是否正常运行是诊断和优化 Java 应用程序性能的重要步骤之一。以下是如何查看 GC 日志并分析它的一般步骤:
启用 GC 日志:
在运行 Java 应用程序时,可以通过以下 JVM 参数来启用 GC 日志:
-XX:+PrintGCDetails
:打印详细的 GC 信息。-XX:+PrintGCDateStamps
:在日志中打印 GC 发生的日期和时间。-Xloggc:<path-to-logfile>
:将 GC 日志输出到指定文件中,例如-Xloggc:/path/to/gc.log
。
例如,你可以在启动应用程序时使用以下命令启用 GC 日志:
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log -jar YourApp.jar
收集 GC 日志:
让应用程序运行一段时间,以便 GC 日志记录了足够的信息。
# 9.GC 日志分析
查看 GC 日志:
打开 GC 日志文件(例如 /path/to/gc.log
)并查看其中的内容。GC 日志通常包含有关 GC 活动的详细信息,包括 GC 类型、发生时间、持续时间、回收的对象数量等。
样例 GC 日志行:
2023-09-05T10:30:15.123-0400: [GC (Allocation Failure) 2023-09-05T10:30:15.123-0400: [ParNew: 5120K->512K(5632K), 0.0012619 secs] 8192K->8192K(19456K), 0.0013687 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
在这个示例中,可以看到一次新生代(ParNew)GC,它告诉我们发生的时间、GC 类型、持续时间以及内存使用情况。
分析 GC 日志:
通过查看 GC 日志,你可以分析以下关键信息:
- GC 的类型(新生代、老年代、Full GC 等)。
- GC 的频率和持续时间。
- 内存使用情况,包括堆内存和非堆内存的使用情况。
- 内存回收的效率,例如对象回收的百分比。
通过分析这些信息,你可以判断 GC 是否正常运行。常见的问题包括频繁的 Full GC、长时间的停顿、内存泄漏等。如果发现异常,可以进一步采取措施来调整堆大小、GC 策略等以优化应用程序性能。
使用工具:
除了手动分析 GC 日志,还可以使用工具如 VisualVM、GCMV、GCViewer 、GCEasy 等来可视化和分析 GC 日志,这些工具可以更方便地查看 GC 行为和性能指标。
# 10.Young GC 频繁?
Young GC(年轻代垃圾收集)频繁且每次只占用 10 毫秒的情况可能是正常的,也可能是不正常的,具体取决于你的应用程序的性质、内存配置和工作负载。以下是分析和处理这种情况的一般步骤:
了解应用程序性质:首先,你需要了解你的应用程序的性质。年轻代垃圾收集通常是正常的,因为它负责回收短暂存在的对象。频繁的 Young GC 可能是应用程序中大量对象的短暂生命周期导致的,这通常是正常的情况。
检查内存配置:确保你的堆内存大小和分代比例设置合理。如果年轻代过小,可能会导致频繁的 Young GC。你可以使用 JVM 参数来调整堆内存大小,例如
-Xmx
和-Xms
来设置最大堆和初始堆大小,以及-XX:NewRatio
来设置年轻代与老年代的比例。分析 GC 日志:查看 GC 日志以获取有关 Young GC 的详细信息。了解每次 Young GC 的频率、持续时间、回收的对象数量等信息。如果 Young GC 的频率较高,但每次 GC 的时间都很短,则可能是正常的。
考虑内存泄漏:如果频繁的 Young GC
持续存在
,可能是因为存在内存泄漏问题,导致对象无法被回收。在这种情况下,你可以使用内存分析工具(如 Eclipse MAT、VisualVM、YourKit 等)来检测和解决内存泄漏。性能测试和优化:如果频繁的 Young GC 影响了应用程序的性能,可以考虑进一步的
性能测试和优化
。优化措施可能包括减少对象的创建、调整垃圾收集器的类型、调整应用程序的算法等。监控和警报:建议设置监控和警报系统,以便在垃圾回收活动异常时及时发出警报。这样可以帮助你在问题发生时迅速采取行动。
Young GC 频繁并不一定是不正常的,但需要结合应用程序的性质和工作负载来评估。如果频繁的 Young GC 影响了应用程序的性能或持续时间很长,那么可能需要进一步的调查和优化。分析 GC 日志和使用监控工具可以帮助你更好地理解和管理垃圾收集行为。
# 五.GC 问题分类-高阶
# 1.IO 密集型和计算密集型
在 JVM 中解决方案:
- IO 交互型: 互联网上目前大部分的服务都属于该类型,例如分布式 RPC、MQ、HTTP 网关服务等,对内存要求并不大,大部分对象在 TP9999 的时间内都会死亡, Young 区越大越好。
- MEM 计算型: 主要是分布式数据计算 Hadoop,分布式存储 HBase、Cassandra,自建的分布式缓存等,对内存要求高,对象存活时间长,Old 区越大越好。
# 2.GC 问题分类
Unexpected GC: 意外发生的 GC,实际上不需要发生,我们可以通过一些手段去避免。
- Space Shock: 空间震荡问题,参见“场景一:动态扩容引起的空间震荡”。
- Explicit GC: 显示执行 GC 问题,参见“场景二:显式 GC 的去与留”。
Partial GC: 部分收集操作的 GC,只对某些分代/分区进行回收。
- CMS: Old GC 频繁,参见“场景五:CMS Old GC 频繁”。
- CMS: Old GC 不频繁但单次耗时大,参见“场景六:单次 CMS Old GC 耗时长”。
- ParNew: Young GC 频繁,参见“场景四:过早晋升”。
- Young GC: 分代收集里面的 Young 区收集动作,也可以叫做 Minor GC。
- Old GC: 分代收集里面的 Old 区收集动作,也可以叫做 Major GC,有些也会叫做 Full GC,但其实这种叫法是不规范的,在 CMS 发生 Foreground GC 时才是 Full GC,CMSScavengeBeforeRemark 参数也只是在 Remark 前触发一次 Young GC。
Full GC: 全量收集的 GC,对整个堆进行回收,STW 时间会比较长,一旦发生,影响较大,也可以叫做 Major GC,参见“场景七:内存碎片&收集器退化”。
MetaSpace: 元空间回收引发问题,参见“场景三:MetaSpace 区 OOM”。
Direct Memory: 直接内存(也可以称作为堆外内存)回收引发问题,参见“场景八:堆外内存 OOM”。
JNI: 本地 Native 方法引发问题,参见“场景九:JNI 引发的 GC 问题”。
# 3.动态扩容引起空间震荡
现象服务刚刚启动时 GC 次数较多,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整
原因在 JVM 的参数中 -Xms
和 -Xmx
设置的不一致,在初始化时只会初始 -Xms
大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC,另外,如果空间剩余很多时也会进行缩容操作.
解决尽量将成对出现的空间大小配置参数设置成固定的,如 -Xms
和 -Xmx
,-XX:MaxNewSize
和 -XX:NewSize
,-XX:MetaSpaceSize
和 -XX:MaxMetaSpaceSize
等。
# 4.显示 GC 的去与留
现象:除了扩容缩容会触发 CMS GC 之外,还有 Old 区达到回收阈值、MetaSpace 空间不足、Young 区晋升失败、大对象担保失败等几种触发条件,如果这些情况都没有发生却触发了 GC ?这种情况有可能是代码中手动调用了 System.gc 方法,此时可以找到 GC 日志中的 GC Cause 确认下。那么这种 GC 到底有没有问题,翻看网上的一些资料,有人说可以添加 -XX:+DisableExplicitGC
参数来避免这种 GC,也有人说不能加这个参数,加了就会影响 Native Memory 的回收。先说结论,笔者这里建议保留 System.gc,那为什么要保留?我们一起来分析下。
原因:找到 System.gc 在 Hotspot 中的源码,可以发现增加 -XX:+DisableExplicitGC
参数后,这个方法变成了一个空方法,如果没有加的话便会调用 Universe::heap()::collect
方法,继续跟进到这个方法中,发现 System.gc 会引发一次 STW 的 Full GC,对整个堆做收集。
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
JVMWrapper("JVM_GC");
if (!DisableExplicitGC) {
Universe::heap()->collect(GCCause::_java_lang_system_gc);
}
JVM_END
2
3
4
5
6
保留 System.gc 时会使用 Foreground Collector 时将会带来非常长的 STW.
如果禁用掉的话就会带来另外一个内存泄漏问题,此时就需要说一下 DirectByteBuffer,它有着零拷贝等特点,被 Netty 等各种 NIO 框架使用,会使用到堆外内存。堆内存由 JVM 自己管理,堆外内存必须要手动释放,DirectByteBuffer 没有 Finalizer,它的 Native Memory 的清理工作是通过 sun.misc.Cleaner
自动完成的,是一种基于 PhantomReference 的清理工具,比普通的 Finalizer 轻量些
解决:保留 System.gc
# 5.MetaSpace 区 OOM
现象:在最底层,JVM 通过 mmap 接口向操作系统申请内存映射,每次申请 2MB 空间,这里是虚拟内存映射,不是真的就消耗了主存的 2MB,只有之后在使用的时候才会真的消耗内存。申请的这些内存放到一个链表中 VirtualSpaceList,作为其中的一个 Node。
JVM 在启动后或者某个时间点开始,MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决。
原因:
MetaSpace 内存管理: 类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在 Metaspace 中的类元数据也是存活的,不能被回收。每个加载器有单独的存储空间,通过 ClassLoaderMetaspace 来进行管理 SpaceManager* 的指针,相互隔离的。
MetaSpace 弹性伸缩:可以动态的调整大小.
关键原因就是 ClassLoader 不停地在内存中 load 了新的 Class ,一般这种问题都发生在动态类加载等情况上
解决:dump 快照之后通过 JProfiler 或 MAT 观察 Classes 的 Histogram(直方图) 即可,或者直接通过命令即可定位, jcmd 打几次 Histogram 的图,看一下具体是哪个包下的 Class 增加较多就可以定位了。
# 6.过早晋升
现象:
分配速率接近于晋升速率,对象晋升年龄较小。
Full GC 比较频繁,且经历过一次 GC 之后 Old 区的变化比例非常大。
过早晋升的危害:
- Young GC 频繁,总的吞吐量下降。
- Full GC 频繁,可能会有较大停顿。
原因:
- Young/Eden 区过小: 过小的直接后果就是 Eden 被装满的时间变短,本应该回收的对象参与了 GC 并晋升,Young GC 采用的是复制算法,也就是 Young GC 耗时本质上就是 copy 的时间,没来及回收的对象增大了回收的代价,所以 Young GC 时间增加,同时又无法快速释放空间,Young GC 次数也跟着增加。
- 分配速率过大: 可以观察出问题前后内存的分配速率,如果有明显波动可以尝试观察网卡流量、存储类中间件慢查询日志等信息,看是否有大量数据被加载到内存中。
解决:增大 Young 区.
如果是分配速率过大
- 偶发较大:通过内存分析工具找到问题代码,从业务逻辑上做一些优化。
- 一直较大:当前的 Collector 已经不满足 Mutator 的期望了,这种情况要么扩容 Mutator 的 VM,要么调整 GC 收集器类型或加大空间。
# 7.Old GC 频繁?
现象:Old 区频繁的做 CMS GC,但是每次耗时不是特别长,整体最大 STW 也在可接受范围内,但由于 GC 太频繁导致吞吐下降比较多。
原因:这种情况比较常见,基本都是一次 Young GC 完成后,负责处理 CMS GC 的一个后台线程 concurrentMarkSweepThread 会不断地轮询,使用 shouldConcurrentCollect()
方法做一次检测,判断是否达到了回收条件。如果达到条件,使用 collect_in_background()
启动一次 Background 模式 GC。轮询的判断是使用 sleepBeforeNextCycle()
方法,间隔周期为 -XX:CMSWaitDuration
决定,默认为 2s。
解决:
- 内存 Dump: 使用 jmap、arthas 等 dump 堆进行快照时记得摘掉流量,同时分别在 CMS GC 的发生前后分别 dump 一次。
- 分析 Top Component: 要记得按照对象、类、类加载器、包等多个维度观察 Histogram,同时使用 outgoing 和 incoming 分析关联的对象,另外就是 Soft Reference 和 Weak Reference、Finalizer 等也要看一下。
- 分析 Unreachable: 重点看一下这个,关注下 Shallow 和 Retained 的大小。如下图所示,笔者之前一次 GC 优化,就根据 Unreachable Objects 发现了 Hystrix 的滑动窗口问题。
# 8.单次 CMS Old GC 耗时长
现象:CMS GC 单次 STW 最大超过 1000ms,不会频繁发生。某些场景下会引起“雪崩效应”,这种场景非常危险,我们应该尽量避免出现。
原因:CMS 在回收的过程中,STW 的阶段主要是 Init Mark 和 Final Remark 这两个阶段,也是导致 CMS Old GC 最多的原因,在初始标记阶段,整个过程比较简单,从 GC Root 出发标记 Old 中的对象,处理完成后借助 BitMap 处理下 Young 区对 Old 区的引用,整个过程基本都比较快,很少会有较大的停顿。
Final Remark 是最终的第二次标记,这种情况只有在 Background GC 执行了 InitialMarking 步骤的情形下才会执行,如果是 Foreground GC 执行的 InitialMarking 步骤则不需要再次执行 FinalRemark。Final Remark 的开始阶段与 Init Mark 处理的流程相同,但是后续多了 Card Table 遍历、Reference 实例的清理并将其加入到 Reference 维护的 pend_list
中,如果要收集元数据信息,还要清理 SystemDictionary、CodeCache、SymbolTable、StringTable 等组件中不再使用的资源。
解决:对 FinalReference 的分析主要观察 java.lang.ref.Finalizer
对象的 dominator tree,找到泄漏的来源。经常会出现问题的几个点有 Socket 的 SocksSocketImpl
、Jersey 的 ClientRuntime
、MySQL 的 ConnectionImpl
# 9.内存碎片&收集器退化
现象:并发的 CMS GC 算法,退化为 Foreground 单线程串行 GC 模式,STW 时间超长,有时会长达十几秒。其中 CMS 收集器退化后单线程串行 GC 算法有两种:
- 带压缩动作的算法,称为 MSC,上面我们介绍过,使用标记-清理-压缩,单线程全暂停的方式,对整个堆进行垃圾收集,也就是真正意义上的 Full GC,暂停时间要长于普通 CMS。
- 不带压缩动作的算法,收集 Old 区,和普通的 CMS 算法比较相似,暂停时间相对 MSC 算法短一些。
原因:晋升失败,增量收集担保失败,显示 GC
解决:
- 内存碎片: 通过配置
-XX:UseCMSCompactAtFullCollection=true
来控制 Full GC 的过程中是否进行空间的整理(默认开启,注意是 Full GC,不是普通 CMS GC),以及-XX: CMSFullGCsBeforeCompaction=n
来控制多少次 Full GC 后进行一次压缩。 - 增量收集: 降低触发 CMS GC 的阈值,即参数
-XX:CMSInitiatingOccupancyFraction
的值,让 CMS GC 尽早执行,以保证有足够的连续空间,也减少 Old 区空间的使用大小,另外需要使用-XX:+UseCMSInitiatingOccupancyOnly
来配合使用,不然 JVM 仅在第一次使用设定值,后续则自动调整。 - 浮动垃圾: 视情况控制每次晋升对象的大小,或者缩短每次 CMS GC 的时间,必要时可调节 NewRatio 的值。另外就是使用
-XX:+CMSScavengeBeforeRemark
在过程中提前触发一次 Young GC,防止后续晋升过多对象。
# 10.堆外内存 OOM
现象:内存使用率不断上升,甚至开始使用 SWAP 内存,同时可能出现 GC 时间飙升,线程被 Block 等现象,通过 top 命令发现 Java 进程的 RES 甚至超过了 -Xmx
的大小。出现这些现象时,基本可以确定是出现了堆外内存泄漏。
原因:
- 通过
UnSafe#allocateMemory
,ByteBuffer#allocateDirect
主动申请了堆外内存而没有释放,常见于 NIO、Netty 等相关组件。 - 代码中有通过 JNI 调用 Native Code 申请的内存没有释放。
解决:
JVM 使用 -XX:MaxDirectMemorySize=size
参数来控制可申请的堆外内存的最大值。在 Java 8 中,如果未配置该参数,默认和 -Xmx
相等。
# 11.JNI 引发的 GC 问题
现象:在 GC 日志中,出现 GC Cause 为 GCLocker Initiated GC。
原因:JNI(Java Native Interface)意为 Java 本地调用,它允许 Java 代码和其他语言写的 Native 代码进行交互。由于 Native 代码直接使用了 JVM 堆区的指针,如果这时发生 GC,就会导致数据错误。因此,在发生此类 JNI 调用时,禁止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。
解决:
- 添加
-XX+PrintJNIGCStalls
参数,可以打印出发生 JNI 调用时的线程,进一步分析,找到引发问题的 JNI 调用。 - JNI 调用需要谨慎,不一定可以提升性能,反而可能造成 GC 问题。
- 升级 JDK 版本到 14,避免 JDK-8048556 导致的重复 GC。
← 05-JVM基础篇 07-类成员加载的顺序 →