JVM八股(上部):引言与内存管理
目录
- 引言
- [1.1 什么是 JVM?](#1.1 什么是 JVM?)
- [1.2 说说JVM的组织架构](#1.2 说说JVM的组织架构)
- 内存管理
- [2.1 能说一下 JVM 的内存区域吗?](#2.1 能说一下 JVM 的内存区域吗?)
- [2.2 说一下 JDK 1.6、1.7、1.8 内存区域的变化?](#2.2 说一下 JDK 1.6、1.7、1.8 内存区域的变化?)
- [2.3 为什么使用元空间替代永久代?](#2.3 为什么使用元空间替代永久代?)
- [2.4 对象创建的过程了解吗?](#2.4 对象创建的过程了解吗?)
- [2.5 堆内存是如何分配的?](#2.5 堆内存是如何分配的?)
- [2.6 new 对象时,堆会发生抢占吗?JVM 怎么解决?](#2.6 new 对象时,堆会发生抢占吗?JVM 怎么解决?)
- [2.7 能说一下对象的内存布局吗?](#2.7 能说一下对象的内存布局吗?)
- [2.8 JVM 怎么访问对象的?](#2.8 JVM 怎么访问对象的?)
- [2.9 说一下对象有哪几种引用?](#2.9 说一下对象有哪几种引用?)
- [2.10 Java 堆的内存分区了解吗?](#2.10 Java 堆的内存分区了解吗?)
- [2.11 说一下新生代的区域划分?](#2.11 说一下新生代的区域划分?)
- [2.12 对象什么时候会进入老年代?](#2.12 对象什么时候会进入老年代?)
- [2.13 STW 了解吗?](#2.13 STW 了解吗?)
- [2.14 对象一定分配在堆中吗?](#2.14 对象一定分配在堆中吗?)
- [2.15 内存溢出和内存泄漏了解吗?](#2.15 内存溢出和内存泄漏了解吗?)
- [2.16 能手写内存溢出的例子吗?](#2.16 能手写内存溢出的例子吗?)
- [2.17 内存泄漏可能由哪些原因导致呢?](#2.17 内存泄漏可能由哪些原因导致呢?)
- [2.18 有没有处理过内存泄漏问题?](#2.18 有没有处理过内存泄漏问题?)
- [2.19 有没有处理过内存溢出问题?](#2.19 有没有处理过内存溢出问题?)
- [2.20 什么情况下会发生栈溢出?](#2.20 什么情况下会发生栈溢出?)
一、引言
1. 什么是 JVM?
JVM,也就是 Java 虚拟机,它是 Java 实现跨平台的基石。
程序运行之前,需要先通过编译器将 Java 源代码文件编译成 Java 字节码文件;
程序运行时,JVM 会对字节码文件进行逐行解释,翻译成机器码指令,并交给对应的操作系统去执行。
图例说明:
Java程序 -> 编译 -> 字节码文件
字节码文件 -> JVM (Windows JVM / Linux JVM / Mac JVM) -> 运行 -> 对应的操作系统 (Windows / Linux / Mac)
这样就实现了 Java 一次编译,处处运行的特性。
说说 JVM 的其他特性?
①、JVM 可以自动管理内存,通过垃圾回收器回收不再使用的对象并释放内存空间。
②、JVM 包含一个即时编译器 JIT,它可以在运行时将热点代码缓存到 codeCache 中,下次执行的时候不用再一行一行的解释,而是直接执行缓存后的机器码,执行效率会大幅提高。
③、任何可以通过 Java 编译的语言,比如说 Groovy、Kotlin、Scala 等,都可以在 JVM 上运行。
为什么要学习 JVM?
学习JVM可以帮助我们开发者更好地优化程序性能、避免内存问题。
- 了解JVM的内存模型和垃圾回收机制,可以帮助我们更合理地配置内存、减少GC停顿。
- 掌握JVM的类加载机制可以帮助我们排查类加载冲突或异常。
- JVM还提供了很多调试和监控工具,可以帮助我们分析内存和线程的使用情况,从而解决内存溢出、内存泄露等问题。
面经问题:
京东面经--有了解JVM吗
字节跳动面经--了解过JVM么?讲一下JVM的特性
2. 说说JVM的组织架构(补充)
JVM大致可以划分为三个部分:类加载器、运行时数据区和执行引擎。
① 类加载器 ,负责从文件系统、网络或其他来源加载Class文件,将Class文件中的二进制数据读入到内存当中。
包括:
- Bootstrap Class Loader
- Extension Class Loader
- Application Class Loader
以及用户自定义类加载器。
② 运行时数据区,JVM在执行Java程序时,需要在内存中分配空间来处理各种数据,这些内存区域按照Java虚拟机规范可以划分为方法区、堆、虚拟机栈、程序计数器和本地方法栈。
③ 执行引擎,也是JVM的心脏,负责执行字节码。它包括一个虚拟处理器、即时编译器JIT和垃圾回收器。
面经问题:
腾讯面经--说说JVM的组织架构
得物面经--JVM的架构,具体阐述一下各个部分的功能?
二、内存管理
3. 能说一下 JVM 的内存区域吗?
按照 Java 虚拟机规范,JVM 的内存区域可以细分为 程序计数器、虚拟机栈、本地方法栈、堆和方法区。
图示说明:
- 所有线程共享的数据区:方法区、堆
- 线程隔离的数据区:虚拟机栈、本地方法栈、程序计数器
介绍一下程序计数器?
程序计数器也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码行号指示器。
介绍一下 Java 虚拟机栈?
Java 虚拟机栈的生命周期与线程相同。
当线程执行一个方法时,会创建一个对应的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,然后栈帧会被压入虚拟机栈中。当方法执行完毕后,栈帧会从虚拟机栈中移除。
一个什么都没有的空方法,空的参数都没有,那局部变量表里有没有变量?
- 对于静态方法,由于不需要访问实例对象 this,因此在局部变量表中不会有任何变量。
- 对于非静态方法,即使是一个完全空的方法,局部变量表中也会有一个用于存储 this 引用的变量。this 引用指向当前实例对象,在方法调用时被隐式传入。
局部变量表:是JVM为每个方法在栈帧中创建的一个"工作区",用于存放方法参数和方法内部定义的局部变量。
静态方法(static method)
* 本质 :属于类,而非某个特定对象。调用时是
ClassName.methodName()。* 局部变量表:既然方法与具体对象无关,JVM在调用时就不会传入任何代表当前对象的引用。因此,对于一个真正的空方法,其局部变量表就是空的。
非静态方法(实例方法)
* 本质 :属于对象。调用时是
objectReference.methodName()。* 关键机制 :JVM在调用任何实例方法时,都会隐式地将调用该方法的对象引用(即
this)作为第一个参数传入方法 。* 局部变量表 :即使方法体是空的,这个隐式传入的
this引用也会占据局部变量表索引为0的位置。这就是为什么"局部变量表不为空"的原因。实例:简单来说,实例就是根据某个"类"创建出来的具体对象,它拥有该类所定义的属性和行为。
this:简单来说,
this就是一个指向当前正在执行该实例方法的那个对象的内存地址的引用变量。你可以把它想象成方法内部一个"隐藏的"、指向自身的指针。
- 它指向谁? 它指向调用该方法的那个具体的对象实例 。例如,你执行
myObject.doSomething(),那么在doSomething方法内部,this就指向myObject所代表的那块内存空间中的对象。- 它从哪来? 它是由 Java 编译器在编译时自动添加到实例方法的参数列表中的第一个参数。当 JVM 执行方法调用指令时,会将调用者对象(如上面的
myObject)的引用作为实参传递给这个方法。- 它存放在哪?
this引用本身作为一个变量,存储在 JVM 栈中当前方法的栈帧的局部变量表 里(通常位于索引为 0 的位置)。而this所指向的那个对象实例本身,则存在于 JVM 的堆内存中。
介绍一下本地方法栈?
本地方法栈与虚拟机栈相似,区别在于虚拟机栈是为 JVM 执行 Java 编写的方法服务的,而本地方法栈是为 Java 调用本地 native 方法服务的,通常由 C/C++ 编写。
在本地方法栈中,主要存放了 native 方法的局部变量、动态链接和方法出口等信息。
介绍一下本地方法栈的运行场景?
当 Java 应用需要与操作系统底层或硬件交互时,通常会用到本地方法栈。
比如调用操作系统的特定功能,如内存管理、文件操作、系统时间、系统调用等。
例如:System.currentTimeMillis() 和 Object 类中的 hashCode()、clone() 方法都是本地方法。
Native 方法?
Native 方法是指用 C/C++ 等语言编写的本地方法,通过 JNI(Java Native Interface)调用。
与 Java 方法不同,Native 方法不运行在 JVM 中,而是直接调用操作系统的本地库函数。
例如,System.loadLibrary("nativeLibrary") 用于加载本地动态链接库,System.currentTimeMillis() 用于获取系统当前时间。
介绍一下 Java 堆?
堆是 JVM 中最大的一块内存区域,被所有线程共享,在 JVM 启动时创建,主要用来存储 new 出来的对象。
Java 中"几乎"所有的对象都会在堆中分配,堆也是垃圾收集器管理的目标区域。
从内存回收的角度来看,堆又被细分为新生代、老年代、Eden空间、From Survivor空间、To Survivor空间等。
堆和栈的区别是什么?
- 堆:属于线程共享的内存区域,几乎所有 new 出来的对象都会在堆上分配。生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不再被任何变量引用,最后被垃圾收集器回收。
- 栈:属于线程私有的内存区域,主要存储局部变量、方法参数、对象引用等,通常随着方法调用的结束而自动释放,不需要垃圾收集器处理。
介绍一下方法区?
方法区并不真实存在,属于 Java 虚拟机规范中的一个逻辑概念,用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
在 HotSpot 虚拟机中,方法区的实现称为永久代 (PermGen),但在 Java 8 及之后的版本中,已经被元空间 (Metaspace) 所替代。
变量存在堆栈的什么位置?
- 局部变量:存储在当前方法栈帧中的局部变量表中。当方法执行完毕,栈帧被回收,局部变量也会被释放。
- 静态变量:存储在 Java 虚拟机规范中的方法区中,在 Java 7 中是永久代,在 Java8 及以后是元空间。
面经问题:
京东面经--堆和栈的区别是什么
比亚迪面经--介绍一下JVM运行时数据区
字节跳动面经--jvm结构 运行时数据区有什么结构堆存什么
腾讯面经--new一个对象存放在哪里?(运行时数据区),局部变量存在JVM哪里
4. 说一下 JDK 1.6、1.7、1.8 内存区域的变化?
- JDK 1.6:使用永久代实现方法区。
- JDK 1.7:仍然是永久代,但将字符串常量池、静态变量存放到了堆上。
- JDK 1.8:使用元空间取代永久代,并将运行时常量池、类常量池都移动到了元空间。
5. 为什么使用元空间替代永久代?
客观上 ,永久代会导致 Java 应用程序更容易出现内存溢出的问题,因为它要受到 JVM 内存大小的限制。
主观上 ,Oracle 收购 BEA 获得 JRockit 后,计划将 JRockit 的优秀功能移植到 HotSpot 中。由于两个虚拟机对方法区实现有差异,导致移植工作遇到阻力。
考虑到 HotSpot 的未来发展,JDK 6 时开发团队就打算放弃永久代。JDK 7 时前进了一小步,把原本放在永久代的字符串常量池、静态变量等移动到了堆中。JDK 8 终于完成了这项移出工作。这样的好处是,元空间的大小不再受到 JVM 内存的限制,而是可以像 J9 和 JRockit 那样,只要系统内存足够,就可以一直用。
6. 对象创建的过程了解吗?
当我们使用 new 关键字创建一个对象时,JVM 首先会检查 new 指令的参数是否能在常量池中定位到类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,就先执行类加载。
如果已经加载,JVM 会为对象分配内存完成初始化(比如数值类型的成员变量初始值是 0,布尔类型是 false,对象类型是 null)。接下来会设置对象头,里面包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。最后,JVM 会执行构造方法 <init> 完成赋值操作,这样一个对象就创建完成了。
对象的销毁过程了解吗?
当对象不再被任何引用指向时,就会变成垃圾。垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不可达,就会被回收。垃圾收集器通过标记清除、标记复制、标记整理等算法来回收内存。
面经问题:
比亚迪面经--对象创建到销毁的流程
美国面经--说说创建对象的流程?
携程面经--对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1内存清理和分配)
7. 堆内存是如何分配的?
在堆中为对象分配内存时,主要使用两种策略:指针碰撞 和空闲列表。
- 指针碰撞:适用于管理简单、碎片化较少的内存区域,如年轻代。假设堆内存是连续的,JVM维护一个指针指向下一个可用地址,分配时只需将指针向后移动对象大小的距离。
- 空闲列表:适用于内存碎片化较严重或对象大小差异较大的场景如老年代。JVM维护一个列表记录所有未占用的内存块,分配时遍历列表寻找足够大的空间。
面经问题:
携程面经--对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1内存清理和分配)
8. new 对象时,堆会发生抢占吗?JVM 怎么解决?
会。 多个线程同时为对象分配内存时会发生抢占。
JVM 通过 TLAB(线程本地分配缓冲区)解决:JVM 为每个线程在 Eden 区保留一小块私有内存空间(TLAB)。当线程需要分配对象时,直接从自己的 TLAB 中分配。只有当 TLAB 用尽或对象太大时,才会使用全局分配指针,这时可能需要同步。
9. 能说一下对象的内存布局吗?
对象在内存中包括三部分:对象头、实例数据和对齐填充。
- 对象头:包含 Mark Word(存储哈希码、GC分代年龄、锁状态等)和类型指针(指向类元数据)。如果是数组,还有一个记录数组长度的字段。
- 实例数据:对象实际的字段值,按声明顺序存储。
- 对齐填充:因为 JVM 要求对象起始地址是 8 字节对齐,如果对象总大小不是 8 的倍数,会填充额外字节。
元空间
(类的"档案馆")
对象头
指向
指向
堆中的对象实例
Mark Word
(哈希码、GC年龄、锁状态等)
类型指针
(Klass Pointer)
实例数据
(字段值)
对齐填充
Klass 结构
(类的"身份证")
方法信息
字段信息
常量池指针
虚方法表(vtable)
运行时常量池
(类名、字面量等)
new Object() 对象的内存大小是多少?
在 64 位 JVM 且开启压缩指针(默认)时,new Object() 的大小是 16 字节(8 字节 Mark Word + 4 字节类型指针 + 4 字节对齐填充)。
用过 JOL 查看对象的内存布局吗?
用过。JOL 是一款分析 JVM 对象布局的工具。引入依赖后,可以使用 ClassLayout.parseInstance(obj).toPrintable() 打印对象内存布局。
对象的引用大小了解吗?
在 64 位 JVM 上,未开启压缩指针时,对象引用占 8 字节;开启压缩指针时,对象引用会被压缩到 4 字节。HotSpot 默认开启压缩指针。
说说对象头的作用?
对象头是对象存储在内存中的元信息,包含了 Mark Word、类型指针等信息。
- Mark Word 存储了对象的运行时状态信息,包括锁、哈希值、GC 标记等。在 64 位操作系统下占 8 个字节,32 位操作系统下占 4 个字节。
- 类型指针指向对象所属类的元数据,也就是 Class 对象,用来支持多态、方法调用等功能。
- 除此之外,如果对象是数组类型,还会有一个额外的数组长度字段。占 4 个字节。
类型指针会被压缩吗?
类型指针可能会被压缩,以节省内存空间。比如说在开启压缩指针的情况下占 4 个字节,否则占 8 个字节。在 JDK 8 中,压缩指针默认是开启的。
实例数据了解吗?
了解一些。
实例数据是对象实际的字段值,也就是成员变量的值,按照字段在类中声明的顺序存储。
java
class ObjectDemo {
int age;
String name;
}
JVM 会对这些数据进行对齐/重排,以提高内存访问速度。
对齐填充了解吗?
由于 JVM 的内存模型要求对象的起始地址是 8 字节对齐(64 位 JVM 中),因此对象的总大小必须是 8 字节的倍数。
如果对象头和实例数据的总长度不是 8 的倍数,JVM 会通过填充额外的字节来对齐。
比如说,如果对象头 + 实例数据 = 14 字节,则需要填充 2 个字节,使总长度变为 16 字节。
为什么非要进行 8 字节对齐呢?
因为 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,CPU 的访问效率就会降低。
也就是说,8 字节对齐,是为了效率的提高,以空间换时间的一种方案。
new Object() 对象的内存大小是多少?
一般来说,目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的,因此在 64 位的 JVM 上,new Object() 的大小是 16 字节(12 字节的对象头 + 4 字节的对齐填充)。
对象头的大小是固定的,在 32 位 JVM 上是 8 字节,在 64 位 JVM 上是 16 字节;如果开启了压缩指针,就是 12 字节。
实例数据的大小取决于对象的成员变量和它们的类型。对于 new Object() 来说,由于默认没有成员变量,因此我们可以认为此时的实例数据大小是 0。
假如 MyObject 对象有三个成员变量,分别是 int、long 和 byte 类型,那么它们占用的内存大小分别是 4 字节、8 字节和 1 字节。
java
class MyObject {
int a; // 4 字节
long b; // 8 字节
byte c; // 1 字节
}
考虑到对齐填充,MyObject 对象的总大小为 12(对象头) + 4(a) + 8(b) + 1(c) + 7(填充) = 32 字节。
面经问题:
帆软面经--Object a = new object()的大小,对象引用占多少大小?
去哪儿面经--Object底层的数据结构(蒙了)
10. JVM 怎么访问对象的?
主流的访问方式有两种:句柄 和直接指针。
- 句柄:通过一个中间的句柄表来定位对象。优点是对象被移动时只需修改句柄表中的指针,不需要修改对象引用本身。
- 直接指针 :引用直接存储对象的内存地址。优点是访问速度更快(少一次寻址),缺点是对象移动时需要更新引用。
HotSpot 虚拟机主要使用直接指针来进行对象访问。
11. 说一下对象有哪几种引用?
四种:强引用、软引用、弱引用和虚引用。
- 强引用 :最常见的引用,如
new关键字赋值。只要强引用存在,对象就不会被回收。 - 软引用 :通过
SoftReference类实现。内存不足时会被回收。 - 弱引用 :通过
WeakReference类实现(如ThreadLocal中的Entry)。下一次垃圾回收时会被回收,无论内存是否充足。 - 虚引用 :通过
PhantomReference类实现。主要用来跟踪对象被垃圾回收的过程,在任何时候都可能被回收。
面经问题:
京东面经--四个引用(强软弱虚)
12. Java 堆的内存分区了解吗?
了解。Java 堆被划分为新生代 和老年代两个区域。
- 新生代:又被划分为一个 Eden 空间和两个 Survivor 空间(From 和 To)。新对象在 Eden 分配,满时触发 Minor GC,存活对象进入 Survivor 区。
- 老年代:存放长期存活的对象。当老年代内存不足时,会触发 Major GC / Full GC。
面经问题:
得物面经--Java中堆内存怎么组织的
腾讯面经--怎么来区分对象是属于哪个代的?
13. 说一下新生代的区域划分?
新生代的垃圾收集主要采用标记-复制算法 。
虚拟机将内存分为一块较大的 Eden 空间 和两块较小的 Survivor 空间,默认比例是 8:1:1。每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。
14. 对象什么时候会进入老年代?
-
长期存活的对象 :对象在新生代中经历 Minor GC 的次数(年龄)超过阈值(默认15,通过
-XX:MaxTenuringThreshold设置)后,会进入老年代。 -
大对象:占用内存较大的对象(如大数组、长字符串等)。
- 判断标准 :
- 大小由 JVM 参数
-XX:PretenureSizeThreshold控制,但在 JDK 8 中,默认值为 0,也就是说默认情况下,对象仅根据 GC 存活的次数来判断是否进入老年代。 - G1 垃圾收集器中,大对象会直接分配到专门的 Humongous 区域。当对象大小超过一个 Region 容量的 50% 时,会被认为是大对象。
- 大小由 JVM 参数
- Region 大小 :可以通过 JVM 参数
-XX:G1HeapRegionSize来设置,默认情况下从 1MB 到 32MB 不等,会根据堆内存大小动态调整。
- 判断标准 :
-
动态年龄判定:如果 Survivor 区中所有对象的总大小超过了一定比例(通常是 Survivor 区的一半),那么年龄较小的对象也可能会被提前晋升到老年代。
- 原因:如果年龄较小的对象在 Survivor 区中占用了较大的空间,会导致 Survivor 区中的对象复制次数增多,影响垃圾回收的效率。
面经问题:
阿里面经--哪些情况下对象会进入老年代?
京东面经--新生代对象转移到老年代的条件
拼多多面经--对象什么时候进入老年代
15. STW 了解吗?
了解。
JVM 进行垃圾回收的过程中,为了保证对象引用在移动过程中不被修改,必须暂停所有的用户线程,这样的停顿称为 Stop The World(STW)。
如何暂停线程?
JVM 使用 安全点(Safe Point) 机制来确保线程能够被安全地暂停。线程执行到安全点(通常位于方法调用、循环跳转、异常处理等位置)后,挂起自身等待垃圾收集完成,GC 完成后再恢复执行。
16. 对象一定分配在堆中吗?
不一定。
默认情况下,Java 对象是在堆中分配的。但 JVM 会进行逃逸分析 ,如果分析发现某个对象的作用域没有逃逸出方法(即生命周期只在方法内部),那么这个对象就可以在栈上分配内存,随着栈帧的出栈而销毁。
什么是逃逸分析?
逃逸分析是一种 JVM 优化技术,用来分析对象的作用域和生命周期,判断对象是否逃逸出方法或线程。
- 方法逃逸:对象被方法外部的代码引用(如作为返回值)。
- 线程逃逸 :对象被另外一个线程引用。
如果对象没有逃逸,就可以进行栈上分配、同步消除、标量替换等优化。
如何确认 JVM 是否开启了逃逸分析?
可以通过以下命令查看:
bash
java -XX:+PrintFlagsFinal -version | grep DoEscapeAnalysis
在 JDK 8 中,逃逸分析默认是开启的。
代码示例:
未逃逸的情况:
java
public static void testStackAllocation() {
User user = new User(); // 对象未逃逸,可能在栈上分配
user.setName("张三");
user.setAge(18);
System.out.println(user.getName());
}
方法逃逸的情况:
java
public static User testMethodEscape() {
User user = new User(); // 对象逃逸出方法
user.setName("张三");
return user; // 返回对象引用,发生方法逃逸
}
线程逃逸的情况:
java
public static void testThreadEscape() {
User user = new User(); // 对象逃逸到线程
executorService.execute(() -> {
System.out.println(user.getName()); // 线程中引用对象
});
}
逃逸分析的优化技术:
- 栈上分配:将未逃逸的对象分配到栈上,减少堆内存使用和 GC 压力
- 同步消除:对于未逃逸的对象,消除同步操作,提高性能
- 标量替换:将对象分解为基本数据类型,进一步减少内存使用
标量替换示例:
java
public static void testScalarReplacement() {
Point point = new Point(1, 2); // 未逃逸的对象
System.out.println(point.getX() + point.getY());
}
// 可能被优化为:
public static void testScalarReplacement() {
int x = 1; // 标量替换
int y = 2; // 标量替换
System.out.println(x + y);
}
面经问题:
收钱吧面经--所有对象都在堆上对不对?
17. 内存溢出和内存泄漏了解吗?
- 内存溢出(OOM) :当程序请求分配内存时,由于没有足够的内存空间,从而抛出
OutOfMemoryError。可能是因为堆、元空间、栈或直接内存不足导致的。 - 内存泄漏:程序在使用完内存后,未能及时释放,导致占用的内存无法再被使用。通常是因为长期存活的对象持有短期存活对象的引用,又没有及时释放。内存泄漏积累会导致可用内存减少,最终可能引发内存溢出。
面经问题:
京东面经--说说 OOM 的原因
快手面经--了解 OOM 吗?
18. 能手写内存溢出的例子吗?
可以。以下是堆内存溢出的例子:
java
class HeapSpaceErrorGenerator {
public static void main(String[] args) {
List<byte[]> bigObjects = new ArrayList<>();
try {
while (true) {
byte[] bigObject = new byte[10 * 1024 * 1024]; // 10M
bigObjects.add(bigObject);
}
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError 发生在 " + bigObjects.size() + " 对象后");
throw e;
}
}
}
通过设置 VM 参数 -Xmx128M 可以更快触发。
不同内存区域的 OOM 情况:
- 程序计数器:唯一不会 OOM 的区域
- 其他所有区域 :都可能因内存耗尽导致 OOM
- 堆:创建太多对象,GC 后仍无法分配内存
- 虚拟机栈 :
- 栈帧过多(无限递归)→
StackOverflowError - 创建线程过多 →
Unable to create native thread
- 栈帧过多(无限递归)→
- 元空间:加载的类/常量过多
- 直接内存:分配的堆外内存过多
一句话验证 :每个区域的 OOM 本质上都是该区域的内存容量被耗尽。
面经问题:
京东面经--说说 OOM 的原因
快手面经--Java哪些内存区域会发生 OOM? 为什么?
19. 内存泄漏可能由哪些原因导致呢?
①、静态集合中添加的对象越来越多,没有及时清理。
②、单例模式对象持有的外部引用无法及时释放。
③、数据库、IO、Socket 等连接资源没有及时关闭。
④、ThreadLocal 的引用未被清理,线程退出后仍然持有对象引用。
20. 有没有处理过内存泄漏问题?
有。在做技术派项目时,由于 ThreadLocal 没有及时清理导致内存泄漏。
排查步骤:
- 使用
jps -l查看 Java 进程 ID。 - 使用
top -p [pid]查看进程 CPU 和内存占用。 - 使用
top -Hp [pid]查看进程下线程占用情况。 - 使用
jstack -F [pid] > dump.txt抓取线程栈,分析是否有死锁、死循环。 - 使用
jstat -gcutil [pid] 5000 10查看 GC 情况,重点关注 Full GC 频率。 - 使用
jmap -dump:format=b,file=heap.hprof [pid]生成堆转储文件。 - 使用 VisualVM 等可视化工具分析 dump 文件,定位内存占用最高的对象和泄漏源头。
面经问题:
京东面经--什么是内存泄露
快手面经--Java哪些内存区域会发生 OOM? 为什么?
美国面经--内存泄漏怎么排查
21. 有没有处理过内存溢出问题?
有。在做技术派时,由于上传的文件过大,没有正确处理,导致内存撑爆。
处理步骤:
- 使用
jmap -dump:format=b,file=heap.hprof <pid>生成 Heap Dump 文件。 - 使用 MAT、JProfiler 等工具分析,查看内存中的对象占用情况。
- 解决方案:
- 如果生产环境内存有余,适当增大堆内存(如
-Xmx4g)。 - 检查代码是否存在内存泄漏,如未关闭的资源。
- 在本地进行压力测试,验证修改效果。
- 如果生产环境内存有余,适当增大堆内存(如
面经问题:
华为面经--如何排查 OOM?
荣耀面经--有没遇到内存泄露,溢出的情况,怎么发生和处理的?
22. 什么情况下会发生栈溢出?(补充)
栈溢出发生在程序调用栈的深度超过 JVM 允许的最大深度时,本质是线程的栈空间不足,无法为新的栈帧分配内存。
最常见场景:
- 递归调用没有终止条件,导致无限递归。
- 方法中定义了特别大的局部变量,导致栈帧过大,栈空间更快耗尽。
面经问题:
OPPO面经--什么情况下会发生栈溢出?