ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

一文读懂 Java JVM:从内存结构、类加载到 GC 与性能调优

一文读懂 Java JVM:从内存结构、类加载到 GC 与性能调优 1. JVM 是什么JVM全称 Java Virtual Machine即 Java 虚拟机。它是 Java 程序运行时的核心组件负责把.class字节码加载到内存中并在不同操作系统和硬件平台上执行。Java 常说的“一次编写到处运行”真正依赖的就是 JVM。Java 源码不会直接运行在操作系统上而是经历如下过程.java 源码 - javac 编译 - .class 字节码 - JVM 加载、校验、解释执行或 JIT 编译执行 - 操作系统与 CPU可以简单理解JDK开发工具包包含编译器、工具和运行环境。JRE运行环境包含 JVM 和核心类库。JVM真正执行 Java 字节码的虚拟机。2. JVM 的整体架构JVM 主要由四部分组成Class Loader 类加载器 Runtime Data Areas 运行时数据区 Execution Engine 执行引擎 Native Interface 本地方法接口简化架构如下Java 源码javac 编译class 字节码类加载器 ClassLoader运行时数据区执行引擎解释器JIT 编译器垃圾回收器 GC本地方法接口 JNI操作系统 Native Library其中最重要的是类加载器负责加载.class文件。运行时数据区负责管理 JVM 内存。执行引擎负责执行字节码。垃圾回收器负责自动回收不再使用的对象。3. JVM 运行时数据区JVM 内存模型通常分为以下区域线程私有 - 程序计数器 - Java 虚拟机栈 - 本地方法栈 线程共享 - 堆 - 方法区 - 运行时常量池 - 直接内存4. 程序计数器程序计数器是一块很小的内存区域用来记录当前线程正在执行的字节码指令地址。因为 Java 是多线程语言线程切换后需要知道自己上次执行到哪里程序计数器就是用来保存这个位置的。特点线程私有。生命周期与线程一致。是 JVM 规范中唯一不会出现OutOfMemoryError的区域。执行 Java 方法时记录字节码地址。执行 Native 方法时通常为空。5. Java 虚拟机栈Java 虚拟机栈也是线程私有的。每个线程启动时都会创建自己的虚拟机栈。每调用一个 Java 方法JVM 都会创建一个栈帧。方法执行结束后对应栈帧出栈。栈帧中主要包含局部变量表。操作数栈。动态链接。方法返回地址。示例代码publicintadd(inta,intb){intcab;returnc;}执行这个方法时a、b、c会放在局部变量表中a b的计算过程会用到操作数栈。常见异常StackOverflowError栈深度过大例如无限递归。OutOfMemoryError线程过多或栈空间无法继续分配。典型无限递归publicclassStackOverflowDemo{publicstaticvoidmain(String[]args){call();}staticvoidcall(){call();}}6. 本地方法栈本地方法栈服务于 Native 方法也就是使用native关键字声明的方法。例如publicnativeinthashCode();Java 有些底层能力需要调用操作系统或 C/C 库这时就会通过 JNI 进入本地方法调用。7. 堆堆是 JVM 中最大的一块内存也是垃圾回收器重点管理的区域。几乎所有对象实例和数组都会分配在堆上UserusernewUser();int[]arraynewint[10];其中user这个引用变量通常在栈帧的局部变量表中真正的User对象在堆中。堆的特点线程共享。存放对象实例和数组。是 GC 的主要管理区域。容易出现OutOfMemoryError: Java heap space。常见参数-Xms512m-Xmx512m含义-Xms堆初始大小。-Xmx堆最大大小。线上服务通常建议把-Xms和-Xmx设置成相同值减少运行过程中堆扩容带来的抖动。8. 方法区与元空间方法区用于存储类相关信息例如类名。字段信息。方法信息。常量。静态变量。JIT 编译后的代码缓存相关信息。在 HotSpot JVM 中Java 8 之前常说永久代Java 8 之后改为元空间。重要区别永久代使用 JVM 堆内或受 JVM 管理的固定区域。元空间使用本地内存。元空间过大可能导致本地内存压力。常见参数-XX:MetaspaceSize128m-XX:MaxMetaspaceSize512m如果应用频繁动态生成类例如大量使用 CGLIB、ByteBuddy、Groovy 脚本、动态代理就需要关注元空间增长。9. 运行时常量池运行时常量池是方法区的一部分用于存放编译期生成的各种字面量和符号引用。例如Stringshello;字符串字面量hello会进入字符串常量池。经典问题Stringahello;Stringbhello;System.out.println(ab);// true因为a和b指向同一个字符串常量池对象。但下面不一样StringanewString(hello);Stringbhello;System.out.println(ab);// falseSystem.out.println(a.equals(b));// truenew String(hello)会在堆上创建新对象所以引用地址不同。10. 直接内存直接内存不是 JVM 运行时数据区的一部分但经常和 JVM 性能问题一起出现。NIO 中的DirectByteBuffer会使用直接内存ByteBufferbufferByteBuffer.allocateDirect(1024*1024);直接内存的优点是减少 Java 堆和 Native 内存之间的数据复制常用于高性能 I/O。常见风险直接内存不受-Xmx限制。使用不当会出现OutOfMemoryError: Direct buffer memory。Netty、Kafka、RocketMQ 等高性能中间件都可能大量使用直接内存。常见参数-XX:MaxDirectMemorySize512m11. 对象是如何创建的当代码执行UserusernewUser();JVM 大致会经历以下步骤检查类是否已经加载。为对象分配内存。将对象内存初始化为零值。设置对象头。执行构造方法。把对象引用赋值给变量。对象在内存中通常包含对象头。实例数据。对齐填充。对象头中会保存锁状态、哈希码、GC 年龄、类型指针等信息。12. 类加载机制类加载过程分为五个阶段加载 - 验证 - 准备 - 解析 - 初始化其中验证、准备、解析统称为链接。加载加载阶段会完成三件事通过类的全限定名获取二进制字节流。将字节流转换为方法区中的运行时数据结构。在堆中生成一个Class对象作为访问入口。验证验证阶段确保.class文件符合 JVM 规范避免非法字节码破坏 JVM 安全。准备准备阶段为类变量分配内存并设置默认值。例如publicstaticintcount10;准备阶段时count的值是0不是10。真正赋值为10发生在初始化阶段。解析解析阶段会把符号引用转换为直接引用。例如方法调用、字段访问、类引用都会在这个阶段或运行过程中被解析。初始化初始化阶段执行类构造器clinit()也就是静态变量赋值和静态代码块。publicclassDemo{staticintcount10;static{count20;}}最终count的值是20。13. 双亲委派模型JVM 类加载器通常遵循双亲委派模型。常见类加载器Bootstrap ClassLoader加载 Java 核心类库。Platform ClassLoader加载平台相关类库。Application ClassLoader加载应用 classpath 下的类。Custom ClassLoader用户自定义类加载器。加载逻辑子加载器收到类加载请求 - 先委托父加载器加载 - 父加载器无法加载 - 子加载器再尝试加载双亲委派的好处避免核心类被篡改。避免同一个类被重复加载。保证 Java 类型体系稳定。例如我们自己定义一个java.lang.String通常不会替代 JDK 自带的String因为核心类会优先由 Bootstrap ClassLoader 加载。14. 执行引擎JVM 加载字节码后需要执行它。执行引擎主要有两种执行方式解释执行。JIT 编译执行。解释执行是一行一行解释字节码启动快但长期运行性能一般。JIT 编译会把热点代码编译成本地机器码执行速度更快。HotSpot JVM 名字里的 HotSpot就是“热点代码”的意思。15. JIT 编译与热点代码JVM 会统计方法调用次数和循环执行次数。当某段代码执行频率很高时就会被识别为热点代码。热点代码可能会被 JIT 编译成本地机器码。常见优化包括方法内联。逃逸分析。锁消除。标量替换。公共子表达式消除。循环优化。示例publicintcompute(inta,intb){returnadd(a,b);}privateintadd(inta,intb){returnab;}JIT 可能会把add()方法内联进compute()减少方法调用开销。16. 逃逸分析逃逸分析用于判断对象是否会逃出当前方法或线程。示例publicintsum(){PointpnewPoint(1,2);returnp.xp.y;}如果 JVM 判断p不会逃出sum()方法就可能不在堆上真实分配这个对象而是进行标量替换。这可以减少对象创建和 GC 压力。17. 垃圾回收基础Java 不需要开发者手动释放对象JVM 会通过 GC 自动回收不再使用的对象。判断对象是否可回收主流 JVM 使用可达性分析。GC Roots 可以理解为一组起点从这些起点能访问到的对象就是存活对象访问不到的对象就是可回收对象。常见 GC Roots虚拟机栈中的引用。方法区中的静态变量引用。方法区中的常量引用。Native 方法栈中的引用。正在运行线程相关对象。18. 分代收集思想大多数 Java 应用符合两个经验规律大部分对象朝生夕死。少部分对象会存活很久。因此 JVM 堆通常会按代管理Young Generation 新生代 Old Generation 老年代新对象通常先进入新生代。经过多次 GC 后仍然存活的对象会晋升到老年代。新生代又常被分为Eden Survivor From Survivor To对象大致流转新对象 - Eden Minor GC 后仍存活 - Survivor 多次存活 - Old19. 常见垃圾回收器不同 JDK 版本和不同运行场景下常见 GC 有Serial GC单线程适合小内存或客户端场景。Parallel GC吞吐量优先适合批处理。G1 GC面向服务端低停顿场景是常见默认选择。ZGC低延迟 GC适合大堆和低停顿要求。Shenandoah GC低停顿 GC关注并发回收。G1 的特点把堆划分成多个 Region。优先回收收益最高的 Region。可通过参数设置期望停顿时间。适合多数在线服务。常见参数-XX:UseG1GC-XX:MaxGCPauseMillis200ZGC 更关注低延迟常用于大内存、低停顿场景-XX:UseZGCGC 选择没有绝对答案需要结合应用类型、堆大小、延迟目标和实际监控数据判断。20. Stop The WorldStop The World简称 STW表示 JVM 暂停所有用户线程专门执行 GC 或其他 JVM 内部任务。STW 时间过长会导致接口响应变慢。定时任务延迟。心跳超时。服务被误判不可用。所以 GC 调优的核心目标通常不是“完全不 GC”而是减少 Full GC。控制单次停顿时间。降低 GC 对业务延迟的影响。避免内存泄漏和对象分配异常。21. JVM 内存溢出常见类型Java heap space堆空间不足java.lang.OutOfMemoryError: Java heap space常见原因一次性加载过多数据。缓存无限增长。集合只增不删。大对象频繁创建。内存泄漏。Metaspace元空间不足java.lang.OutOfMemoryError: Metaspace常见原因动态生成类过多。类加载器泄漏。热部署反复加载类。脚本引擎或代理类无限生成。Direct buffer memory直接内存不足java.lang.OutOfMemoryError: Direct buffer memory常见原因NIO 直接缓冲区过多。Netty 直接内存配置不合理。堆外内存未及时释放。unable to create native thread无法创建本地线程java.lang.OutOfMemoryError: unable to create native thread常见原因线程池无界。线程数量过多。操作系统线程资源不足。单个线程栈设置过大。22. Java 内存模型 JMMJMM全称 Java Memory Model即 Java 内存模型。它解决的是多线程环境下变量如何在内存中读写、如何保证可见性、原子性和有序性的问题。三个核心概念原子性一个操作不可被中断。可见性一个线程修改的值其他线程能及时看到。有序性代码执行顺序在并发下仍符合规则约束。常见关键字volatile保证可见性和一定程度的有序性不保证复合操作原子性。synchronized保证原子性、可见性和有序性。final保证对象构造完成后 final 字段的安全发布语义。示例privatevolatilebooleanrunningtrue;publicvoidstop(){runningfalse;}publicvoidwork(){while(running){// do something}}如果没有volatile工作线程可能无法及时感知running的变化。但下面这样仍然不是线程安全的privatevolatileintcount0;publicvoidincr(){count;}因为count不是一个原子操作它包含读取、加一、写回三个步骤。23. synchronized 与锁synchronized可以修饰实例方法、静态方法和代码块。实例方法锁的是当前对象publicsynchronizedvoidupdate(){// lock this}静态方法锁的是 Class 对象publicstaticsynchronizedvoidupdateGlobal(){// lock Demo.class}代码块可以指定锁对象publicvoidupdate(){synchronized(this){// critical section}}JVM 会通过对象头和 Monitor 机制实现锁。现代 JVM 会对锁进行多种优化例如轻量级锁、锁消除、锁粗化等。24. JVM 常用诊断工具jps查看 Java 进程jps-ljinfo查看 JVM 参数jinfopidjstat查看 GC 统计jstat-gcutilpid1000表示每 1 秒输出一次 GC 使用情况。jmap导出堆转储jmap-dump:formatb,fileheap.hprofpid也可以查看堆对象统计jmap-histo:livepidjstack查看线程堆栈jstackpidthread.txt常用于分析死锁。线程阻塞。CPU 飙高。线程池耗尽。jcmdjcmd是比较推荐的综合诊断工具jcmdpidVM.flags jcmdpidGC.heap_info jcmdpidThread.print jcmdpidGC.class_histogramJFRJFR即 Java Flight Recorder是 JDK 自带的低开销诊断工具。启动一次 60 秒采样jcmdpidJFR.startnameprofileduration60sfilenameprofile.jfr它可以分析CPU 热点。锁竞争。GC 活动。对象分配。I/O 延迟。线程状态。25. 常见 JVM 参数服务端常见参数示例-Xms2g-Xmx2g-XX:UseG1GC-XX:MaxGCPauseMillis200-XX:MetaspaceSize256m-XX:MaxMetaspaceSize512m-XX:MaxDirectMemorySize512m -Xlog:gc*:filegc.log:time,uptime,level,tags含义-Xms2g初始堆大小 2GB。-Xmx2g最大堆大小 2GB。-XX:UseG1GC使用 G1 垃圾回收器。-XX:MaxGCPauseMillis200期望最大 GC 停顿时间为 200ms。-XX:MetaspaceSize256m元空间初始触发阈值。-XX:MaxMetaspaceSize512m元空间最大值。-XX:MaxDirectMemorySize512m直接内存最大值。-Xlog:gc*输出 GC 日志。Java 8 的 GC 日志参数和 Java 9 之后不同。Java 9 之后推荐使用统一日志-Xlog:gc*:filegc.log:time,uptime,level,tagsJava 8 常见写法-XX:PrintGCDetails-XX:PrintGCDateStamps-Xloggc:gc.log26. JVM 调优的正确思路JVM 调优不是背参数也不是看到慢就加内存。正确流程应该是明确问题现象。收集监控和日志。判断瓶颈类型。提出假设。小范围调整。对比调整前后的数据。固化配置和告警。常见误区只调-Xmx不看对象分配。只看平均响应时间不看 P95、P99。只看堆内存不看直接内存和线程数。只看 GC 次数不看 GC 停顿时间。没有压测和监控就直接改生产参数。27. CPU 飙高如何排查常见步骤top找到 Java 进程 PID。再查看线程top-Hppid找到 CPU 高的线程 ID并转成十六进制printf%x\ntid导出线程栈jstackpidthread.txt在thread.txt中搜索十六进制线程 ID。常见原因死循环。正则表达式回溯。JSON 序列化大对象。大量 GC。锁竞争。线程池过载。28. Full GC 频繁如何排查Full GC 频繁通常说明老年代、元空间或系统资源存在压力。排查方向是否存在内存泄漏。是否有大对象直接进入老年代。是否缓存无上限。是否一次查询过多数据。是否对象晋升过快。是否元空间持续增长。是否显式调用了System.gc()。建议动作打开 GC 日志。导出堆快照。使用 MAT、JProfiler、VisualVM 等工具分析对象引用链。重点查看大对象、集合、缓存、ClassLoader。对比不同时间点的 heap dump。29. 线程问题如何排查线程问题常见表现接口卡住。请求堆积。CPU 飙高。死锁。线程池耗尽。数据库连接池耗尽。排查方式jstackpidthread.txt重点关注线程状态RUNNABLE正在运行或等待 CPU。BLOCKED等待进入 synchronized 锁。WAITING无限期等待。TIMED_WAITING限时等待。如果大量线程卡在数据库调用问题可能在数据库或连接池。如果大量线程卡在远程 HTTP 调用问题可能在下游服务、网络或超时时间配置。如果大量线程BLOCKED在同一把锁上说明存在锁竞争。30. 编写 JVM 友好代码的建议控制对象创建避免在高频路径中创建大量临时对象。// 不推荐循环中频繁创建无意义对象for(inti0;isize;i){StringresultnewString(value);}集合设置合理初始容量MapString,StringmapnewHashMap(1024);如果明确知道集合规模设置初始容量可以减少扩容成本。避免无界缓存privatefinalMapString,ObjectcachenewConcurrentHashMap();如果这个缓存只增不删就可能造成内存泄漏。更推荐使用支持容量、过期时间和统计指标的缓存组件。避免一次性加载大数据不推荐ListOrderordersorderMapper.selectAll();推荐分页、游标或流式处理。合理配置线程池不推荐Executors.newCachedThreadPool();生产环境更推荐显式配置ThreadPoolExecutorexecutornewThreadPoolExecutor(8,16,60,TimeUnit.SECONDS,newArrayBlockingQueue(1000),newThreadPoolExecutor.CallerRunsPolicy());31. 面试中常问的 JVM 问题JVM 内存区域有哪些线程私有程序计数器。Java 虚拟机栈。本地方法栈。线程共享堆。方法区。运行时常量池。直接内存。堆和栈有什么区别栈保存方法调用相关数据例如局部变量和栈帧。堆保存对象实例和数组是 GC 管理的主要区域。类加载过程是什么类加载过程是加载 - 验证 - 准备 - 解析 - 初始化什么是双亲委派类加载器收到加载请求时先委托父加载器加载父加载器加载不了子加载器才自己加载。它的作用是保护核心类库避免类重复加载保证类型体系稳定。Minor GC、Major GC、Full GC 有什么区别Minor GC通常发生在新生代。Major GC通常指老年代 GC不同语境下含义可能不同。Full GC通常会回收整个堆和方法区相关数据停顿影响更大。如何判断对象可以被回收主流 JVM 使用可达性分析。从 GC Roots 出发无法访问到的对象就可以被回收。volatile 和 synchronized 的区别volatile主要保证可见性和有序性不保证复合操作原子性。synchronized可以保证原子性、可见性和有序性但会涉及锁竞争成本。32. 总结JVM 是 Java 体系中最核心的运行基础。理解 JVM不能只停留在“会背内存结构”和“会背 GC 算法”更重要的是知道它如何影响真实系统。需要重点掌握JVM 如何加载 class 文件。Java 对象如何分配和回收。堆、栈、方法区、直接内存分别解决什么问题。JIT 为什么能提升 Java 性能。GC 为什么会导致停顿。JMM 如何影响多线程正确性。如何通过工具定位 CPU、内存、GC、线程问题。对业务开发来说JVM 知识的价值不是为了炫技而是为了在系统变慢、内存上涨、线程卡死、GC 频繁时能够从现象定位到证据再从证据找到真正原因。
返回列表