文章目录
JVM内存篇
上篇主要介绍了JVM的内存划分、类加载相关的内容,链接: JVM面试题之内存区域、类加载篇
引言
本文将为继续介绍JVM相关的知识,主要是老生常谈的JVM垃圾回收和以及在实际工作中JVM的优化相关的内容。
一、如何标记垃圾?
-
引用计数法:
给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率也很高
缺点:他很难解决对象之间相互循环引用的问题,基本上被抛弃 -
可达性分析法:
通过一系列的成为“GC Roots”(活动线程相关的各种引用,虚拟机栈帧引用,静态变量引用,JNI引用)的对象作为起始点,从这些节点ReferenceChains开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的;当前使用的方法主要是这种。 -
两次标记过程:
对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。
二、垃圾回收算法
垃圾回收算法:复制算法、标记清除、标记整理、分代收集
复制算法:(young)
复制算法主要用于新生代,新生代对象创建频繁、回收也同样频繁,存活时间短。复制算法是将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收;
优点:实现简单,内存效率高,不易产生碎片
缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低
标记清除:(cms)
标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象
缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚,需要预留空间给分配阶段的浮动垃圾
标记整理:(old)
标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题
分代收集
当前JVM主要实现都是分代收集,根据各个年代的特点选择合适的垃圾收集算法。
新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
老年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
MinorGC、MajorGC、FullGC
JVM的垃圾回收日志中可以看到不同类型的这三种GC,分别是什么意思呢?
- MinorGC 在年轻代空间不足的时候发生,
- MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC。
- FullGC 1、当老年代无法再分配内存的时候;2、元空间不足的时候;3、显示调用 System.gc 的时候。另外,像 CMS 一类的垃圾回收器,在 MinorGC 出现 promotion failure 的时候也会发生 FullGC。
对象优先在 Eden 区分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
大对象直接进入老年代
大对象是指需要连续内存空间的对象,比如很长的字符串以及数组。老年代直接分配的目的是避免在 Eden 区和 Survivor 区之间出现大量内存复制。
长期存活的对象进入老年代
虚拟机给每个对象定义了年龄计数器,对象在 Eden 区出生之后,如果经过一次 Minor GC 之后,将进入 Survivor 区,同时对象年龄变为 1,增加到一定阈值时则进入老年代(阈值默认为 15)
动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到阈值才能进入老年代。如果在 Survivor 区中相同年龄的所有对象的空间总和大于 Survivor 区空间的一半,则年龄大于或等于该年龄的对象直接进入老年代。
空间分配担保
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间总和,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立则进行 Full GC。
三、垃圾回收器
1. parallel Scavenge:(关注吞吐量)
Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
2. parallel old
Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。
3. JDK8-CMS:(关注最短垃圾回收停顿时间)
CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
- 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,STW。
- 并发标记:进行 ReferenceChains跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,STW。
- 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
4. JDK9-G1:(精准控制停顿时间,避免垃圾碎片)
G1是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是:
【1】基于标记-整理算法,不产生内存碎片。
【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
- 初始标记:Stop The World,仅使用一条初始标记线程对GC Roots关联的对象进行标记
- 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢
- 最终标记:Stop The World,使用多条标记线程并发执行
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行
5. JDK11-ZGC:(在不关注容量的情况获取最小停顿时间5TB/10ms)
着色笔技术:加快标记过程
读屏障:解决GC和应用之间并发导致的STW问题
支持 TB 级堆内存(最大 4T, JDK13 最大16TB)
最大 GC 停顿 10ms
对吞吐量影响最大,不超过 15%
缺点:对CPU资源敏感;⽆法处理浮动垃圾;使⽤“标记清除”算法,会导致⼤量空间碎⽚产⽣。
四、JVM性能调优
JVM性能调优,是针对对应进程的JVM状态以定位问题和解决问题并作出相应的优化
经常需要用到命令:jps、jinfo、jstat、jstack、jmap
jps:查看java进程及相关信息
jps -l 输出jar包路径,类全名
jps -m 输出main参数
jps -v 输出JVM参数
jinfo:查看JVM参数
jinfo 11666
jinfo -flags 11666
Xmx、Xms、Xmn、MetaspaceSize
jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收
jstat [option] LVMID [interval] [count]其中LVMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印) option参数解释:-gc 垃圾回收堆的行为统计-gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计-gcutil 垃圾回收统计概述-gcnew 新生代行为统计-gcold 年老代和永生代行为统计
jstack:查看JVM线程快照,jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环
jstack [-l] <pid> (连接运行中的进程) option参数解释:-F 当使用jstack <pid>无响应时,强制输出线程堆栈。-m 同时输出java和本地堆栈(混合模式)-l 额外显示锁信息
jmap:可以用来查看内存信息(配合jhat使用)
jmap [option] <pid> (连接正在执行的进程)option参数解释:-heap 打印java heap摘要-dump:<dump-options> 生成java堆的dump文件
开启GC日志
要开启Java应用程序的GC(Garbage Collection)日志记录以便监控和调试垃圾收集行为,你可以通过在启动Java应用程序时传递特定的JVM参数来实现。以下是一些常用的参数及其说明:
基本参数
-XX:+PrintGCDetails:打印详细的GC事件,包括每次GC的时间戳、原因、前/后堆大小、各代的使用情况等。
-XX:+PrintGCDateStamps:在GC日志中添加日期和时间戳。
-XX:+PrintGCTimeStamps:在GC日志中添加自JVM启动以来的时间戳。
-XX:+PrintGCCause:显示导致GC的原因。
-XX:+PrintTenuringDistribution:显示对象晋升到老年代的情况。
-XX:+PrintHeapAtGC:在每次GC前后打印堆的概览。
-XX:+UseGCLogFileRotation:启用日志文件旋转,允许多个日志文件。
-XX:NumberOfGCLogFiles=:指定日志文件的数量。
-XX:GCLogFileSize=:设定单个日志文件的最大尺寸。
-Xloggc::指定GC日志文件的位置及名称。
具体要进行何种操作来使得JVM的性能最大化,需要根据实际的运行情况和业务特征来选择合适的垃圾回收器、合适的内存比例,观察GC日志,使GC的次数及GC的单词耗时达到业务可接受范围的一个平衡。