news 2026/11/2 6:35:36

JVM 对象创建及内存分配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM 对象创建及内存分配

一、JDK 体系结构与跨平台特性

Java 的跨平台能力来自「一次编写,到处运行」的设计:源码经javac编译为与平台无关的字节码(.class),再由各平台对应的 JVM 负责解释执行或即时编译(JIT)为本地机器码。因此跨平台的是字节码,而 JVM 本身是平台相关的。

JDK 体系结构可以按层次理解为:

  • JDK:Java 开发工具包,包含 JRE 以及javac、javap、jps、jstat、jmap、jstack等工具。
  • JRE:Java 运行时环境,包含 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:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -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 参数没有固定标准,需要结合项目实际情况分析。以日均百万级订单交易系统为例,思路如下:

  1. 先估算对象生命周期:大部分订单对象是「朝生夕死」的短命对象,应尽量让它们在新生代完成分配与回收。
  2. 适当放大新生代,减少对象过早晋升到老年代,从而降低 Full GC 频率。
  3. 给系统留足堆内存,避免新生代频繁触发 Minor GC。
  4. 元空间固定大小,避免启动期反复 Full GC。

核心优化原则:尽可能让对象在新生代分配和回收,避免大量对象频繁进入老年代,避免频繁对老年代做垃圾回收;同时给系统充足内存,避免新生代频繁 GC。

六、对象的创建过程

虚拟机遇到一条new指令时,对象的创建大致分为五步。语言层面的new关键字、对象克隆、对象序列化,最终都会落到new指令上。

  1. 类加载检查:先检查指令参数能否在常量池中定位到一个类的符号引用,并确认该类是否已被加载、解析和初始化过;若没有,先执行类加载过程。
  2. 分配内存:类加载完成后对象所需内存大小即可完全确定,分配空间等同于把一块确定大小的内存从 Java 堆中划分出来。
  3. 初始化零值:将分配到的内存空间(不含对象头)初始化为零值,若使用 TLAB (线程本地分配缓冲区) 可提前到分配时进行。这保证了实例字段不赋初值也能直接使用,访问到的是对应类型的零值。
  4. 设置对象头:设置对象是哪个类的实例、如何找到元数据信息、哈希码、GC 分代年龄等,存放在对象头 Object Header 中。
  5. 执行<init>方法:按程序员意愿初始化,即给属性赋真实值并执行构造方法。

1. 内存划分的两种方式

  • 指针碰撞(Bump the Pointer):堆内存绝对规整时,已用内存在一边、空闲内存在另一边,中间放一个分界指针,分配时只需把指针向空闲方向挪动与对象大小相等的距离。默认使用指针碰撞。
  • 空闲列表(Free List):堆内存不规整、已用与空闲交错时无法指针碰撞,虚拟机维护一个列表记录可用内存块,分配时找到足够大的块划分出去并更新列表。

2. 并发分配的两种解决方案

  • CAS + 失败重试:多线程修改Eden分配指针top时,采用比较并交换的无锁机制保证原子性,对分配内存的动作做同步处理,防止对象内存覆盖,保证更新指针操作的原子性。TLAB就是为了减少CAS竞争
  • TLAB(Thread 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> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
import 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 total

2. 什么是对象指针压缩

  • 从 JDK 1.6 update14 开始,64 位操作系统中 JVM 支持指针压缩。
  • 参数为UseCompressedOops,其中 compressed 表示压缩,oop 即 ordinary object pointer(对象指针)。
  • 启用:-XX:+UseCompressedOops(默认开启);禁止:-XX:-UseCompressedOops。

3. 为什么要压缩?

  1. 64 位 HotSpot 若使用 32 位指针,内存使用会多出约 1.5 倍;较大指针在主内存与缓存之间移动数据会占用更大带宽,GC 压力也更大。
  2. 为减少 64 位平台下的内存消耗,启用指针压缩。
  3. 32 位地址最大支持 4G(2 的 32 次方),通过对对象指针做压缩编码与解码,JVM 只用 32 位地址即可支持更大内存(不超过 32G)。
  4. 堆内存小于 4G 时无需启用压缩,JVM 会直接去掉高 32 位地址,使用低虚拟地址空间。
  5. 堆内存大于 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:+DoEscapeAnalysis,JDK 7 之后默认开启,关闭用-XX:-DoEscapeAnalysis。
  • 标量替换:确定对象不逃逸且可进一步分解时,JVM 不创建对象,而是把成员变量拆成若干局部变量,在栈帧或寄存器上分配空间,避免因缺少大块连续内存而分配失败。开关-XX:+EliminateAllocations,JDK 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。大量对象分配在 Eden,Eden 满后触发 Minor GC,99% 以上对象被回收,存活对象挪到为空的 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 GC;GC 期间发现 allocation1 无法放入 Survivor,于是提前转移到老年代,老年代空间足够,不会触发 Full GC:

[GC (Allocation Failure) [PSYoungGen: 65253K->936K(76288K)] 65253K->60944K(251392K), 0.0279083 secs] [Times: user=0.13 sys=0.02, real=0.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:PretenureSizeThreshold=1000000 -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 都会计算老年代剩余可用空间:

  1. 若可用空间小于年轻代现有所有对象大小之和(含垃圾对象),检查-XX:-HandlePromotionFailure参数是否设置(JDK 1.8 默认已设置)。
  2. 若设置了该参数,再看老年代可用内存是否大于之前每次 Minor GC 后进入老年代对象的平均大小。
  3. 若上一步结果为小于,或该参数未设置,则触发一次 Full GC,对老年代和年轻代一起回收;回收后仍无足够空间存放新对象则发生 OOM。
  4. 若 Minor GC 后存活对象仍需挪到老年代且大于老年代可用空间,同样触发 Full GC;Full 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 SoftReference<User> user = new SoftReference<User>(new User()); // 弱引用 public static WeakReference<User> user = new WeakReference<User>(new User());

软引用的典型应用是浏览器的后退按钮:若网页浏览结束即回收内容,按后退需重新构建;若把浏览过的网页都存到内存,又会造成内存浪费甚至溢出,软引用正好在两者之间取得平衡。

4. finalize() 方法最终判定对象是否存活

可达性分析中不可达的对象并非「非死不可」,而是处于「缓刑」阶段,真正宣告死亡至少要经历两次标记:

  1. 第一次标记并筛选:判断对象是否有必要执行finalize()。未覆盖finalize方法的对象直接回收。
  2. 第二次标记:若覆盖了finalize方法,这是对象逃脱死亡的最后机会。只要在finalize()中重新与引用链上任意对象建立关联(如把自己赋值给某个类变量或对象成员变量),第二次标记时就会被移出「即将回收」集合;否则基本就真的被回收了。

注意:一个对象的finalize()方法只会被执行一次,自我拯救的机会只有一次。

public class OOMTest { public static void main(String[] args) { List&lt;Object&gt; list = new ArrayList&lt;&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,什么情况下抛 OutOfMemoryError?

StackOverflowError 通常由递归过深或栈帧过大导致单线程栈耗尽;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(默认 15,CMS 默认 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 对象没有任何引用,无法通过反射访问其方法。三者同时成立,方法区中的类元信息才可能被回收。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 6:35:19

小白程序员必备:大模型在医疗领域的最新动态与学习指南

本文探讨了医疗大模型领域的最新进展&#xff0c;包括微软开发的逐步追问与检查的诊断系统、模型在指南更新中的应用、多智能体在会诊中的作用&#xff0c;以及本地推理模型在文献筛选中的应用。文章还介绍了讯飞医疗的多模态升级和紫荆智康的多学科人机协作框架。这些研究展示…

作者头像 李华
网站建设 2026/11/2 6:34:48

springmvc 和 springBoot 创建拦截器的区别

springmvc创建拦截器方式 1、创建MyInterceptor&#xff08;类名自己起&#xff09;类 实现 HandlerInterceptor 接口。 2、重写 HandlerInterceptor 接口的 preHandle、postHandle、afterCompletion方法。 3.创建SpringmvcConfig&#xff08;类名自己起&#xff09; 类&…

作者头像 李华
网站建设 2026/11/2 6:32:44

链表的特点以及实现步骤

一、链表是什么 链表不是一整块内存&#xff0c;它把每一份数据包装成节点Node。 每个节点做两件事&#xff1a;存真正的数据 data &#xff1b;存下一个节点的地址 next &#xff0c;指向下一个小伙伴 1.链表的优点 新增尾部数据很快。我们代码里加了尾节点 last &#xff0…

作者头像 李华
网站建设 2026/11/2 6:32:22

揭秘Linux进程管理核心:从PCB到fork机制

目录 一、进程是什么 1.1 什么是进程 1.2 描述进程&#xff1a;PCB进程控制块 1.3 task_struct中存储什么 二、 组织进程 2.1进程管理 2.2 进程查看 三、fork创建进程 3.1 fork的作用 3.2 fork的原理 四、Linux内核中的进程状态 4.1 运行态 4.2 阻塞态 4.3 挂起态…

作者头像 李华
网站建设 2026/11/2 6:31:29

Agent 16. 如何设计多 Agent 的协作与动态切换机制?

16. 如何设计多 Agent 的协作与动态切换机制&#xff1f; &#x1f454;面试官&#xff1a;多 Agent 系统里&#xff0c;各个 Agent 之间怎么协作&#xff1f; &#x1f64b;‍♂️我&#xff1a;一个 Agent 做完之后把结果传给下一个 Agent&#xff0c;就像流水线一样&#xf…

作者头像 李华
网站建设 2026/11/2 6:29:35

mithril.js PWA 完整指南:用 8.93KB 框架搭出可安装的离线应用

mithril.js PWA 完整指南&#xff1a;用 8.93KB 框架搭出可安装的离线应用 【免费下载链接】mithril.js A JavaScript Framework for Building Brilliant Applications 项目地址: https://gitcode.com/gh_mirrors/mi/mithril.js mithril.js 是一个构建单页应用的 JavaSc…

作者头像 李华