吃透JVM运行时数据区:内存模型到OOM排查实战
聊到Java性能最绕不开的一张图就是JVM运行时数据区。无论是线上OOM排查、面试八股还是日常参数调优只要你写Java早晚得把它吃透。网上讲这个的文章很多多数是照着规范念一遍堆啊栈啊方法区啊背完就忘。我这边结合自己这些年排查线上问题的实操经验重新把这些区域掰开揉碎讲一遍顺便告诉你怎么用工具验证、遇到异常怎么定位。这套内容既能当面试复习提纲也能当排障手册用。先说明白一点运行时数据区不是单纯的概念背记它直接决定了你写的对象放哪、方法调用怎么走、报错信息说的是哪块内存。搞懂它你才能看懂GC日志才知道OOM之后该调哪个参数。1. 运行时数据区总览JVM到底在管哪些内存1.1 从一份Java代码到内存分配的旅程Java代码跑起来之后经历了什么编译成class文件、类加载器加载、字节码校验、解释执行最终真正落脚的地方就是运行时数据区。可以把它想象成一座工厂的生产线你写的每个对象、每次方法调用、每个类元数据都会被送到不同的车间去处理。一座工厂要运作必须有原材料仓库、加工工位、成品区还有每个工人自己的工具台。JVM的运行时数据区也是这个逻辑程序计数器相当于工人手中的工单记录当前做到哪一步。Java虚拟机栈相当于工人的工具台方法调用时在这里压栈、出栈。本地方法栈相当于给外聘专家准备的工位服务native方法。Java堆相当于成品和半成品的仓库所有对象实例都放这里。方法区相当于工厂的档案馆存类的结构信息和常量。这个类比虽然粗但是够直观。后面每一个区域我都会展开讲包括它存什么、什么时候创建、什么时候销毁、报错长什么样。1.2 六大区域的分类逻辑线程私有与线程共享划分运行时数据区第一刀应该从“线程”切。JVM规范里明确区分了两类区域区域线程关系生命周期主要存储内容典型异常程序计数器线程私有与线程同生共死当前线程执行字节码行号无Java虚拟机栈线程私有与线程同生共死栈帧局部变量表、操作数栈等StackOverflowError、OOM本地方法栈线程私有与线程同生共死native方法调用栈同上Java堆线程共享JVM启动时创建关机时销毁对象实例、数组OOM方法区线程共享JVM启动时创建类型信息、常量池、静态变量OOM运行时常量池线程共享属于方法区一部分编译期生成的字面量与符号引用OOM理解这个分类的关键在于线程私有的区域天然不需要垃圾回收方法结束、线程结束栈帧和计数器自然销毁。线程共享的区域是GC的主战场对象没人引用就得被回收类卸载也是这个逻辑。很多人会把“栈存引用、堆存对象”挂在嘴边但这是简化说法。真正执行一个方法时局部变量表里存的是基本类型值或者对象引用对象本身在堆里如果对象是逃逸分析出来的标量替换那可能连堆都没去直接分配在栈上这个后面再提。从JDK 8开始方法区的落地实现从永久代换成了元空间这块改动影响了很多人的排查习惯。后面我会单独讲为什么换、换了之后怎么调参数现在先记住一句话方法区的逻辑划分没变但物理存储位置从JVM堆内挪到了本地内存。2. 线程私有区域每个线程自己的“私房钱”2.1 程序计数器唯一不会OOM的区域程序计数器Program Counter Register也叫PC寄存器是运行时数据区里最不起眼、但绝对不能缺的一块。它是当前线程正在执行的字节码指令的行号指示器。线程执行Java方法时PC记录正在执行的虚拟机字节码地址执行native方法时PC的值是undefined。为什么要有一个计数器因为线程切换之后得知道上次执行到哪了。JVM的多线程是通过轮流切换、分配处理器执行时间片实现的一个线程被换下去再换上来必须能回到原来的执行位置PC就是干这个的。它是唯一一个在Java虚拟机规范里没有规定任何OutOfMemoryError情况的区域。原因很简单它的空间需求是固定且极小的只是记录一个行号不可能被撑爆。实际排查中我也从没见过PC导致的异常这地方属于“知道有它就行”的知识点。有个细节值得注意PC是线程私有的每个线程独立一份。写并发代码时你别指望用它做共享状态它跟逻辑业务没有任何关系纯粹是JVM底层执行引擎的辅助数据。2.2 Java虚拟机栈从栈帧到StackOverflowError虚拟机栈描述的是Java方法执行时的内存模型。每个方法从调用到执行完毕对应一次入栈和出栈。栈里装的是栈帧Stack Frame一个方法对应一个栈帧。一个栈帧里至少包含这样几块内容局部变量表以Slot为单位的变量空间存放方法参数和方法内部定义的局部变量。64位长度的long和double占两个Slot其余类型占一个Slot。Slot是可复用的方法体中局部变量作用域结束后这块位置可以被后续变量覆盖所以局部变量表大小并不等于变量数量之和。操作数栈一个后进先出的栈结构字节码指令在这里取数、计算、存结果。执行a b这样的操作就是把a和b压入操作数栈执行加法指令再把结果压回来。动态链接指向运行时常量池中该方法的引用。因为字节码里调方法用的是符号引用真正找到目标方法的内存入口是在运行期动态完成的这就叫动态链接。方法出口记录方法正常返回或异常返回时要恢复的调用者状态包括返回到哪个PC地址。我举个例子串一遍int result add(1, 2);这个方法调用发生时JVM为add方法创建一个栈帧压入当前线程的虚拟机栈。局部变量表里有形参a1、b2操作数栈里把1和2压进去执行iadd指令弹出34把结果7存回局部变量表方法结束栈帧出栈回到调用方的PC继续执行。虚拟机栈有两个异常点线程请求的栈深度大于虚拟机所允许的深度抛出StackOverflowError典型场景是无限递归。如果虚拟机动