第四篇:JVM——Java程序跑在了一台“虚拟电脑”上
别再背内存分区的大小了你只需要知道“你的对象在内存里是怎么活着、怎么死的”写在前面很多人觉得JVM是Java面试里最“玄学”的一块什么堆栈方法区、垃圾回收算法、类加载机制……听起来像天书。别慌。JVM的本质就是一台“虚拟的电脑”。类加载器 把.class文件你的代码装进这台电脑的“硬盘”。内存区域运行时数据区 这台电脑的“内存条”。执行引擎 这台电脑的“CPU”负责执行字节码指令。垃圾回收器GC 这台电脑的“自动清洁工”定时把内存里没人用的垃圾扫走。你把JVM想象成一台装了特殊操作系统Java字节码的虚拟电脑所有的问题就迎刃而解了。一、JVM内存区域你的对象住在哪个房间面试官最爱问“JVM的内存结构是怎样的” 别慌我们分区域讲每个区域负责什么一目了然。1.1 堆Heap—— 最大的“公共仓库”这是JVM里最大的一块内存所有线程共享。你new出来的所有对象都住在这里。垃圾回收GC的主要战场清洁工GC没事就在这个仓库里转悠把没人要的对象扔出去。分代设计绝大多数GC器都采用年轻代Young Generation新对象刚出生时放这儿。这里又分Eden区和两个Survivor区S0和S1。对象在这里“熬过”几次垃圾回收后就会被挪到老年代。老年代Old Generation / Tenured存放生命周期长的“老油条”对象。注G1等新一代垃圾回收器逻辑上不再强制物理分代而是划分为多个Region但分代概念依然存在。面试常问堆大小怎么设置-Xms初始堆大小比如-Xms2g。-Xmx最大堆大小比如-Xmx4g。最好把-Xms和-Xmx设成一样大这样可以避免JVM在运行时频繁向操作系统申请扩容/缩容减少性能抖动。1.2 栈Stack—— 每个线程的“工作台”每个线程都有自己的栈互不干扰。它存的是你方法调用的“现场”。每调用一个方法就在栈上压入一个栈帧Stack Frame。栈帧里存了局部变量表方法里的变量、操作数栈做计算的地方、方法返回地址。方法执行完栈帧弹出销毁。面试常问栈里存对象吗不存栈里只存基本数据类型int、boolean等和对象的引用reference相当于指向堆里对象的“遥控器”对象本体永远在堆里。1.3 方法区Method Area/ 元空间Metaspace—— 放“图纸”的地方这是所有线程共享的。它存的是“类”的信息而不是“对象”的信息。类的全限定名、方法字节码、字段信息、常量池、静态变量static。JDK 1.8的重大变化永久代PermGen被移除改成了元空间Metaspace。为什么改因为PermGen大小很难设且容易OOM。Metaspace不再在JVM内存里而是直接用本地内存Native Memory最大可用空间受限于操作系统内存不容易炸。1.4 程序计数器PC Register—— 记“小账本”每个线程独有。它非常小就记录着一件事当前线程执行到哪一行字节码了。线程切换CPU时间片轮转时需要它来保存和恢复执行位置不然CPU切回来不知道从哪开始。1.5 本地方法栈Native Method Stack和栈功能一样只不过它是用来执行**native修饰的方法**比如Java调用底层的C/C代码如Object.hashCode()的底层实现。二、类加载机制你的代码是怎么被装进虚拟电脑的面试官问“类加载过程是怎样的” 你千万别只背“加载、验证、准备、解析、初始化”这五个词要说出为什么这么设计。2.1 类加载的生命周期加载Loading把.class文件二进制字节流找到读进内存并在堆里生成一个java.lang.Class对象。验证Verification检查这个字节流合不合规是不是恶意代码防止破坏JVM安全。准备Preparation给静态变量static分配内存并设置默认初始值比如int是0引用是null。注意准备阶段不会执行代码里的赋值语句比如static int a 10此时a只是被赋值为0真正的10要到初始化阶段。解析Resolution把符号引用类里的那些“代号”转换成直接引用内存里的实际地址。初始化Initialization执行类构造器clinit()即执行静态代码块和静态变量的赋值。这时候才真正执行了你写的赋值逻辑a 10。2.2 双亲委派模型绝对核心这是Java类加载最巧妙的设计。三个默认加载器启动类加载器Bootstrap ClassLoader加载JAVA_HOME/lib下最核心的类比如rt.jar里的java.lang.*。扩展类加载器Extension ClassLoader加载JAVA_HOME/lib/ext下的类。应用类加载器Application ClassLoader加载你的Classpath你写的代码。什么叫双亲委派当一个类加载器收到加载请求它不会自己去加载而是把请求抛给它的父加载器。父加载器加载不了才甩给子加载器自己加载。面试追问“为什么要这么设计”这才是考点核心目的防止核心类库被篡改。假设你自己写了一个类叫java.lang.String包名和类名跟JDK一样。如果没有双亲委派你的String类就会被加载进JVM。有了双亲委派向上交给启动类加载器启动类加载器一看是java.lang.String直接就从JDK的rt.jar里把正宗String拿出来了。你的山寨String永远没机会加载JVM核心安全得到了保障。三、垃圾回收GC—— 清洁工是怎么打扫卫生的GC是面试绝对的重头戏。面试官想知道内存满了谁被清理怎么清理3.1 怎么判断对象是垃圾引用计数 vs 可达性分析① 引用计数法已淘汰每个对象记着被引用了多少次。引用归零就回收。弊端无法解决循环引用A引用BB引用A但外部没人用它们它们互相死抱着对方内存泄漏。② 可达性分析JVM用的从“GC Roots”作为起点往下找引用链能找得到的对象就是“活的”找不到的对象就是“垃圾”可回收。GC Roots包括什么虚拟机栈栈帧局部变量表中引用的对象。静态属性static引用的对象。常量池引用的对象。Native方法引用的对象。注意互相引用的A和B如果没有被GC Roots引用到依然会被判定为垃圾。3.2 垃圾回收三大算法理解原理即可① 标记-清除Mark-Sweep先标记出所有垃圾然后清除。缺点清除后内存变得碎片化东一块西一块的空洞大对象来了找不到连续空间。② 复制算法Copying把内存分成两块只往一块里放对象满了之后把活着的对象复制到另一块前一块整块清空。缺点内存只能利用一半。应用年轻代的Eden S0/S1就是复制算法。③ 标记-整理Mark-Compact先标记垃圾但清除时不是直接扔而是把所有存活对象往内存的一头移动压缩让内存连续。应用老年代用这个算法因为老年代对象存活率高用复制算法太亏。3.3 分代收集理论为什么分年轻代和老年代一句话概括“朝生夕死的放一起老不死的放一起分治处理。”年轻代对象死的快用复制算法效率高只有少量存活对象要复制。老年代对象存活率高用标记-清除/标记-整理清理频率低但每次清理成本高。3.4 常见垃圾回收器面试常问但不用背细节记住主流就行Serial / ParNew早期的单线程/多线程会Stop The WorldSTW停下所有用户线程。现在极少用。CMSConcurrent Mark Sweep追求低延迟垃圾回收时用户线程可以和GC线程并发执行。但有浮动垃圾清理时又产生了新垃圾、产生内存碎片。JDK 9后已标记废弃不要再用了。G1Garbage First目前的主流JDK 9默认。不再物理分代而是把堆分成多个Region区域。可以设定预期停顿时间比如100msG1会计算每个Region的垃圾回收收益优先回收垃圾最多的RegionGarbage First名字来源。ZGCJDK 11号称实现亚毫秒级停顿10ms不管是几G还是几十G的堆。算法极其复杂了解即可。四、JVM调优和OOM排查面试官真正关心你的实战能力面试官不会只问你理论一定会问“你们线上出过OOM吗怎么排查的”4.1 常见的OOM场景堆内存溢出java.lang.OutOfMemoryError: Java heap space对象太多撑爆了。比如大List循环往里塞数据。元空间溢出java.lang.OutOfMemoryError: Metaspace动态生成太多类比如频繁使用CGLib代理。栈内存溢出StackOverflowError递归调用太深方法栈帧把栈撑爆了。4.2 排查步骤面试必备话术启动时加上参数让JVM在OOM时自动dump快照-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/dump.hprof应用挂了之后拿到dump.hprof文件。用工具分析MATMemory Analyzer Tool、JVisualVM、Eclipse Memory Analyzer。重点看哪个对象占用了最多的内存Dominator TreeGC Roots指向了谁到底是哪个线程、哪行代码一直在申请内存。找到原因后修复要么改代码释放引用要么增大-Xmx内存要么优化数据结构。五、面试官真正想听什么场景1“讲一下JVM内存模型。”普通回答及格背出堆、栈、方法区、程序计数器。面试官想听的加分“除了堆和方法区是线程共享栈和程序计数器是线程私有外我更关注哪些区域容易发生OOM。堆存对象最容易爆栈的线程数太多会爆OutOfMemoryError: unable to create new native thread元空间如果大量使用动态代理也要留意。在实际项目中我最关心堆的使用情况因为GC主要发生在这里。”场景2“你遇到过GC频繁的问题吗”死记硬背版“遇到过可能是代码有问题。”加分版真实感“我们系统遇到过Full GC频繁导致接口RT飙高。我先用jstat -gcutil pid 1000看GC频率发现老年代占用持续上升且无法回收。然后dump了堆内存用MAT分析发现是某个缓存类被误存了百万级对象且没有设置过期时间导致老年代瞬间填满。后来我们改成用Guava Cache设置最大容量和TTLFull GC频率降为0。”小结核心概念大白话面试考点堆Heap对象本体的大仓库GC主战场-Xms和-Xmx设置栈Stack方法调用的工作台存引用和基本类型StackOverflowError元空间Metaspace放类图纸用本地内存静态变量存这里双亲委派找老爸先加载防止核心类被篡改可达性分析从GC Roots找引用链判断垃圾的方式G1垃圾回收器分区回收优先清垃圾多的主流GC器设定停顿时间OOM排查加参数dumpMAT分析真正的实战能力记住一句话JVM面试不考记忆力考的是你处理过多少内存问题。哪怕没线上实战过能流畅地说出“如何加启动参数dump堆栈、如何用MAT分析最大的对象”面试官也会觉得你是有经验的人。下一篇我们讲MySQL——数据库到底是怎么存数据的为什么索引偏偏是B树InnoDB和MyISAM到底怎么选下期见。