运行时数据区:线程私有的三块与共享的两块
程序计数器、虚拟机栈、本地方法栈,随线程生灭。程序计数器记录字节码行号,是唯一不抛OOM的区域。虚拟机栈存栈帧,每个方法调用压入一个,局部变量表、操作数栈、动态链接、返回地址都在里面。栈深度超限抛StackOverflowError,线程过多则可能OOM。本地方法栈服务native方法。线程私有的内存,是并发的安全区,也是栈溢出的高发地。
堆和方法区(JDK8后为元空间)是线程共享的。堆存对象实例和数组,是GC主战场。方法区存类元信息、常量、静态变量,JDK8后用本地内存实现元空间,默认不设上限,但受物理内存约束。堆是对象的天堂,也是GC的战场;元空间是类的档案室,只增不减需要警惕。直接内存通过NIO的ByteBuffer.allocateDirect分配,不受堆大小限制,但受物理内存和MaxDirectMemorySize约束,溢出时同样抛OOM。
对象的一生:从Eden到老年代
新对象优先在Eden分配。Eden满,触发Minor GC,存活对象复制到Survivor区,年龄加一。两个Survivor区交替使用,对象在多次Minor GC后年龄达到阈值(默认15),晋升老年代。大对象直接进老年代,避免在Survivor间反复复制。对象的一生,是一场从Eden到老年代的迁徙;每一次Minor GC,都是幸存者的筛选。动态年龄判定:Survivor中相同年龄对象总和超过一半,该年龄及以上直接晋升。空间分配担保:Minor GC前检查老年代最大可用连续空间,不够则触发Full GC。
GC算法与收集器:没有银弹
标记-清除产生碎片,标记-复制浪费空间,标记-整理消除碎片但移动对象。分代收集把堆分成新生代和老年代,各用各的算法。GC调优的第一原则:没有最好的收集器,只有最合适的权衡。Serial适合单核客户端,ParNew配合CMS曾是低延迟标配,Parallel Scavenge追求吞吐量,CMS以低停顿著称但有碎片和并发模式失败,G1把堆分成Region,可预测停顿模型,ZGC和Shenandoah追求超低停顿。选择收集器,本质是在吞吐量、停顿时间、内存占用之间做三角权衡。
调优实战:参数、工具与心法
常用参数:-Xms/-Xmx设堆初始与最大,-Xmn设新生代,-XX:SurvivorRatio设Eden与Survivor比例,-XX:MaxTenuringThreshold设晋升年龄,-XX:+UseG1GC启用G1,-XX:MaxGCPauseMillis设目标停顿。工具:jps看进程,jstat看GC频率与耗时,jmap导堆转储,jstack看线程栈,VisualVM和GC日志分析是进阶。调优不是背参数,是读日志、看监控、做假设、再验证的循环。遇到Full GC频繁,先看是否内存泄漏,再看晋升阈值和Survivor比例。遇到停顿过长,换收集器或调Region大小。JVM调优的终点,是让应用在有限内存里,跑出稳定而可预测的延迟。面试官想听的,正是你从日志里读出故事的能力。