jvm性能调优
2026-01-06 10:13:17 0 举报
AI智能生成
在进行JVM性能调优时,核心关注点应聚焦于内存管理、垃圾回收策略、线程池配置及应用程序性能监控。优化目标是为了提高吞吐量、减少延迟或限制内存占用。具体做法可能包括调整堆大小设置(-Xms和-Xmx标志)、选择合适的垃圾收集器(如G1、CMS或Parallel GC)以及调节其相关参数(例如新生代和老年代的比例)。此外,精确配置线程堆栈大小(-Xss)、启用或调整JIT编译器的行为,以及使用监控工具(如jstat、VisualVM等)来分析热点方法和性能瓶颈也至关重要。优化工作应逐步进行,并以数据为依据,逐步微调以达到最佳性能状态。
作者其他创作
大纲/内容
class字节码文件规范
操作码
JVM 虚拟机的字节码指令由⼀个字节⻓度的,代表着某种特定操作含义的数字
Java 虚拟机中的操作码的⻓度只有⼀个字节(能表示的数据是0~255),这意味着 JVM 指令集的操作码总数不超过 256 条。
调用方法指令
invokevirtual 指令
⽤于调⽤对象的实例⽅法,根据对象的实际类型进⾏分派(虚⽅法分派),这也是 Java 语⾔中最常⻅的⽅法分派⽅式。
invokeinterface 指令
⽤于调⽤接⼝⽅法,它会在运⾏时搜索⼀个实现了这个接⼝⽅法的对象,找出适合的⽅法进⾏调⽤。
invokespecial 指令
⽤于调⽤⼀些需要特殊处理的实例⽅法,包括实例初始化⽅法私有⽅法和⽗类⽅法。
invokestatic 指令
⽤于调⽤类静态⽅法(static ⽅法)。
invokedynamic 指令
⽤于在运⾏时动态解析出调⽤点限定符所引⽤的⽅法。并执⾏该⽅法。前⾯四条调⽤指令的分派逻辑都固定在 Java 虚拟机内部,⽤户⽆法改变,⽽invokedynamic指令的分派逻辑是由⽤户所设定的引导⽅法决定的。 Java 从诞⽣到现在,只增加过⼀条指令,就是invokedynamic。⾃ JDK7 ⽀持并开始进
⾏改进,这也是为 JDK8 实现Lambda表达式⽽做的技术储备。
⾏改进,这也是为 JDK8 实现Lambda表达式⽽做的技术储备。
try-cache-finally的执⾏流程
如果try语句块中出现了属于 Exception 或者其⼦类的异常,转到catch语句块处理。
如果try语句块中出现了不属于 Exception 或其⼦类的异常,转到finally语句块处理。
如果catch语句块中出现了任何异常,转到finally语句块处理
类装载子系统
jdk1.8
每个类加载器对加载过的类保持⼀个缓存
appClassLoader
extClassLoader
Bootstrap
双亲委派机制,即向上委托查找,向下委托加载
双亲委派机制有⼀个最⼤的作⽤就是要保护JDK内部的核⼼类不会被应⽤覆盖。
沙箱保护机制
preDefineClass
if ((name != null) && name.startsWith("java."))
throw new SecurityException
类加载过程
class.forname()
会走链接过程,就会涉及到内存分配
loadclass()并不会走连接过程,而是延迟连接,
JVM 需要先拿到类的元数据(Class 对象)才能知道这个类是什么。
JVM 需要先拿到类的元数据(Class 对象)才能知道这个类是什么。
只有正在用到该类时才会走连接过程
创建类的实例(new)
访问类的静态变量(getstatic)
调用类的静态方法(invokestatic)
使用反射调用类(Class.forName(“...”))
初始化一个类的子类(会先触发父类的初始化)
被标记为启动类的类(包含 main() 方法的类
访问类的静态变量(getstatic)
调用类的静态方法(invokestatic)
使用反射调用类(Class.forName(“...”))
初始化一个类的子类(会先触发父类的初始化)
被标记为启动类的类(包含 main() 方法的类
延迟连接优点
提升启动速度:这是最主要的原因。应用程序启动时,只需要加载和初始化最核心的类(如 main 类),大量的辅助类、库类只有在真正用到的时候才会进行连接和初始化。这让应用可以“秒开”,用户体验更好。
节省内存:很多类在程序运行期间可能根本不会被用到(例如上面例子中的 BigClass)。延迟连接确保了这些永远不会被使用的类也永远不会被加载到内存中并进行连接,节省了宝贵的方法区(元空间)内存
降低复杂性,避免不必要的加载失败:假设 Main 类引用了 OptionalDependency 类,但运行环境中这个依赖库是可选的。如果没有这个库,OptionalDependency 类就不存在
如果立即解析,JVM 在启动 Main 时就会因为找不到类而抛出 NoClassDefFoundError,导致程序无法启动。
如果延迟解析,只要程序逻辑不走到使用 OptionalDependency 的那条分支,应用程序就可以正常启动和运行。这增加了程序的健壮性和灵活性
如果延迟解析,只要程序逻辑不走到使用 OptionalDependency 的那条分支,应用程序就可以正常启动和运行。这增加了程序的健壮性和灵活性
运行时数据区域
栈
线程独有
先进后出结构
每个方法就是一个栈帧
局部变量表
局部变量表可以认为是⼀个数组结构,主要负责存储计算结果。存放⽅法参数和⽅法内部定义的局部变量。以 Slot 为最⼩单位,第0位就是存放的this
操作数栈
动态链接
库
库
动态链接库主要存储⼀些指向运⾏时常量池的⽅法引⽤。每个栈帧中都会包含⼀个指向运⾏时常量池中该栈帧所属⽅法的应⽤,持有这个引⽤是为了⽀持⽅法动态调⽤过程中的动态链接。
返回地址
返回地址存放调⽤当前⽅法的指令地址。⼀个⽅法有两种退出⽅式,⼀种是正常退出,⼀种是抛异常退出。如果⽅法正常退出,这个返回地址就记录下⼀条指令的地址。如果是抛出异常退出,返回地址就会通过异常表来确定
附加信息
附加信息主要存放⼀些 HotSpot 虚拟机实现时需要填⼊的⼀些补充信息。这部分信息不在 JVM 规范要求之内,由各种虚拟机实现⾃⾏决定。
堆
字符串常量池
字符串常量池
实例对象
对象的创建与内存分配
逃逸分析与标量替换:默认开启,如果对象没有逃逸出当前栈帧也就是方法,则会标量替换在栈上分配。栈上分配不了则会在堆上创建实例对象
JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
堆上创建实例对象
内存分配
指针碰撞
规整的内存划分,需要创建对象时,将记录指针往空余内存移动对象大小的位置即可
TLAB
防止并发创建对象内存地址竞争
JVM会默认开启XX:+UseTLAB,XX:TLABSize 指定TLAB大小
cas解决并发创建对象的内存地址竞争
空闲列表
针对非规整内存,jvm内部记录那些内存地址可用。维护的一张表
实例对象组成
对象头
mark word
32位系统4字节,64位系统8字节
Klass pointer 类型指针
开启指针压缩4字节,不开启8字节
如果是数组对象,还会用4字节来记录数组长度
实例数据
对齐填充
只有对象大小不满足8的倍数才会生效,目的是为了操作系统更好的寻址,提高性能。
类可实现最大接口个数65535
类在编译的时候就确定了局部变量表与操作数栈深度还有操作数个数。
依据对象年龄分代思想将堆内存分为
年轻代
eden
su1
su2
复制算法
老年代
算法标记清除,标记整理清除
对象
大对象直接进入老年代
-XX:PretenureSizeThreshold
可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代
这个参数只在 Serial 和ParNew两个收集器下
有效。
有效。
为了避免为大对象分配内存时的复制操作而降低效率
对象动态年龄判断
minorGc后,进入su区的对象就会触发对象动态年龄判断,将su区的对象年龄从1+2+3+n如果到n后1-n的那批对象大小超过su区的50%,则把年龄n(含)以上的对象都放入老年代
-XX:TargetSurvivorRatio可以指定,默认50%
长期存活的对象将进入老年代
对象每被Minor GC一次 年龄就会加1,最大年龄15,
可以通过参数 -XX:MaxTenuringThreshold 来设置
CMS收集器默认6岁
老年代空间分配担保机制
每次minorGc之前都会检查老年代的空间是不是比年轻代所有对象包含垃圾对象还大,如果老年代空间足够,则直接进行minorGC,如果空间不够,就看jvm有没有开启-XX:HandlePromotionFailure,1.8默认开启,开启了则看每次minorGC进入老年代的平均对象大小是不是跟当前老年代空间还大,如果是,则直接进行fullGC,之后在进行minorGC,如果平均对象小于老年代空间则进行MinorGC。
对象内存回收
引用记数法
一般不用无法解决对象相互引用情况
可达性分析算法
GC Roots
线程栈的本地变量、静态变量、本地方法栈的变量等等
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的
对象都是垃圾对象
对象都是垃圾对象
引用类型
强引用类型
普通的变量引用
弱引用
将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
软引用
将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放
新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
虚引用
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
程序计数器
线程将要执行的字节码指令
元数据区
类信息,常量,静态变量
如何判断一个类是无用的类
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
运行时常量池
未加载到元空间前叫静态常量池
常量池中主要存放两大类常量:字面量和符号引用
本地方法栈
本地方法库
参照图
相关调整参数
参照图
-Xms
最小堆内存
-Xmx
最大堆内存
-Xmn
设置年轻代内存大小
eden和suv区默认 8:1:1
-Xss
设置栈大小
越小能存的栈帧就越少,但是理论上能启用的线程就会越多
-XX:MetaspaceSize
设置元空间初始化大小默认21M
-XX:MaxMetaspaceSize
设置最大元空间大小
字节码执行引擎
解释执行
编译执行
c1
c2
混合模式
解释+编译
垃圾回收器
Serial收集器
-XX:+UseSerialGC -XX:+UseSerialOldGC
新生代采用复制算法,老年代采用标记-整理算法
它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
Parallel Scavenge收集器
-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代
jdk1.8默认收集器
多线程收集器,针对 多核cpu能发挥更大的优势
(-XX:ParallelGCThreads)指定收集线程数一般与cpu核数相同即可
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
新生代采用复制算法,老年代采用标记-整理算法
CMS收集器
-XX:+UseConcMarkSweepGC
一款针对老年代的垃圾收集器,新生代默认用parNewGC
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
早期低延迟收集探索后续在这基础上诞生了G1和shenandoah
收集过程步骤
初始标记
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快
并发标记
GC 线程与应用程序线程一起运行
重新标记
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要
是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。
是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。
三色标记
来源背景
在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。漏标的问题主要引入了三色标记算法来解决
描述
三色标记算法是把Gc roots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成三种颜色
黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段,仍然是白色的对象, 即代表不可达
增量更新
就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
关注新的引用关系。它的原则是:“有新的引用被创建了,那我标记阶段结束后再重新扫描一遍这些被修改的对象,确保不遗漏。”
如何工作:在写屏障中,CMS会记录下持有新引用的对象通过卡表标记为脏。在后续的重新标记阶段,GC会把这些记录下来的脏卡重新作为根进行扫描。
注意:它不直接关心被断开引用的旧对象,断开的对象本次不做清理,会成为浮动垃圾
对CMS的影响
产生浮动垃圾:因为不关心断开的引用(B),B即使已经死了,在本次GC中也不会被回收,成为浮动垃圾
重新标记阶段负担重:重新标记阶段需要重新扫描所有在并发阶段被修改过的对象,如果并发阶段修改很多,这个STW阶段就会变长,不利于低延迟目标。这是CMS的一个已知缺点。
SATB原始快照
就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发标记结束之后, 将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
哲学:关注旧的引用关系。它的原则是:“我在标记开始时给活对象拍了个快照,那么快照里的所有对象都必须要存活。任何导致快照中对象‘死亡’的操作都被视为无效,需要记录下来并补救。”
如何工作:在写屏障 A.field = C 中,G1的SATB机制会记录下被覆盖的旧值(B)。在写屏障中,它会将B压入一个线程本地的SATB队列。
背后的逻辑是:B在标记开始时是活的,你现在把它从A的引用中去掉了,如果我不记录一下,它可能就真的死了。为了保证快照的完整性,我必须先当它还活着。
背后的逻辑是:B在标记开始时是活的,你现在把它从A的引用中去掉了,如果我不记录一下,它可能就真的死了。为了保证快照的完整性,我必须先当它还活着。
对G1的影响:
减少重新标记工作量:SATB的关注点是“丢失”的旧引用(B),这些对象数量通常远小于“增量更新”中需要重新扫描的“被修改过”的对象(A)。这使得G1的最终标记阶段(STW)的处理工作量更小,更有利于实现低停顿目标。
可能产生更多浮动垃圾:因为它保守地保留了所有开始时快照中的对象,即使其中一些引用后来立刻被断开了。
减少重新标记工作量:SATB的关注点是“丢失”的旧引用(B),这些对象数量通常远小于“增量更新”中需要重新扫描的“被修改过”的对象(A)。这使得G1的最终标记阶段(STW)的处理工作量更小,更有利于实现低停顿目标。
可能产生更多浮动垃圾:因为它保守地保留了所有开始时快照中的对象,即使其中一些引用后来立刻被断开了。
并发清理
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
并发重置
重置本次GC过程中的标记数据
优点
并发收集、低停顿
缺点
对CPU资源敏感(会和服务抢资源)
无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了)
多标-浮动垃圾
gcroot引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”
针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-
XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop theworld,用serial old垃圾收集器来回收
相关核心参数
1. -XX:+UseConcMarkSweepGC:启用cms
2. -XX:ConcGCThreads:并发的GC线程数
3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
配置合理的占比能杜绝并发收集失败concurrent mode failure的情况
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
G1垃圾收集器
-XX:+UseG1GC
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC
停顿时间要求的同时,还具备高吞吐量性能特征
停顿时间要求的同时,还具备高吞吐量性能特征
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。
唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%
收集过程
初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快
并发标记(Concurrent Marking):同CMS的并发标记
最终标记(Remark,STW):同CMS的重新标记
筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时
间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young
GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young
GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够
的空region能够承载拷贝对象就会触发一次Full GC
的空region能够承载拷贝对象就会触发一次Full GC
Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
特点
并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短StopThe-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念
空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的
可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集
核心参数设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个
年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一
会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
ZGC收集器
-XX:+UseZGC
目标
支持TB量级的堆
最大GC停顿时间不超10ms
奠定未来GC特性的基础
最糟糕的情况下吞吐量会降低15%
ZGC内存布局
小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象
颜色指针Colored Pointers / Metadata Pointers
核心原理
在传统的 64 位系统上,指针是 64 位的,但现代硬件和操作系统实际上并没有使用全部的 64 位来寻址。AMD64 架构目前只使用了 48 位 来进行虚拟地址寻址(高 16 位必须是 0 或 1,称为符号扩展)。ZGC 巧妙地利用了这未使用的 高 16 位(或者更准确地说,是 42 位寻址下的 22 位可用位)来存储垃圾回收所需的元信息
Finalizable: 表示此对象只能通过 finalizer 访问,而不是强引用
Remapped: 表示该指针已被重映射(即,对象被移动后,其新地址的指针会设置此位)。如果一个指针的 Remapped 位是 1,说明它指向的是对象的新地址,是“正确”的指针。
Marked1: 用于标记阶段
Marked0: 同样用于标记阶段。
Marked0 和 Marked1 交替使用,每个垃圾回收周期切换一次,用于区分当前周期和上一个周期
读屏障
读屏障是 JVM 在从堆内存中读取引用字段时插入的一小段代码。可以把它想象成一个“钩子”或者“安全检查点”。
核心原理
每当你的 Java 代码执行类似 Object obj = someObject.field; 的操作时,在将 someObject.field 的值(一个指针)返回给程序使用之前,ZGC 会先介入执行一段读屏障代码。
这段代码的核心工作就是检查该指针的颜色(即它的元数据位)。具体来说,它检查这个指针的 “Remapped”位 和 当前周期的标记位(Marked0 或 Marked1)。
读屏障的逻辑可以简化为:
“如果我读到的指针没有被重映射(Remapped 位为 0),并且没有被标记为当前周期的存活对象(当前 MarkedX 位为 0),那么我就需要做一些处理。”
这段代码的核心工作就是检查该指针的颜色(即它的元数据位)。具体来说,它检查这个指针的 “Remapped”位 和 当前周期的标记位(Marked0 或 Marked1)。
读屏障的逻辑可以简化为:
“如果我读到的指针没有被重映射(Remapped 位为 0),并且没有被标记为当前周期的存活对象(当前 MarkedX 位为 0),那么我就需要做一些处理。”
作用
发现对象图:在标记阶段,读屏障拦截到未被标记的指针后,会将其加入标记队列,随后 GC 线程会递归标记这个新发现的对象。这是并发的基石,应用程序线程在运行的同时也在帮 GC 发现活对象。
修正指针:在重定位/迁移阶段,如果一个线程尝试读取一个已经被移动了的对象(其旧地址的指针 Remapped 位为 0),读屏障会拦截这个访问,并自动将其修正为指向新地址的指针(Remapped 位为 1),然后才返回给程序。这个过程对应用程序线程是完全透明的。
读屏障的开销非常低(只是一次位操作和条件判断)
收集过程
阶段 1: 初始标记 (Pause Mark Start)
行为:需要 STW。但这个阶段非常快,因为它只做一件事:从 GC Roots(如线程栈、静态变量等)开始,直接关联到的对象进行标记。
如何标记:ZGC 通过修改指针的 Marked0 或 Marked1 位(取决于当前周期)来标记对象为存活。
阶段 2: 并发标记 (Concurrent Mark)
行为:并发。GC 线程与应用程序线程一起运行。
如何工作:从初始标记的对象开始,遍历整个对象图。读屏障在这里至关重要:当应用程序线程访问堆中的引用时,读屏障会帮助发现并标记新的对象(将其加入标记栈)。这个阶段完成了绝大部分的标记工作。
阶段 3: 再标记 (Pause Mark End)
行为:需要 STW。这个阶段是为了“兜底”,处理在并发标记阶段结束时可能尚未处理完的少量引用。由于需要处理的数据量极少,这个停顿也非常短。
阶段 4: 并发准备和重定位 (Concurrent Prepare/Relocate)
准备:GC 分析堆中哪些区域包含的垃圾最多(即碎片化最严重),这些区域将被压缩(Compact)。
重定位:GC 线程并发地将存活对象从这些区域复制到新的区域。
对象被复制后,会在旧对象的地址上留下一个“转发表”(Forwarding Pointer),就像搬家后留在旧地址的新地址纸条。
读屏障再次发挥核心作用:当应用程序线程试图通过旧指针访问已移动的对象时,读屏障会拦截这个访问,通过“转发指针”找到新地址,自动修正该指针(设置 Remapped 位),然后才让程序使用。这样,后续所有访问都会直接使用正确的新指针
安全点和安全域
安全点
安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比
如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。
如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。
1. 方法返回之前
2. 调用某个方法之后
3. 抛出异常的位置
4. 循环的末尾
安全域
Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
jvm调优工具
Jmap
此命令可以用来查看内存信息,实例个数以及占用内存大小
jmap -histo 进程id
num:序号
instances:实例数量
bytes:占用空间大小
class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][
jmap -heap 进程id
查看堆使用信息
Jstack
用jstack加进程id查找死锁
jstack找出占用cpu最高的线程堆栈信息
使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
按H,获取每个线程的内存情况
找到内存和cpu占用最高的线程tid,比如19664
将pid转为十六进制得到 0x4cd0,此为线程id的十六进制表示
执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
Jinfo
查看正在运行的Java应用程序的扩展参数
jinfo -flags pid
查看java系统参数
jinfo - sysprops pid
Jstat
以查看堆内存各部分的使用量,以及加载类的数量
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况
S0C:第一个幸存区的大小,单位KB
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小(元空间)
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间,单位s
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间,单位s
GCT:垃圾回收消耗总时间,单位s
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小(元空间)
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间,单位s
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间,单位s
GCT:垃圾回收消耗总时间,单位s
jstat -gccapacity pid
堆内存统计
NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
EC:伊甸园区的大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:当前老年代大小
MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代gc次数
FGC:老年代GC次数
NGCMX:新生代最大容量
NGC:当前新生代容量
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
EC:伊甸园区的大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:当前老年代大小
MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代gc次数
FGC:老年代GC次数
jstat -gcnew pid
新生代垃圾回收统计
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
jstat -gcnewcapacity
新生代内存统计
NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0CMX:最大幸存1区大小
S0C:当前幸存1区大小
S1CMX:最大幸存2区大小
S1C:当前幸存2区大小
ECMX:最大伊甸园区大小
EC:当前伊甸园区大小
YGC:年轻代垃圾回收次数
FGC:老年代回收次数
NGCMX:新生代最大容量
NGC:当前新生代容量
S0CMX:最大幸存1区大小
S0C:当前幸存1区大小
S1CMX:最大幸存2区大小
S1C:当前幸存2区大小
ECMX:最大伊甸园区大小
EC:当前伊甸园区大小
YGC:年轻代垃圾回收次数
FGC:老年代回收次数
jstat -gcold pid
老年代垃圾回收统计
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
OC:老年代大小
OU:老年代使用大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
OC:老年代大小
OU:老年代使用大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
jstat -gcoldcapacity
老年代内存统计
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:老年代大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:老年代大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
常见问题
full gc比minor gc还多的原因有哪些?
元空间不够导致的多余full gc
显示调用System.gc()造成多余的full gc,这种一般线上尽量通过XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果
老年代空间分配担保机制
内存泄露到底是怎么回事
对于一些老旧数据没有及时清理导致一直占用着宝贵的内存
资源
资源
阿里巴巴Arthas
dashboard可以查看整个进程的运行情况,线程、内存、GC、运行环境信息
thread可以查看线程详细情况
thread加上线程ID 可以查看线程堆栈
thread -b 可以查看线程死锁
jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确
JDK17新特性
Switch 表达式增强
扩展switch语句,使其既可以作为语句使⽤,也可以作为表达式使⽤,并且两种形式都可以⽤“传统”或“简化”的作⽤域和控制流⾏为。同时添加了yield关键字,提供break 与switch返回值的功能。
可以将多个匹配写到⼀起
switch (name) {
case "李⽩", "杜甫", "⽩居易" -> System.out.println("唐代诗⼈");
case "苏轼", "⾟弃疾" -> System.out.println("宋代诗⼈");
default -> System.out.println("其他朝代诗⼈");
}
case "李⽩", "杜甫", "⽩居易" -> System.out.println("唐代诗⼈");
case "苏轼", "⾟弃疾" -> System.out.println("宋代诗⼈");
default -> System.out.println("其他朝代诗⼈");
}
每个分⽀直接返回⼀个值。
int tmp = switch (name) {
case "李⽩", "杜甫", "⽩居易" -> 1;
case "苏轼", "⾟弃疾" -> 2;
default -> {
System.out.println("其他朝代诗⼈");
yield 3;
}
};
case "李⽩", "杜甫", "⽩居易" -> 1;
case "苏轼", "⾟弃疾" -> 2;
default -> {
System.out.println("其他朝代诗⼈");
yield 3;
}
};
instanceof的模式匹配
instances 增加了模式匹配的功能,如果变量类型经过instances判断能够匹配⽬标类型,则对应分⽀中⽆需再做
类型强转。
类型强转。
if (o instanceof Integer i && i > 0) {
System.out.println(i.intValue());
} else if (o instanceof String s && s.startsWith("t")) {
System.out.println(s.charAt(0));
}
System.out.println(i.intValue());
} else if (o instanceof String s && s.startsWith("t")) {
System.out.println(s.charAt(0));
}
var 局部变量推导
对于某些可以直接推导出类型的局部变量,可以使⽤var进⾏声明。
var nums = new int[] {1, 2, 3, 4, 5};
var sum = Arrays.stream(nums).sum();
System.out.println("数组之和为:" + sum);
var sum = Arrays.stream(nums).sum();
System.out.println("数组之和为:" + sum);
模块化及类封装
记录类 record
public record Point(int x, int y) {
}
}
这个类只能初始化设置属性值,初始化后,不允许修改属性值,⽤反射也不⾏。唯⼀和我们⾃⼰写的 POJO
有点不同的是,获取属性的⽅法,与属性同名,⽽不再是getXXX这样的了。
有点不同的是,获取属性的⽅法,与属性同名,⽽不再是getXXX这样的了。
密封类 Sealed Classes
final,表示这个⼦类不能再被继承了。
non-sealed 表示这个⼦类没有密封特性,可以随意继承。
sealed 表示这个⼦类有密封特性。再按照之前的⽅式声明他的⼦类。
non-sealed 表示这个⼦类没有密封特性,可以随意继承。
sealed 表示这个⼦类有密封特性。再按照之前的⽅式声明他的⼦类。
public sealed abstract class Shape permits Circle, Rectangle, Square {
public abstract int lines();
}
public abstract int lines();
}
模块化 Module System
模块化在包之上增加了更⾼级别的聚合,它包括
⼀组密切相关的包和资源以及⼀个新的模块描述符⽂件。简单点说,module是 java 中package包的上⼀层抽象。
⼀组密切相关的包和资源以及⼀个新的模块描述符⽂件。简单点说,module是 java 中package包的上⼀层抽象。
引⼊模块化机制后,应⽤需要在每个模块的根⽬录下创建⼀个module-info.java⽂件,⽤来声明⼀个模块。然后
在这个⽂件中,⽤module关键字,声明⼀个模块
在这个⽂件中,⽤module关键字,声明⼀个模块
module roy.demomodule {
}
}
这样,当前⽬录下的所有package下的代码,都将属于同⼀个module。module名字必须全局唯⼀。⾄于具体的格式,没有强制要求,不过通常的惯例是类似于包结构,全部⽤⼩写,⽤.连接。
接下来就需要在roy.demomodule中声明module的⼀些补充信息。这些补充信息主要包括:
对其他module的依赖关系
当前module对外开放的 API
使⽤和提供的服务
对其他module的依赖关系
当前module对外开放的 API
使⽤和提供的服务
require 声明module依赖
在module-info.java中⾸先要声明当前module需要依赖哪些外部模块。⽐如,如果你要使⽤junit,那么除了要
在pom.xml中引⼊junit对应的依赖外,还需要在module-info.java中添加配置,否则项⽬编译就会报错。
在pom.xml中引⼊junit对应的依赖外,还需要在module-info.java中添加配置,否则项⽬编译就会报错。
requires junit;
exports 和 opens 声明对外的 API
exports关键字开放的成员在编译和运⾏时都可以访问,但是不能⽤反射的⽅式访问。如果想要通过反射的⽅式访问⼀个模块内的成员,需要改为使⽤opens关键字声明。声明⽅式和exports⼀样。
uses 服务开放机制
构建模块化 Jar 包
类加载机制调整
以往ExtClassLoader和AppClassLoader都继承⾃URLClassLoader,现在PlatformClassLoader和
AppClassLoader 都改为继承⾃BuildinClassLoader。在BuildinClassLoader中实现了新的模块化架构下类如何从
模块中加载的逻辑,以及模块中资源可访问性的处理。
AppClassLoader 都改为继承⾃BuildinClassLoader。在BuildinClassLoader中实现了新的模块化架构下类如何从
模块中加载的逻辑,以及模块中资源可访问性的处理。
GC调整
在 JDK14 中,就彻底删除了 CMS 垃圾回收器。与 CMS ⼀起退场的,还有Serial垃圾回收器
0 条评论
下一页