undefined
。程序计数器是唯一在 JVM 规范中没有规定任何OutOfMemoryError
情况的区域,其主要目的是为了线程切换后能恢复到正确的执行位置,保证程序的有序执行。·StackOverflowError
;若栈动态扩展失败,会抛出OutOfMemoryError
。它是Java方法执行的核心数据结构,确保线程内的方法调用链能正确恢复执行上下文。StackOverflowError
(栈深度超限)和OutOfMemoryError
(扩展失败)。它为Java提供了与操作系统、硬件交互的桥梁,常用于JNI(Java Native Interface)、反射等场景。-Xms
(初始大小)和-Xmx
(最大大小)参数配置,若对象分配时堆空间不足,会触发GC,若GC后仍无法满足需求则抛出OutOfMemoryError
。-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
等参数配置,若类加载过多导致空间不足,会抛出OutOfMemoryError: Metaspace
。String.intern()
将字符串添加到常量池)。运行时常量池的特点是全局共享且具备动态性,例如两个不同类的相同字符串常量会指向同一个运行时常量池实例。若常量池无法申请到足够内存,会抛出OutOfMemoryError: Metaspace
(JDK 8+)。它是Java实现字符串驻留(String Interning)和符号引用解析的核心机制,确保了常量的高效复用和引用的准确解析。java.nio.DirectByteBuffer
等类进行操作,避开了 Java 堆与 native 堆之间的数据拷贝,能显著提升 NIO(非阻塞 IO)操作的性能,尤其适用于频繁的网络通信、文件读写等场景。直接内存的分配不受 JVM 堆大小限制,但受限于系统总内存,其回收机制依赖于垃圾回收(通过 Cleaner 或 PhantomReference 触发),若分配过多可能导致OutOfMemoryError
。由于它不属于 JVM 管理的内存,-Xmx
等堆参数无法直接控制其大小,通常需通过-XX:MaxDirectMemorySize
指定上限。-Xss
参数调整),由 JVM 在创建线程时动态分配和管理。当栈溢出时,通常是因为递归调用过深导致栈帧数量过多,或局部变量占用空间过大(如定义超大数组)。堆的空间较大且线程共享,默认大小通常为物理内存的 1/4(可通过 -Xmx
参数调整),支持动态扩展,由 JVM 通过垃圾回收机制(GC)自动管理。堆溢出通常是由于持续创建大量大对象,或存在内存泄漏(如静态集合持有对象引用导致无法被 GC 回收)。基本数据类型(如 int、float、bool 等):在栈中直接存储值本身。
引用类型(对象、数组等):栈中通常存储的是指向堆中对象的指针(或引用地址),而对象的实际数据存放在堆中。比如 Java 中String s = new String("test");
,栈中存储的是s
这个引用(可理解为指向堆中字符串对象的指针),真正的 “test” 字符数据则在堆中。
程序计数器是 JVM 中用于记录当前线程正在执行的字节码指令地址的内存区域,其核心作用是支持指令的顺序执行、分支跳转及线程切换时的上下文恢复;而它被设计为线程私有的原因,在于多线程环境下需确保各线程执行路径独立隔离,避免指令执行混乱,同时保障线程切换和异常处理时的上下文正确性,是线程并发执行的基础保障。
iadd
指令将两个整数相加)。创建方式 | 存储位置 | 示例 |
---|---|---|
字面量赋值("xxx" ) |
字符串常量池 | String s = "hello"; |
new String("xxx") |
堆 | String s = new String("hello"); |
intern() 方法入池 |
字符串常量池 | String s = new String("java").intern(); |
运行时动态拼接(变量 + 变量) | 堆 | String s = a + b; (a、b 为变量) |
字符串常量池和堆内存。
String
实例new String(...)
关键字的语义是强制在堆中创建一个新的String
对象,该对象会引用常量池中 “abc” 的字符数据(JDK 7 + 为引用共享,而非拷贝)。即使常量池已有相同内容,new
也会保证生成一个新的堆对象。引用类型 | 回收条件 | 典型用途 | 强度等级(从强到弱) | 代码示例 |
---|---|---|---|---|
强引用 | 无强引用指向对象时(如 obj=null ) |
常规对象存储(如成员变量、局部变量) | 最强 | Object obj = new Object(); |
软引用 | 内存不足(即将 OOM)时 | 内存敏感的缓存(如图片缓存) | 较强 | SoftReference |
弱引用 | 发生 GC 时(无论内存是否充足) | 临时关联数据(如 WeakHashMap ) |
较弱 | WeakReference |
虚引用 | 对象被 GC 回收时(仅用于接收通知) | 跟踪对象回收状态(如释放 Native 资源) | 最弱 | PhantomReference |
字符串常量池引用 | 无强引用且被 JVM 卸载时(极罕见) | 字符串复用(如字面量 "abc" ) |
同强引用 | String s = "abc"; (若常量池已有 "abc" ,则 s 直接引用池中的对象) |
类引用 | 类加载器被回收且无其他引用时 | 反射操作(如 Class.forName() ) |
较强 | Class> cls = String.class; |
static List
)存储临时对象,或单例模式持有外部资源(如数据库连接)。InputStream
、Connection
等资源未关闭,导致对象无法被回收。new byte[1024*1024*100]
)或加载大文件。-Xmx
)设置过小,无法满足程序运行需求。内存区域 | 溢出类型 | 触发条件 | 典型错误信息 | 常见场景 |
---|---|---|---|---|
堆内存(Heap) | 堆溢出(最常见) | 1. 创建的对象过多且未被回收(如无限循环创建对象) 2. 内存泄漏累积导致堆空间耗尽 3. 堆内存配置过小(-Xmx 不足) |
java.lang.OutOfMemoryError: Java heap space |
- 批量处理大量数据时未分页 - 内存泄漏(如静态集合持有大量对象) - 大对象分配(如超大数组 new byte[1024*1024*1000] ) |
方法区 / 元空间 | 方法区 / 元空间溢出 | 1. 加载的类过多(如动态生成类的框架:CGLib、反射) 2. 常量池过大(如大量字符串 intern 操作) 3. 元空间配置过小(-XX:MaxMetaspaceSize 不足) |
JDK 7 及以前:java.lang.OutOfMemoryError: PermGen space JDK 8+:java.lang.OutOfMemoryError: Metaspace |
- 频繁使用动态代理生成类 - 应用服务器热部署类未卸载(类加载器泄漏) - 常量池中存储大量字符串且未被回收 |
虚拟机栈 / 本地方法栈 | 栈溢出(Stack Overflow) | 1. 方法调用层级过深(如无限递归) 2. 单个线程栈空间过小(-Xss 配置不足) |
java.lang.StackOverflowError |
- 无终止条件的递归调用(如 public void f() { f(); } ) - 复杂算法导致调用栈过深 |
虚拟机栈 / 本地方法栈 | 栈内存溢出(线程创建过多) | 1. 创建大量线程,每个线程占用栈空间,总栈内存超过系统限制 2. 栈空间配置过大(-Xss 过大)导致总内存超限 |
java.lang.OutOfMemoryError: unable to create new native thread |
- 短时间内创建 thousands 级线程(如循环创建线程且不销毁) - 32 位系统中进程总内存有限,线程栈过多易触发 |
直接内存(Direct Memory) | 直接内存溢出 | 1. NIO 中 ByteBuffer.allocateDirect() 分配的直接内存过多 2. 直接内存未被释放(如未调用 cleaner() ) 3. 直接内存配置过小(-XX:MaxDirectMemorySize 不足) |
java.lang.OutOfMemoryError: Direct buffer memory |
- 频繁使用 NIO 操作大文件,未及时释放直接内存 - 框架(如 Netty)误用直接内存导致泄漏 |
静态属性导致内存泄露
静态属性的生命周期与 JVM 一致,添加的对象永远不会被 GC 回收,导致内存持续占用,最终可能引发OutOfMemoryError: Java heap space
。
**如何优化?**第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。
未关闭的资源
无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。
**如何优化?**第一,始终记得在finally中进行资源的关闭;第二,关闭连接的自身代码不能发生异常;第三,Java7以上版本可使用try-with-resources代码方式进行资源关闭。
使用ThreadLocal
ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
**如何优化?**第一,使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;第二,不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。第三,最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。
当执行new Person(...)
时,JVM 首先会检查:
Person
)是否已被加载到方法区(Method Area)中。com.example.Person
)执行类加载过程(加载→验证→准备→解析→初始化),将类的信息(属性、方法、常量等)存入方法区。类加载完成后,JVM 会为新对象在堆内存(Heap) 中分配空间,空间大小由类的属性(成员变量)决定(包括继承自父类的属性)。
内存分配后,JVM 会先将分配的内存空间初始化为零值(如int
为 0,String
为null
),这一步保证了对象即使未显式初始化,也能访问到默认值。
随后,JVM 会设置对象的对象头(Object Header) 信息,包括:
hashCode()
时计算);零值初始化后,JVM 会调用类的构造方法(Constructor),对对象进行显式初始化(如为name
和age
赋值)。
super()
),再执行当前类构造方法中的代码。5. 将对象引用赋值给变量
最后,JVM 将堆中对象的内存地址(引用)赋值给栈内存中的变量(如Person p = new Person(...)
中的p
),此后通过该引用即可操作对象。
java.lang.*
、java.util.*
等。JRE/lib
目录下的类库(如rt.jar
、charsets.jar
)。java.lang.Object
、java.util.ArrayList
。JRE/lib/ext
目录下的类库(如 XML 解析器、加密算法等)。java.net.URLClassLoader
。javax.crypto.*
包中的类。classpath
下的类库,即开发者编写的代码及依赖。CLASSPATH
或maven
/gradle
等构建工具配置的依赖路径。URLClassLoader
,是ClassLoader.getSystemClassLoader()
的返回值。com.example.MyClass
)。java.lang.ClassLoader
或URLClassLoader
实现特殊加载需求。findClass()
方法(推荐)或loadClass()
方法(需谨慎,避免破坏双亲委派)。双亲委派模型的工作原理是:当一个类加载器收到加载类的请求时,它不会先自己尝试加载,而是先将请求委派给父类加载器处理;父类加载器同样会把请求向上委派,直到传递到最顶层的启动类加载器;之后从启动类加载器开始,逐层向下尝试加载该类,若某个类加载器能成功加载就返回结果,若所有父类加载器都无法加载,最终才由最初发起请求的类加载器自行加载。这一过程通过“向上委派请求、向下查找加载”的方式,确保了类加载的安全性和唯一性。
如果在向下查找时刚好遇到最初请求的类加载器才能成功加载,此时属于什么情况?
这种场景属于 “所有父类加载器都无法加载,回到最初请求的类加载器加载”。因为 “向下查找” 的过程本身就包含了对 “所有父类是否能加载” 的验证 —— 只有当上层父类全部失败后,才会轮到最初的加载器尝试,而它的成功正是 “父类均失败” 的直接结果。两者并非矛盾,而是同一过程的前后逻辑:“向下查找” 是流程形式,“父类均失败后自身加载” 是该流程在终点处的具体结果。
java.lang.Object
类只会由启动类加载器加载,任何自定义类加载器都无法自行加载同名类,避免了 “恶意类篡改核心类” 的风险(如伪造java.lang.String
类替换系统类),保障了 Java 核心库的安全性。rt.jar
),扩展类加载器加载扩展库(如ext
目录),应用类加载器加载应用程序类,自定义类加载器则处理特殊需求(如加密类、网络加载类)。这种层级划分使得类加载流程清晰可控,符合 Java “沙箱安全” 和 “模块化设计” 的理念。**加载:**通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
**连接:**验证、准备、解析 3 个阶段统称为连接。
**验证:**确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证
**准备:**为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了
**解析:**解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用, 那引用的目标必定已经存在在内存中了。
初始化:初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法,要注意的是这里的构造器方法()并不是开发者写的,而是编译器自动生成的。
**使用:**使用类或者创建对象
**卸载:**如果有下面的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。 3. 类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
在 Java 中,垃圾回收(Garbage Collection,简称 GC) 是 JVM(Java 虚拟机)自动管理内存的机制,用于识别并回收不再被程序使用的对象所占用的内存空间,避免内存泄漏和溢出,简化开发者的内存管理工作。
引用计数法:
原理:
为每个对象维护一个引用计数器,当有新的引用指向该对象时,计数器加 1;当引用失效(如引用被赋值为null
或超出作用域)时,计数器减 1。当计数器为 0 时,对象被视为垃圾。
缺陷:
无法解决循环引用问题。例如:
class A { public B b; }
class B { public A a; }
A objA = new A();
B objB = new B();
objA.b = objB; // A引用B
objB.a = objA; // B引用A
objA = null;
objB = null
此时,objA
和objB
相互引用,各自的引用计数器为 1,但外部已无引用指向它们,导致它们无法被回收,造成内存泄漏。
可达性算法:
原理:
以一组称为GC Roots的对象为起点,通过引用链(Reference Chain)遍历所有可达对象(即 “活对象”),未被遍历到的对象则被视为不可达(即垃圾)。
GC Roots 的类型(以下对象永远不会被回收):
虚拟机栈(栈帧中的本地变量表)引用的对象:
例如方法中的局部变量。
public void method() {
Object obj = new Object(); // obj是GC Root,指向的对象不可回收
} // 方法结束后,obj出栈,其指向的对象可能被回收
方法区中类静态属性引用的对象:
例如static变量。
public class Demo {
static Object staticObj = new Object(); // staticObj是GC Root
}
方法区中常量引用的对象:
例如final常量。
public class Demo {
final static Object CONST_OBJ = new Object(); // CONST_OBJ是GC Root
}
本地方法栈中 JNI(Native 方法)引用的对象:
例如 Java 调用 C/C++ 代码时,Native 方法引用的 Java 对象。
JVM 内部的引用:
如类加载器、基本数据类型的 Class 对象、常驻的异常对象(如NullPointerException
)等。
核心思想:分为 “标记” 和 “清除” 两个阶段,是最基础的垃圾回收算法。
示例:
堆中存在 A(存活)、B(垃圾)、C(存活)、D(垃圾)四个对象,标记后清除 B 和 D,剩余 A 和 C。
优点:
实现简单,无需移动对象。
缺点:
核心思想:将堆内存分为两块大小相等的区域(如 From 区和 To 区),每次只使用其中一块(From 区)。当 From 区满时,将存活对象复制到另一块未使用的区域(To 区),然后清空 From 区,交换两者的角色(From 变 To,To 变 From)。
示例:
From 区有 A(存活)、B(垃圾)、C(存活),复制时仅将 A 和 C 复制到 To 区,然后清空 From 区,后续新对象分配到原 To 区(现在的 From 区)。
优点:
缺点:
核心思想:结合 “标记 - 清除” 和 “复制” 的优点,分为 “标记”“整理” 两个阶段。
示例:
堆中标记出 B 和 D 为垃圾,整理时将 A 和 C 移动到内存起始位置,然后清除 A、C 之后的所有内存(包括 B 和 D)。
优点:
缺点:
核心思想:根据对象的存活周期,将堆内存划分为不同区域(新生代、老年代、永久代 / 元空间),针对不同区域采用不同的回收算法。
这是当前所有商用 JVM 的默认垃圾回收策略(非独立算法,而是对上述算法的组合应用)。
-XX:PretenureSizeThreshold
),则直接进入 老年代。-XX:MaxTenuringThreshold
(默认 15)。1. 标记 - 清除算法(Mark-Sweep)
2. 标记 - 复制算法(Mark-Copy)
3. 标记 - 整理算法(Mark-Compact)
4. 分代回收算法(基于上述算法组合)
分代回收(如新生代用复制算法,老年代用标记 - 整理 / 清除)中,各代的回收阶段均可能 STW:
维度 | Minor GC(新生代 GC) | Major GC(老年代 GC) | Full GC(全局 GC) |
---|---|---|---|
回收区域 | 仅新生代(Eden 区 + Survivor 区) | 仅老年代 | 新生代 + 老年代(有时包含永久代 / 元空间) |
触发频率 | 高(新对象频繁分配,Eden 区易满) | 低(老年代对象存活时间长,空间增长慢) | 低(通常是 Major GC 的 “附带产物” 或特殊场景触发) |
STW 时间 | 短(新生代对象存活少,复制算法高效) | 较长(老年代对象多,标记 - 整理 / 清除算法耗时) | 最长(回收区域大,涉及对象多) |
使用的回收算法 | 标记 - 复制算法(新生代特点:对象存活率低) | 标记 - 清除 / 标记 - 整理算法(老年代特点:存活率高) | 同 Major GC(覆盖所有区域,算法组合使用) |
触发Full Gc的场景
直接调用System.gc()或Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。
Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。
当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。
堆内存的 GC
堆是 Java 垃圾回收的核心区域,所有通过new
创建的对象都存储在这里。GC 会频繁针对堆的新生代(Minor GC)和老年代(Major GC/Full GC)进行操作,通过标记 - 清除、复制、标记 - 整理等算法,识别并回收不再被引用的对象,释放内存空间,这是 GC 最主要、最活跃的工作范围。
方法区 / 元空间的 GC
方法区(Java 8 前)或元空间(Java 8 及后)存储类元信息、常量池等数据,其 GC 并非主要目标,回收频率较低。当加载的类过多、常量池过大导致空间不足时,GC 会尝试回收无用的类(需满足类实例全被回收、类加载器被回收等严格条件)或未被引用的常量,以此释放空间,这一过程通常伴随 Full GC 发生。
直接内存的 GC
直接内存是通过ByteBuffer.allocateDirect()
分配的堆外内存,本身不由 JVM 直接管理,但它的引用对象(DirectByteBuffer
)存于堆中。当堆中的DirectByteBuffer
被回收时,关联的直接内存会通过虚引用触发的 “Cleaner 机制” 释放;若直接内存不足,可能间接触发 Full GC,通过回收堆内存间接释放对应的堆外空间。
选 CMS:
选 G1:
默认优先 G1,除非明确符合 CMS 场景。
维度 | CMS | G1 |
---|---|---|
算法基础 | 标记 - 清除 | 标记 - 整理 + 复制 |
内存布局 | 分代(新生代 + 老年代) | 分区(Region) |
STW 控制 | 依赖并发阶段减少 STW | 通过 Region 筛选和停顿预测模型 |
碎片问题 | 标记 - 清除导致内存碎片 | 标记 - 整理避免碎片 |
大内存处理 | 老年代连续空间,大对象分配易失败 | Region 分散存储,支持更大内存 |
Full GC 频率 | 高(并发失败或碎片导致) | 低(通过整理减少碎片) |
适用场景 | 中小堆内存(<4GB)、低延迟需求 | 大内存(>8GB)、混合负载 |
各自的GC流程
CMS:
G1:
G1 的特点:
G1最大的特点是引入分区的思路,弱化了分代的概念。
合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷
相比较 CMS 的改进:
算法: G1 基于标记–整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。
停顿时间可控: G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
**并行与并发:**G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间