(1)JVM 由哪些部分组成?
JVM的主要组成部分包括加载器子系统(ClassLoader)、运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)以及本地方法接口(Native Interface, JNI)。
Java虚拟机(JVM)是一个运行Java程序的虚拟环境,它负责将Java字节码转换为机器码并执行。
理解记忆上述几个组成部分:
- 首先需要准备编译好的Java字节码文件(即class文件)。
- 然后需要先通过一定方式(类加载器)将class文件加载到内存中(运行时数据区)。
- 又因为字节码文件时是VM定义的一套指令集规范,底层操作系统无法直接执行,因为需要特定的命令解释器(执行引擎)将字节码翻译成特定的操作系统指令集交给CPU去执行。
- 这个过程会需要调用到一些不同语言为Java提供的接口(例如驱动、地图制作等),这就用到了本地方法接口(NativeInterface)。
(2)JVM 垃圾回收调优的主要目标是什么?
Java虚拟机(JVM)的垃圾回收(GC)是自动内存管理的核心机制,它负责回收不再使用的对象以释放内存空间。虽然GC是自动进行的,但不当的配置可能导致应用程序性能下降。
主要目标
1. 降低GC停顿时间
GC停顿时间(Stop-The-World, STW)是指JVM在执行某些GC操作时需要暂停所有应用线程的时间。调优的主要目标之一是减少这种停顿:
- 对于延迟敏感型应用(如交易系统、实时系统),需要最小化最大停顿时间
- 可以通过选择适当的GC算法(如G1、ZGC)来减少停顿
- 调整堆大小和区域大小可以影响停顿时间
2. 提高吞吐量
吞吐量是指应用程序执行时间占总运行时间(应用执行时间+GC时间)的比例:
- 目标通常是达到95%以上的吞吐量
- 对于批处理系统,高吞吐量比低延迟更重要
- 可以通过增大堆大小、调整GC线程数来提高吞吐量
3. 优化内存占用
合理的内存占用目标包括:
- 避免堆过大导致系统资源浪费
- 避免堆过小导致频繁GC
- 平衡不同内存区域(新生代/老年代)的比例
- 监控和优化非堆内存(元空间、线程栈等)
4. 避免内存泄漏和OOM
调优目标包括:
- 确保不再使用的对象能被及时回收
- 识别和修复因静态集合、缓存、监听器等导致的内存泄漏
- 合理设置堆大小以避免OutOfMemoryError
5. 选择合适的GC算法
根据应用特点选择GC算法:
- Serial GC:小型应用,单处理器
- Parallel GC:吞吐量优先
- CMS:低延迟,已废弃
- G1:平衡吞吐量和延迟
- ZGC/Shenandoah:超大堆,极低延迟
调优平衡艺术
GC调优往往需要在多个目标之间取得平衡:
- 低延迟 vs 高吞吐量
- 小堆(快速GC) vs 大堆(减少GC频率)
- 预测性 vs 适应性
(3)如何对 Java 的垃圾回收进行调优?
1. 确定调优目标
根据应用类型确定调优重点:
- Web应用:通常更关注低延迟
- 批处理应用:通常更关注高吞吐量
- 混合型应用:需要平衡两者
2 收集GC日志
启用GC日志是调优的第一步,添加以下JVM参数:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:<gc-log-file-path>
3 分析GC日志
使用工具如GCViewer、GCEasy等分析GC日志,关注:
- Full GC频率
- Young GC持续时间
- 内存回收效率
4 调整堆大小
推荐设置:
- Xms和Xmx设置为相同值,避免堆动态调整
- 新生代通常占堆的1/3到1/2
5 选择垃圾回收器
根据应用特点选择合适的回收器:
- 吞吐量优先:-XX:+UseParallelGC
- 低延迟优先:-XX:+UseG1GC 或 -XX:+UseZGC(Java 11+)
- 大内存应用:-XX:+UseG1GC
6 优化GC参数
常见调优参数示例:
// G1 GC调优示例
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:ConcGCThreads=4
// Parallel GC调优示例
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
-XX:MaxGCPauseMillis=500
(4)常用的 JVM 配置参数有哪些?
内存相关参数
1. 堆内存设置
- -Xms:初始堆大小,如 -Xms512m 设置初始堆为512MB
- -Xmx:最大堆大小,如 -Xmx2g 设置最大堆为2GB
- -Xmn:新生代大小,如 -Xmn256m
- -XX:NewRatio:老年代与新生代的比例,如 -XX:NewRatio=2 表示老年代是新生代的2倍
- -XX:SurvivorRatio:Eden区与Survivor区的比例,如 -XX:SurvivorRatio=8 表示Eden是单个Survivor的8倍
2. 元空间设置
- -XX:MetaspaceSize:初始元空间大小
- -XX:MaxMetaspaceSize:最大元空间大小
垃圾回收相关参数
1. 垃圾回收器选择
- -XX:+UseSerialGC:使用串行垃圾回收器
- -XX:+UseParallelGC:使用并行垃圾回收器
- -XX:+UseConcMarkSweepGC:使用CMS回收器
- -XX:+UseG1GC:使用G1回收器
- -XX:+UseZGC:使用ZGC(Java 11+)
- -XX:+UseShenandoahGC:使用ShenandoahGC
2. GC日志参数
- -XX:+PrintGCDetails:打印GC详细信息
- -XX:+PrintGCDateStamps:打印GC时间戳
- -Xloggc:<file>:将GC日志输出到文件
- -XX:+UseGCLogFileRotation:启用GC日志轮转
- -XX:NumberOfGCLogFiles=5:保留的GC日志文件数
- -XX:GCLogFileSize=20M:单个GC日志文件大小
性能调优参数
- -server:启用服务器模式(64位JVM默认)
- -client:启用客户端模式(32位JVM默认)
- -XX:+TieredCompilation:启用分层编译(Java 7+默认)
- -XX:CompileThreshold:方法调用多少次后编译为本地代码
- -XX:+UseFastAccessorMethods:优化getter/setter方法
故障排查参数
1. 内存溢出处理
-XX:+HeapDumpOnOutOfMemoryError:内存溢出时生成堆转储文件
-XX:HeapDumpPath= :指定堆转储文件路径
-XX:OnOutOfMemoryError=:内存溢出时执行指定命令
2. 调试参数
-XX:+PrintFlagsFinal:打印所有JVM参数最终值
-XX:+PrintCommandLineFlags:打印命令行设置的JVM参数
-XX:+TraceClassLoading:跟踪类加载过程
-XX:+PrintCompilation:打印方法编译信息
常用参数组合示例
1. Web应用典型配置
-Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ParallelRefProcEnabled
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
2. 微服务典型配置
-Xms512m -Xmx512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1HeapRegionSize=8m
-XX:+HeapDumpOnOutOfMemoryError
参数选择建议
- 根据应用类型选择合适的内存配置
- 根据硬件资源设置合理的堆大小
- 根据性能目标选择合适的GC算法
- 生产环境务必配置OOM时的堆转储
- 监控GC日志并根据实际情况调整参数
(5)JVM 的内存区域是如何划分的?
Java虚拟机运行时数据区分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。
1)方法区(Method Area):
- 存储类信息、常量、静态变量和即时编译器(JIT)编译后的代码。
- 属于线程共享区域,所有线程共享方法区内存。
- 在JDK8之前,HotSpot使用永久代(PermGen)来实现方法区,JDK8之后被元空间(Metaspace)取代,元空间使用的是本地内存(Native Memory)。
2)堆 (Heap):
用于存放所有线程共享的对象和数组,是垃圾回收的主要区域。
3)虚拟机栈 (JVM Stack) :
- 每个线程创建一个栈,用来保存局部变量、操作数栈、动态链接、方法出口信息等。
- 局部变量表中存储的是基本数据类型(如int、float)以及对象引用。
- 栈是线程私有的,生命周期与线程相同。
4) 本地方法栈(Native Method Stack):
- 为本地方法服务,使用JNI(JavaNativeInterface)调用的本地代码在此区域分配内存。
- 和虚拟机栈类似,也是线程私有的。
5) 程序计数器(Program Counter Register):
- 是一个小的内存区域,保存当前线程执行的字节码指令的地址或行号。
- 每个线程都有一个独立的程序计数器,属于线程私有。
还有一个 直接内存(Direct Memory) 这里也提一下,它属于JVM之外的内存区域:
- 由NIO 库通过ByteBuffer直接分配的内存。
- 直接内存的大小不受堆内存限制,但会受到本机内存的限制。
(6)JVM 有哪几种情况会产生 OOM(内存溢出)?
Java中常见的OOM情况可以概括为以下几种:
OOM错误类型 | 原因 | 常见解决办法 |
---|---|---|
Java Heap Space | 堆内存不足,常见于大量对象创建 | 优化对象使用或增大堆内存 |
StackOverflowError | 栈空间耗尽,常见于递归或深层嵌套调用 | 优化递归逻辑或增大栈空间 |
PermGen Space / Metaspace | 方法区或元空间不足,常见于频繁动态生成类的场景 | 增大方法区或元空间,减少类加载频率 |
Direct Buffer Memory | 直接内存不足,常见于 NIO 操作 | 增大直接内存限制或减少直接内存使用 |
Unable to Create New Native Thread | 线程数过多,超出系统资源限制 | 控制线程数或线程池大小,避免无限制创建新线程 |
GC Overhead Limit Exceeded | GC 时间过长且回收内存不足 | 增大堆内存、优化内存使用、调整 GC 策略 |
StackOverflowError虽然不算OutOfMemoryError但是一般可以归类一起说。
(7)怎么分析 JVM 当前的内存占用情况?OOM 后怎么分析?
分析 JVM 当前的内存占用情况,你可以使用以下方法和工具:
1. 实时监控工具:
- 使用 jvisualvm 或 jconsole 等工具可以实时监控 Java 应用的内存使用情况。这些工具提供了可视化界面,可以监控内存使用、查看线程、分析堆等。
2. JVM 命令行工具:
- jstat:用于监视 JVM 内存和垃圾回收情况,可以显示各个堆区的使用情况、GC 时间等。
- jmap:可以用来生成堆转储文件(Heap Dump),以便进行后续分析。
3. 分析工具:
- Eclipse Memory Analyzer (MAT):一个强大的 Java 堆分析工具,可以帮助识别内存泄漏和查看内存消耗情况。
- VisualVM:除了监控功能外,也支持加载和分析 Heap Dump 文件。
当 JVM 发生 OOM(OutOfMemoryError)后,分析步骤通常包括:
1. 获取 Heap Dump 文件:
确保 JVM 在遇到 OOM 时自动生成堆转储文件,可以通过设置 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump.hprof 参数来实现。
2. 使用分析工具分析 Heap Dump 文件:
- 使用 MAT 或 VisualVM 打开 Heap Dump 文件,关注以下几点:
- 查找内存中对象的分布,特别是占用内存最多的对象。
- 分析这些对象的引用链,确定是哪部分代码引起的内存泄漏或过度消耗。
- 检查 ClassLoader,以确认是否有过多的类被加载导致的元空间(Metaspace)OOM。
3. 分析日志和异常信息:
- OOM 之前的日志可能会提供一些导致内存溢出的操作或业务逻辑的线索。
4. 调整 JVM 参数:
- 根据分析结果,可能需要调整 JVM 的内存分配参数,如增加堆大小 -Xmx 或调整新生代与老年代的比例。
5. 代码优化:
- 如果分析发现内存泄漏,需要优化代码,减少内存消耗,修复内存泄漏。
6. 监控和预防:
- 在解决问题后,继续使用监控工具观察内存使用情况,预防未来的内存问题。
通过上述步骤,你可以有效地分析和解决 JVM 的内存占用问题。在实际操作中,可能需要根据具体情况调整分析方法和工具。