JVM 对象创建及内存分配

发布时间:2026/11/2 6:35:51
JVM 对象创建及内存分配 一、JDK 体系结构与跨平台特性Java 的跨平台能力来自「一次编写到处运行」的设计源码经javac编译为与平台无关的字节码.class再由各平台对应的 JVM 负责解释执行或即时编译JIT为本地机器码。因此跨平台的是字节码而 JVM 本身是平台相关的。JDK 体系结构可以按层次理解为JDKJava 开发工具包包含 JRE 以及javac、javap、jps、jstat、jmap、jstack等工具。JREJava 运行时环境包含 JVM 与核心类库。JVM负责加载字节码、管理内存、执行垃圾回收与即时编译。二、JVM 整体结构与内存模型JVM 运行时数据区按线程是否共享可分为两类区域是否线程共享主要作用常见异常程序计数器线程私有记录当前线程执行到的字节码行号无虚拟机栈线程私有存放栈帧局部变量表、操作数栈、动态链接、返回地址StackOverflowError / OutOfMemoryError本地方法栈线程私有为 native 方法服务StackOverflowError / OutOfMemoryError堆线程共享存放对象实例与数组是 GC 的主要区域OutOfMemoryError: Java heap space方法区元空间线程共享存放类元信息、常量、静态变量等OutOfMemoryError: Metaspace堆内通常再划分为新生代Eden 两个 Survivor与老年代默认比例约为 1:2新生代内部 Eden 与 Survivor 默认比例为 8:1:1。对象优先在 Eden 分配经过多次 Minor GC 仍存活的对象晋升到老年代。三、JVM 内存参数设置Spring Boot 程序可直接在启动命令中追加 JVM 参数若使用 Tomcat 部署则加在bin目录下的catalina.sh中。java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize256M -XX:MaxMetaspaceSize256M -jar microservice-eureka-server.jar常用参数含义-Xms堆初始大小-Xmx堆最大大小。生产环境建议两者设为相同值避免堆频繁扩容收缩。-Xmn新生代大小等价于-XX:NewSize与-XX:MaxNewSize同时设置。-Xss单个线程栈大小默认约 1M。-XX:MetaspaceSize元空间触发 Full GC 的初始阈值默认约 21M。-XX:MaxMetaspaceSize元空间最大值默认 -1即不限制仅受本地内存约束。关于元空间的两个参数需要特别注意-XX:MetaspaceSize是触发 Full GC 进行类型卸载的初始阈值并非元空间的固定初始容量。达到该值后会触发 Full GC收集器再根据释放情况动态调整该阈值释放空间多则适当降低释放空间少则在不超过MaxMetaspaceSize的前提下适当提高。它与早期 JDK 的-XX:PermSize永久代初始容量含义不同。由于调整元空间大小需要 Full GC代价很高。如果应用启动阶段频繁发生 Full GC通常就是元空间大小在反复调整。因此一般建议把MetaspaceSize与MaxMetaspaceSize设为相同值且比初始值更大。以 8G 物理内存的机器为例通常两个值都设为 256M。StackOverflowError 示例与结论下面的代码通过无限递归演示栈溢出运行时设置-Xss128k默认约 1M// JVM 设置 -Xss128k默认 1M public class StackOverflowTest { static int count 0; static void redo() { count; redo(); } public static void main(String[] args) { try { redo(); } catch (Throwable t) { t.printStackTrace(); System.out.println(count); } } }运行结果java.lang.StackOverflowError at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12) at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13) at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13) ......结论-Xss设置越小count 值越小说明单个线程栈能容纳的栈帧越少但对整个 JVM 而言可创建的线程数会更多。因此-Xss是「单线程深度」与「并发线程数」之间的权衡。五、JVM 内存参数该如何设置JVM 参数没有固定标准需要结合项目实际情况分析。以日均百万级订单交易系统为例思路如下先估算对象生命周期大部分订单对象是「朝生夕死」的短命对象应尽量让它们在新生代完成分配与回收。适当放大新生代减少对象过早晋升到老年代从而降低 Full GC 频率。给系统留足堆内存避免新生代频繁触发 Minor GC。元空间固定大小避免启动期反复 Full GC。核心优化原则尽可能让对象在新生代分配和回收避免大量对象频繁进入老年代避免频繁对老年代做垃圾回收同时给系统充足内存避免新生代频繁 GC。六、对象的创建过程虚拟机遇到一条new指令时对象的创建大致分为五步。语言层面的new关键字、对象克隆、对象序列化最终都会落到new指令上。类加载检查先检查指令参数能否在常量池中定位到一个类的符号引用并确认该类是否已被加载、解析和初始化过若没有先执行类加载过程。分配内存类加载完成后对象所需内存大小即可完全确定分配空间等同于把一块确定大小的内存从 Java 堆中划分出来。初始化零值将分配到的内存空间不含对象头初始化为零值若使用 TLAB (线程本地分配缓冲区) 可提前到分配时进行。这保证了实例字段不赋初值也能直接使用访问到的是对应类型的零值。设置对象头设置对象是哪个类的实例、如何找到元数据信息、哈希码、GC 分代年龄等存放在对象头 Object Header 中。执行init方法按程序员意愿初始化即给属性赋真实值并执行构造方法。1. 内存划分的两种方式指针碰撞Bump the Pointer堆内存绝对规整时已用内存在一边、空闲内存在另一边中间放一个分界指针分配时只需把指针向空闲方向挪动与对象大小相等的距离。默认使用指针碰撞。空闲列表Free List堆内存不规整、已用与空闲交错时无法指针碰撞虚拟机维护一个列表记录可用内存块分配时找到足够大的块划分出去并更新列表。2. 并发分配的两种解决方案CAS 失败重试多线程修改Eden分配指针top时采用比较并交换的无锁机制保证原子性对分配内存的动作做同步处理防止对象内存覆盖保证更新指针操作的原子性。TLAB就是为了减少CAS竞争TLABThread Local Allocation Buffer本地线程分配缓冲为每个线程在堆中预先分配一小块内存各线程在自己的空间里分配。通过-XX:/-UseTLAB控制是否启用默认开启-XX:TLABSize指定大小。3. 初始化零值内存分配完成后虚拟机需要将分配到的内存空间都初始化为零值不包括对象头如果使用TLAB这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用程序能访问到这些字段的数据类型所对应的零值。4. 设置对象头初始化零值之后虚拟机要对对象进行必要的设置例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。在HotSpot虚拟机中对象在内存中存储的布局可以分为3块区域对象头Header、 实例数据Instance Data和对齐填充Padding。 HotSpot虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据 如哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针即对象指向它的类元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。32位对象头64位对象头5. 对象的内存布局与指针压缩HotSpot 中对象在内存中的布局分为三块对象头Header、实例数据Instance Data、对齐填充Padding。对象头包含两部分Mark Word存储对象自身运行时数据如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。类型指针Klass Pointer指向对象所属类元数据的指针虚拟机据此确定对象是哪个类的实例。1. 用 jol-core 查看对象大小dependency groupIdorg.openjdk.jol/groupId artifactIdjol-core/artifactId version0.9/version /dependencyimport org.openjdk.jol.info.ClassLayout; /** 计算对象大小 */ public class JOLSample { public static void main(String[] args) { ClassLayout layout ClassLayout.parseInstance(new Object()); System.out.println(layout.toPrintable()); System.out.println(); ClassLayout layout1 ClassLayout.parseInstance(new int[]{}); System.out.println(layout1.toPrintable()); System.out.println(); ClassLayout layout2 ClassLayout.parseInstance(new A()); System.out.println(layout2.toPrintable()); } // -XX:UseCompressedOops 默认开启压缩所有指针 // -XX:UseCompressedClassPointers 默认开启压缩对象头里的类型指针 Klass Pointer // Oops : Ordinary Object Pointers public static class A { // 8B mark word // 4B Klass Pointer若关闭压缩-XX:-UseCompressedClassPointers 或 -XX:-UseCompressedOops则占 8B int id; // 4B String name; // 4B若关闭压缩 -XX:-UseCompressedOops 则占 8B byte b; // 1B Object o; // 4B若关闭压缩 -XX:-UseCompressedOops 则占 8B } }运行结果java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) //mark word 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) //mark word 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) //Klass Pointer 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal 4 bytes external 4 bytes total [I object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 0 int [I.elements N/A Instance size: 16 bytes Space losses: 0 bytes internal 0 bytes external 0 bytes total com.tuling.jvm.JOLSample$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407) 12 4 int A.id 0 16 1 byte A.b 0 17 3 (alignment/padding gap) 20 4 java.lang.String A.name null 24 4 java.lang.Object A.o null 28 4 (loss due to the next object alignment) Instance size: 32 bytes Space losses: 3 bytes internal 4 bytes external 7 bytes total2. 什么是对象指针压缩从 JDK 1.6 update14 开始64 位操作系统中 JVM 支持指针压缩。参数为UseCompressedOops其中 compressed 表示压缩oop 即 ordinary object pointer对象指针。启用-XX:UseCompressedOops默认开启禁止-XX:-UseCompressedOops。3. 为什么要压缩64 位 HotSpot 若使用 32 位指针内存使用会多出约 1.5 倍较大指针在主内存与缓存之间移动数据会占用更大带宽GC 压力也更大。为减少 64 位平台下的内存消耗启用指针压缩。32 位地址最大支持 4G2 的 32 次方通过对对象指针做压缩编码与解码JVM 只用 32 位地址即可支持更大内存不超过 32G。堆内存小于 4G 时无需启用压缩JVM 会直接去掉高 32 位地址使用低虚拟地址空间。堆内存大于 32G 时压缩指针失效强制使用 64 位8 字节寻址又回到第 1 点的问题因此堆内存最好不要大于 32G。关于对齐填充对于大部分处理器对象以8字节整数倍来对齐填充都是最高效的存取方式。七、对象的内存分配策略1. 栈上分配与逃逸分析对象一般在堆上分配未被引用时依赖 GC 回收对象数量多会给 GC 带来压力。JVM 通过逃逸分析判断对象是否会被外部访问若不会逃逸可将对象分配在栈上随栈帧出栈一起销毁从而减轻 GC 压力。public User test1() { User user new User(); user.setId(1); user.setName(zhuge); // TODO 保存到数据库 return user; } public void test2() { User user new User(); user.setId(1); user.setName(zhuge); // TODO 保存到数据库 }test1中的 user 被返回作用域不确定发生逃逸test2中的 user 在方法结束后即无效未逃逸可分配在栈上。逃逸分析开关-XX:DoEscapeAnalysisJDK 7 之后默认开启关闭用-XX:-DoEscapeAnalysis。标量替换确定对象不逃逸且可进一步分解时JVM 不创建对象而是把成员变量拆成若干局部变量在栈帧或寄存器上分配空间避免因缺少大块连续内存而分配失败。开关-XX:EliminateAllocationsJDK 7 之后默认开启。标量与聚合量标量是不可再分解的量如基本数据类型与 reference聚合量是可再分解的量Java 中的对象就是聚合量。/** * 栈上分配标量替换 * 代码调用了 1 亿次 alloc()如果分配到堆上大概需要 1GB 以上堆空间堆空间小于该值必然触发 GC。 * * 使用如下参数不会发生 GC * -Xmx15m -Xms15m -XX:DoEscapeAnalysis -XX:PrintGC -XX:EliminateAllocations * 使用如下参数都会发生大量 GC * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:PrintGC -XX:EliminateAllocations * -Xmx15m -Xms15m -XX:DoEscapeAnalysis -XX:PrintGC -XX:-EliminateAllocations */ public class AllotOnStack { public static void main(String[] args) { long start System.currentTimeMillis(); for (int i 0; i lt; 100000000; i) { alloc(); } long end System.currentTimeMillis(); System.out.println(end - start); } private static void alloc() { User user new User(); user.setId(1); user.setName(zhuge); } }结论栈上分配依赖于逃逸分析和标量替换两者缺一不可。2. 对象优先在 Eden 区分配大多数对象在新生代 Eden 区分配Eden 区空间不足时触发一次 Minor GC。Minor GC / Young GC新生代的垃圾收集动作非常频繁回收速度较快。Major GC / Full GC一般回收老年代、年轻代和方法区速度通常比 Minor GC 慢 10 倍以上。Eden 与 Survivor 默认比例为 8:1:1。大量对象分配在 EdenEden 满后触发 Minor GC99% 以上对象被回收存活对象挪到为空的 Survivor 区下次 Eden 满时再把 Eden 与该 Survivor 的存活对象一次性挪到另一块空的 Survivor。新生代对象朝生夕死8:1:1 让 Eden 尽量大、Survivor 够用即可。注意-XX:UseAdaptiveSizePolicy默认开启会导致 8:1:1 比例自动变化若不想变化可设置-XX:-UseAdaptiveSizePolicy。// 添加运行 JVM 参数-XX:PrintGCDetails public class GCTest { public static void main(String[] args) throws InterruptedException { byte[] allocation1, allocation2; allocation1 new byte[60000 * 1024]; } }Heap PSYoungGen total 76288K, used 65536K [0x000000076b400000, 0x0000000770900000, 0x00000007c0000000) eden space 65536K, 100% used [0x000000076b400000,0x000000076f400000,0x000000076f400000) from space 10752K, 0% used [0x000000076fe80000,0x000000076fe80000,0x0000000770900000) to space 10752K, 0% used [0x000000076f400000,0x000000076f400000,0x000000076fe80000) ParOldGen total 175104K, used 0K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000) object space 175104K, 0% used [0x00000006c1c00000,0x00000006c1c00000,0x00000006cc700000) Metaspace used 3342K, capacity 4496K, committed 4864K, reserved 1056768K class space used 361K, capacity 388K, committed 512K, reserved 1048576K再为 allocation2 分配 8000K 时Eden 已几乎分配完触发 Minor GCGC 期间发现 allocation1 无法放入 Survivor于是提前转移到老年代老年代空间足够不会触发 Full GC[GC (Allocation Failure) [PSYoungGen: 65253K-936K(76288K)] 65253K-60944K(251392K), 0.0279083 secs] [Times: user0.13 sys0.02, real0.03 secs] Heap PSYoungGen total 76288K, used 9591K [0x000000076b400000, 0x0000000774900000, 0x00000007c0000000) eden space 65536K, 13% used [0x000000076b400000,0x000000076bc73ef8,0x000000076f400000) from space 10752K, 8% used [0x000000076f400000,0x000000076f4ea020,0x000000076fe80000) to space 10752K, 0% used [0x0000000773e80000,0x0000000773e80000,0x0000000774900000) ParOldGen total 175104K, used 60008K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000) object space 175104K, 34% used [0x00000006c1c00000,0x00000006c569a010,0x00000006cc700000)3. 大对象直接进入老年代大对象指需要大量连续内存空间的对象如字符串、数组。参数-XX:PretenureSizeThreshold可设置大对象阈值超过该大小直接进入老年代不经过年轻代该参数只在 Serial 和 ParNew 收集器下有效。-XX:PretenureSizeThreshold1000000 -XX:UseSerialGC这样做的目的是避免为大对象分配内存时发生复制操作而降低效率。4. 长期存活对象进入老年代虚拟机为每个对象设置年龄Age计数器对象在 Eden 出生并经过第一次 Minor GC 后仍存活且能被 Survivor 容纳就移入 Survivor 并将年龄设为 1此后每熬过一次 Minor GC 年龄加 1达到阈值默认 15 岁CMS 默认 6 岁晋升老年代。阈值可通过-XX:MaxTenuringThreshold设置。5. 对象动态年龄判断在存放对象的 Survivor 区中若一批对象的总大小大于该 Survivor 区内存的 50%可用-XX:TargetSurvivorRatio指定则大于等于这批对象年龄最大值的对象可直接进入老年代。例如年龄 1 年龄 2 … 年龄 n 的对象总和超过 Survivor 的 50%则年龄 n含以上的对象都放入老年代。该机制一般在 Minor GC 之后触发目的是让可能长期存活的对象尽早进入老年代。6. 老年代空间分配担保机制年轻代每次 Minor GC 之前JVM 都会计算老年代剩余可用空间若可用空间小于年轻代现有所有对象大小之和含垃圾对象检查-XX:-HandlePromotionFailure参数是否设置JDK 1.8 默认已设置。若设置了该参数再看老年代可用内存是否大于之前每次 Minor GC 后进入老年代对象的平均大小。若上一步结果为小于或该参数未设置则触发一次 Full GC对老年代和年轻代一起回收回收后仍无足够空间存放新对象则发生 OOM。若 Minor GC 后存活对象仍需挪到老年代且大于老年代可用空间同样触发 Full GCFull GC 后仍放不下则 OOM。八、对象内存回收堆中几乎存放着所有对象实例垃圾回收的第一步是判断哪些对象已经死亡不能再被任何途径使用。1. 引用计数法给对象添加引用计数器每被引用一次加 1引用失效减 1计数器为 0 即不可能再被使用。实现简单、效率高但主流虚拟机并未采用最主要原因是难以解决对象之间循环引用的问题。public class ReferenceCountingGc { Object instance null; public static void main(String[] args) { ReferenceCountingGc objA new ReferenceCountingGc(); ReferenceCountingGc objB new ReferenceCountingGc(); objA.instance objB; objB.instance objA; objA null; objB null; } }objA 与 objB 相互引用除此之外再无引用但计数器都不为 0引用计数算法无法通知 GC 回收它们。2. 可达性分析算法以「GC Roots」对象为起点向下搜索引用对象能找到的对象标记为非垃圾其余未标记对象即为垃圾。GC Roots 根节点包括线程栈的本地变量、静态变量、本地方法栈的变量等。3. 四种引用类型引用类型回收时机典型用途强引用只要引用链可达就永不回收普通变量引用软引用内存不足、GC 后仍释放不出空间时回收内存敏感的高速缓存如浏览器后退弱引用下一次 GC 直接回收很少使用如 ThreadLocal 的 key虚引用随时可能被回收仅用于回收通知几乎不用// 强引用 public static User user new User(); // 软引用 public static SoftReferenceUser user new SoftReferenceUser(new User()); // 弱引用 public static WeakReferenceUser user new WeakReferenceUser(new User());软引用的典型应用是浏览器的后退按钮若网页浏览结束即回收内容按后退需重新构建若把浏览过的网页都存到内存又会造成内存浪费甚至溢出软引用正好在两者之间取得平衡。4. finalize() 方法最终判定对象是否存活可达性分析中不可达的对象并非「非死不可」而是处于「缓刑」阶段真正宣告死亡至少要经历两次标记第一次标记并筛选判断对象是否有必要执行finalize()。未覆盖finalize方法的对象直接回收。第二次标记若覆盖了finalize方法这是对象逃脱死亡的最后机会。只要在finalize()中重新与引用链上任意对象建立关联如把自己赋值给某个类变量或对象成员变量第二次标记时就会被移出「即将回收」集合否则基本就真的被回收了。注意一个对象的finalize()方法只会被执行一次自我拯救的机会只有一次。public class OOMTest { public static void main(String[] args) { Listlt;Objectgt; list new ArrayListlt;gt;(); int i 0; int j 0; while (true) { list.add(new User(i, UUID.randomUUID().toString())); new User(j--, UUID.randomUUID().toString()); } } }5. 如何判断一个类是无用的类方法区主要回收无用的类类需要同时满足以下三个条件才算「无用的类」该类所有实例都已被回收即 Java 堆中不存在该类的任何实例。加载该类的 ClassLoader 已经被回收。该类对应的java.lang.Class对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法。九、学习小结跨平台依赖字节码与 JVM 规范JVM 实现本身与平台相关。内存结构要能区分线程私有与共享区域并对应到常见异常类型。参数设置的核心是稳定-Xms与-Xmx一致元空间两个参数一致。调优目标是让对象尽量在新生代回收减少对象晋升与 Full GC。排查问题要结合 GC 日志与 jstat、jmap、jstack 等工具用数据说话。对象创建五步类加载检查、分配内存、初始化零值、设置对象头、执行init每一步都有明确的职责边界。内存划分看堆是否规整规整用指针碰撞不规整用空闲列表并发安全靠 CAS 或 TLAB。对象布局为对象头、实例数据、对齐填充指针压缩默认开启堆内存建议不超过 32G。分配策略的核心是「让对象尽量在新生代回收」栈上分配依赖逃逸分析与标量替换大对象直接进老年代长期存活与动态年龄判断决定晋升时机。回收判定用可达性分析而非引用计数四种引用类型对应不同生命周期finalize()只有一次自救机会。十、面试常见提问1. 说说 JVM 的内存结构JVM 运行时数据区包括程序计数器、虚拟机栈、本地方法栈、堆和方法区JDK 8 起为元空间。前三个是线程私有的随线程创建和销毁堆和方法区是线程共享的。堆是 GC 主战场方法区存放类元信息、常量和静态变量。2. 为什么 Java 能跨平台因为javac编译产出的是与平台无关的字节码由各平台各自的 JVM 负责解释执行或 JIT 编译成本地机器码。跨平台的是字节码和 JVM 规范JVM 实现本身是平台相关的。3. 堆和栈有什么区别栈是线程私有的存放栈帧生命周期与线程一致分配回收自动速度快容量小溢出抛StackOverflowError堆是线程共享的存放对象实例由 GC 管理容量大溢出抛OutOfMemoryError。4.-Xms和-Xmx为什么要设置成一样如果两者不同堆会在运行期动态扩容和收缩扩容需要向操作系统申请内存收缩会触发 GC都会带来额外开销和停顿。设为相同值可以让堆大小稳定减少不必要的抖动。5. 元空间和永久代有什么区别永久代是 JDK 7 及以前 HotSpot 对方法区的实现位于堆内受-XX:MaxPermSize限制容易出现PermGen space溢出。JDK 8 起改为元空间使用本地内存默认不限制大小通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize控制字符串常量池也移到了堆中。6.-XX:MetaspaceSize和-XX:MaxMetaspaceSize分别是什么MaxMetaspaceSize是元空间最大值默认 -1 表示不限制只受本地内存约束。MetaspaceSize是触发 Full GC 进行类型卸载的初始阈值默认约 21M达到后会触发 Full GC收集器再根据释放情况动态调整该阈值。它不是元空间的初始容量与-XX:PermSize含义不同。7. 为什么建议把两个元空间参数设成一样因为调整元空间大小需要 Full GC代价很高。若两者不同启动阶段元空间反复扩容会触发多次 Full GC。设为相同值可以避免动态调整一般建议比初始值大8G 内存机器通常都设为 256M。8.-Xss调大调小分别有什么影响-Xss越大单线程栈能容纳的栈帧越多递归深度更大但同样内存下可创建的线程数更少-Xss越小则相反。示例中-Xss128k时 count 明显小于默认 1M 的情况说明栈帧数量受栈大小限制。9. 什么情况下会抛 StackOverflowError什么情况下抛 OutOfMemoryErrorStackOverflowError 通常由递归过深或栈帧过大导致单线程栈耗尽OutOfMemoryError 则是内存区域无法再分配例如堆空间不足Java heap space、元空间不足Metaspace、创建线程过多unable to create new native thread。10. 日均百万级订单系统如何设置 JVM 参数先分析对象生命周期订单类对象多为短命对象应让其在新生代回收。可适当增大新生代比例减少过早晋升堆大小根据压测结果确定-Xms与-Xmx保持一致元空间固定为 256M 左右再结合 GC 日志持续观察 Minor GC 与 Full GC 频率进行调优。核心目标是让对象尽量在新生代回收避免频繁 Full GC。11. 如何排查 JVM 内存问题常用手段包括jps查看进程jstat -gc观察 GC 频率与各代使用量jmap -heap与jmap -dump查看堆与导出快照jstack分析线程栈配合 MAT 或 VisualVM 分析 dump 文件定位大对象与内存泄漏。12. 说说对象的创建过程分为五步类加载检查、分配内存、初始化零值、设置对象头、执行init方法。类加载检查确认类已加载解析初始化分配内存按堆是否规整选择指针碰撞或空闲列表初始化零值保证字段不赋初值也能访问到零值设置对象头写入 Mark Word 与类型指针最后执行构造方法完成程序员意愿的初始化。13. 指针碰撞和空闲列表分别适用于什么场景指针碰撞适用于堆内存绝对规整的场景如使用 Serial、ParNew 等带压缩整理的收集器分配时只需移动分界指针效率高也是默认方式。空闲列表适用于内存不规整、已用与空闲交错的场景如 CMS 这类基于标记清除的收集器虚拟机需维护可用内存块列表并更新记录。14. 并发创建对象时如何保证内存分配安全两种方案一是 CAS 加失败重试对分配动作做同步保证更新指针的原子性二是 TLAB为每个线程在堆中预分配一小块内存各线程在自己的缓冲区分配避免竞争。TLAB 默认开启可用-XX:/-UseTLAB控制-XX:TLABSize指定大小。15. 对象在内存中的布局是怎样的分为对象头、实例数据和对齐填充三部分。对象头包含 Mark Word哈希码、GC 分代年龄、锁状态标志、偏向线程 ID 等和类型指针指向类元数据。实例数据是对象真正存储的字段内容对齐填充用于满足 8 字节对齐要求。16. 什么是指针压缩为什么要开启指针压缩指在 64 位 HotSpot 中用 32 位指针表示对象引用参数为-XX:UseCompressedOops默认开启。开启原因是 64 位指针会让内存占用增加约 1.5 倍且在主内存与缓存间移动数据占用更大带宽、加大 GC 压力。通过压缩编码解码32 位地址可支持不超过 32G 的堆堆大于 32G 时压缩失效因此堆内存最好不要超过 32G。17. 什么是逃逸分析它带来哪些优化逃逸分析是分析对象动态作用域判断对象是否会被外部方法引用。若对象不逃逸可做两项优化一是栈上分配对象随栈帧出栈销毁减轻 GC 压力二是标量替换把对象成员变量拆成局部变量在栈帧或寄存器分配避免大块连续内存不足。两者分别由-XX:DoEscapeAnalysis和-XX:EliminateAllocations控制JDK 7 后默认开启。18. 对象什么时候进入老年代主要有四种情况一是大对象超过-XX:PretenureSizeThreshold直接进入二是长期存活对象年龄达到-XX:MaxTenuringThreshold默认 15CMS 默认 6三是动态年龄判断Survivor 中一批对象总大小超过 50% 时年龄不小于该批最大年龄的对象晋升四是 Minor GC 后 Survivor 放不下的存活对象通过分配担保机制提前转移到老年代。19. Minor GC 和 Full GC 有什么区别Minor GC 又称 Young GC只回收新生代触发频繁、速度较快Full GC 一般回收老年代、年轻代和方法区速度通常比 Minor GC 慢 10 倍以上应尽量避免频繁发生。老年代空间分配担保机制会在必要时先触发 Full GC以减少后续晋升失败的风险。20. 在minor gc过程中对象挪动后引用如何修改对象在堆内部挪动的过程其实是复制原有区域对象还在一般不直接清理JVM内部清理过程只是将对象分配指针移动到区域的头位置即可比如扫描s0区域扫到gcroot引用的非垃圾对象是将这些对象复制到s1或老年代最后扫描完了将s0区域的对象分配指针移动到区域的起始位置即可s0区域之前对象并不直接清理当有新对象分配了原有区域里的对象也就被清除了。minor gc在根扫描过程中会记录所有被扫描到的对象引用(在年轻代这些引用很少因为大部分都是垃圾对象不会扫描到)如果引用的对象被复制到新地址了最后会一并更新引用指向新地址。21. 如何判断对象已经死亡为什么不用引用计数法主流 JVM 使用可达性分析以 GC Roots线程栈本地变量、静态变量、本地方法栈变量等为起点搜索引用链不可达的对象判定为可回收。引用计数法实现简单效率高但无法解决对象间循环引用导致计数器永不为 0 的问题因此未被采用。22. 四种引用类型有什么区别强引用只要可达就永不回收软引用在内存不足、GC 后仍无法腾出空间时回收适合做内存敏感缓存弱引用在下一次 GC 时直接回收生命周期更短虚引用最弱随时可能被回收仅用于对象被回收时收到通知几乎不用。23. finalize() 能拯救对象吗能但只有一次机会。可达性分析不可达后对象进入「缓刑」第一次标记筛选是否执行finalize()若覆盖了该方法可在其中重新与引用链上对象建立关联第二次标记时被移出回收集合。但一个对象的finalize()只会执行一次且执行时机不确定生产环境不建议依赖它释放资源。24. 如何判断一个类是无用的类需同时满足三个条件该类所有实例都已被回收加载该类的 ClassLoader 已被回收该类对应的 Class 对象没有任何引用无法通过反射访问其方法。三者同时成立方法区中的类元信息才可能被回收。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询