标记 - 清除算法:
流程:分为 “标记” 和 “清除” 两阶段。先遍历内存标记所有需要回收的对象,再遍历内存回收所有标记对象。
优点:实现简单,无需移动对象;缺点:标记和清除效率低,回收后会产生大量不连续的内存碎片,可能导致后续大对象无法分配内存。
标记 - 复制算法:
流程:将内存划分为大小相等的两块(如 From 区和 To 区),每次仅使用其中一块。当使用的区域内存耗尽,将存活对象复制到另一块区域,再清空原区域内存。
优点:无内存碎片,对象分配时只需指针移动,效率高;缺点:内存利用率低(仅 50%),适合存活对象少的场景。
标记 - 整理算法:
流程:先标记所有存活对象,然后将存活对象向内存一端移动,使存活对象紧凑排列,最后清理内存边界外的所有回收对象。
优点:无内存碎片,内存利用率高;缺点:需移动存活对象,会增加系统开销,适合存活对象多的场景。
分代收集算法:
思想:根据对象存活周期将内存划分为不同代(新生代、老年代),针对不同代的特点采用不同回收算法。
新生代(存活周期短、存活对象少):用标记 - 复制算法;老年代(存活周期长、存活对象多):用标记 - 清除或标记 - 整理算法,兼顾效率和内存利用率。
定义:TLAB 是 JVM 为每个线程在新生代 Eden 区分配的一块私有内存缓冲区,专门用于线程私有对象的内存分配。
核心作用:减少多线程并发分配内存时的锁竞争,提升对象分配效率。多线程同时分配内存时,若直接在 Eden 区公共区域分配,需加锁保证线程安全;而线程优先在自己的 TLAB 分配,无需加锁,仅当 TLAB 不足时才竞争公共区域。
工作原理:
线程创建时,JVM 为其分配一块 TLAB(默认大小为 Eden 区的 1%,可通过 -XX:TLABSize 调整);
线程创建对象时,先在 TLAB 中分配内存,若 TLAB 剩余空间足够则直接分配,否则触发 “TLAB 补充” 或 “TLAB 扩容”;
当 TLAB 无法满足对象分配需求(如对象过大),线程会通过加锁在 Eden 区公共区域分配内存。
特点:默认开启(可通过 -XX:+UseTLAB 控制),仅用于新生代 Eden 区,不影响老年代内存分配。
Java 实现跨平台的核心是 “Java 虚拟机(JVM)”,通过 “一次编写,到处运行(Write Once, Run Anywhere)” 的设计理念,屏蔽不同操作系统的底层差异,具体实现流程如下:
源码编译为字节码:Java 源代码(.java 文件)通过 javac 编译器编译为与平台无关的二进制指令 —— 字节码(.class 文件)。字节码不依赖具体操作系统,仅面向 JVM 规范,是跨平台的基础。
JVM 解释 / 编译字节码:不同操作系统(如 Windows、Linux、macOS)需安装对应的 JVM 实现(如 HotSpot、OpenJ9)。JVM 负责将字节码转换为当前操作系统可执行的本地机器码:
解释执行:JVM 解释器逐行将字节码翻译为机器码,边解释边执行,启动快但执行效率低;
编译执行:JVM 的 JIT 编译器将频繁执行的 “热点代码” 一次性编译为机器码并缓存,后续直接执行机器码,提升执行效率。
JVM 屏蔽底层差异:JVM 封装了操作系统的内存管理、线程调度、文件操作等底层功能,Java 代码无需关注不同系统的 API 差异,只需通过 JVM 提供的统一接口调用,实现跨平台运行。
JVM 主要由 “类加载器、运行时数据区、执行引擎、本地方法接口” 四部分组成,各部分协同工作完成字节码的加载、执行和内存管理:
类加载器(Class Loader):
作用:负责将磁盘上的 .class 文件加载到 JVM 内存(方法区),并完成类的 “加载、链接、初始化” 三个阶段。
核心组件:Bootstrap ClassLoader(加载 JDK 核心类)、Extension ClassLoader(加载扩展类)、Application ClassLoader(加载应用类)、自定义类加载器(按需扩展)。
运行时数据区:
作用:存储 JVM 执行过程中产生的所有数据,分为线程共享区域和线程私有区域:
线程共享:堆(存储对象实例)、方法区(存储类信息、常量、静态变量);
线程私有:程序计数器(记录当前线程执行的字节码地址)、虚拟机栈(存储方法调用的栈帧)、本地方法栈(为 native 方法服务)。
执行引擎(Execution Engine):
作用:执行字节码指令,是 JVM 的核心组件,包含三个核心模块:
解释器:逐行解释字节码为机器码,启动快但效率低;
JIT 编译器:将热点代码编译为机器码缓存,提升执行效率;
垃圾回收器(GC):管理堆内存,自动回收无用对象,避免内存泄漏。
本地方法接口(JNI,Java Native Interface):
作用:作为 Java 代码与本地代码(如 C/C++ 代码)的桥梁,允许 Java 调用操作系统的 native 方法(如硬件操作、系统级 API),弥补 Java 对底层操作的不足。
(1)编译执行与解释执行的区别
(2)JVM 的执行方式
JVM 采用 “混合模式”(解释执行 + 编译执行),结合两种方式的优势:
初始阶段:JVM 启动时,解释器先逐行解释字节码执行,确保快速启动(如应用程序启动时无需等待编译完成);
运行阶段:JVM 通过 “热点探测”(如方法调用次数、循环执行次数)识别 “热点代码”,JIT 编译器将热点代码一次性编译为机器码并缓存到 “方法区” 或 “本地内存”;
后续执行:热点代码后续执行时,直接调用缓存的机器码,无需重新解释,提升执行效率。
补充:JVM 也支持纯解释执行(通过 -Xint 参数开启)或纯编译执行(通过 -Xcomp 参数开启),但默认混合模式兼顾启动速度和执行效率,是最优选择。
JVM 内存区域根据 “线程是否共享” 分为 “线程共享区域” 和 “线程私有区域”,具体划分如下:
线程共享区域(所有线程共用,生命周期与 JVM 一致):
堆(Heap):
作用:存储所有对象实例和数组,是 JVM 内存中最大的区域,也是垃圾回收器(GC)的主要工作区域;
细分:按对象存活周期分为新生代(Eden 区、From Survivor 区、To Survivor 区)和老年代,新生代默认占堆内存的 1/3,老年代占 2/3。
方法区(Method Area):
作用:存储类信息(类名、字段、方法)、常量、静态变量、即时编译后的机器码(JIT 缓存);
特点:JDK 8 前为 “永久代”(属于堆内存),JDK 8 后改为 “元空间”(使用本地内存),避免永久代内存溢出问题。
线程私有区域(每个线程独立拥有,生命周期与线程一致):
程序计数器(Program Counter Register):
作用:记录当前线程执行的字节码指令地址(如行号),线程切换时通过程序计数器恢复执行位置;
特点:内存占用小,无内存溢出(OOM)风险,是 JVM 规范中唯一没有规定 OOM 的区域。
虚拟机栈(VM Stack):
作用:存储方法调用时的 “栈帧”(包含局部变量表、操作数栈、方法出口等),方法调用时入栈,方法执行完出栈;
特点:栈深度固定(可通过 -Xss 调整),若方法嵌套调用过深(如递归无终止条件),会抛出 StackOverflowError;若栈内存动态扩展时无法申请到足够内存,会抛出 OutOfMemoryError。
本地方法栈(Native Method Stack):
作用:与虚拟机栈功能类似,专门为 native 方法(如 C/C++ 实现的方法)提供栈空间;
特点:部分 JVM 实现(如 HotSpot)将本地方法栈与虚拟机栈合并,异常类型与虚拟机栈一致。
会。方法区虽然存储的是类信息、常量等生命周期较长的数据,但当方法区无法容纳新的类信息或常量时,会抛出 java.lang.OutOfMemoryError: Metaspace(JDK 8+)或 java.lang.OutOfMemoryError: PermGen space(JDK 8 前),常见导致内存溢出的场景如下:
频繁动态生成类:
若通过 CGLib 动态代理、反射(如 Proxy.newProxyInstance)、字节码生成框架(如 ASM)频繁创建新类,且类加载器无法回收(如自定义类加载器未被 GC),会导致方法区类信息堆积,最终溢出;
示例:Spring 框架的 AOP 功能通过 CGLib 动态生成代理类,若频繁创建代理类且未合理管理,可能触发方法区溢出。
常量池过大:
若程序中定义大量字符串常量(如通过 String.intern() 强制将字符串加入常量池),或加载包含大量常量的类(如枚举类、配置类),会导致方法区常量池占满,引发溢出。
JVM 参数配置过小:
JDK 8+ 中,方法区(元空间)默认使用本地内存,大小受操作系统内存限制,但可通过 -XX:MetaspaceSize(初始大小)和 -XX:MaxMetaspaceSize(最大大小)限制;若 MaxMetaspaceSize 配置过小,且类信息增长超过限制,会触发溢出;
JDK 8 前,永久代(方法区)大小通过 -XX:PermSize 和 -XX:MaxPermSize 配置,若配置过小,同样会导致溢出。
类加载器泄漏:
若自定义类加载器加载类后,未被 GC 回收(如类加载器被静态变量引用),其加载的所有类也无法回收,导致方法区类信息堆积,最终溢出。
OOM(java.lang.OutOfMemoryError)是 JVM 内存不足时抛出的异常,根据内存区域不同,主要分为以下 5 种情况:
堆内存溢出(java.lang.OutOfMemoryError: Java heap space):
原因:堆内存中创建的对象过多,且垃圾回收器无法回收足够内存(如对象被长期引用,无法成为垃圾);
场景:频繁创建大对象(如大数组、大集合)、内存泄漏(如静态集合持有对象引用未释放)、堆内存配置过小(-Xms/-Xmx 配置不足)。
方法区 / 元空间溢出(java.lang.OutOfMemoryError: Metaspace / PermGen space):
原因:方法区无法容纳新的类信息、常量或 JIT 编译后的代码;
场景:频繁动态生成类(如 CGLib 代理)、常量池过大、元空间 / 永久代参数配置过小(-XX:MaxMetaspaceSize/-XX:MaxPermSize 不足)。
虚拟机栈 / 本地方法栈溢出:
① StackOverflowError:方法嵌套调用过深(如递归无终止条件),栈帧超出栈深度限制;
② OutOfMemoryError: Stack overflow:虚拟机栈动态扩展时无法申请到足够内存(如线程过多导致栈内存总占用超物理内存);
场景:递归调用层级过多、线程创建数量过多(每个线程占用独立栈空间)、栈内存配置过小(-Xss 不足)。
直接内存溢出(java.lang.OutOfMemoryError: Direct buffer memory):
原因:通过 ByteBuffer.allocateDirect() 申请的直接内存(堆外内存)超出限制,且无法回收;
场景:频繁申请大的直接内存(如 NIO 操作)、直接内存配置过小(-XX:MaxDirectMemorySize 不足)、直接内存未手动释放(虽有 GC 回收,但回收时机不确定)。
线程创建溢出(java.lang.OutOfMemoryError: Unable to create new native thread):
原因:创建的线程数量过多,超出操作系统对线程数量的限制(如 Linux 默认限制单个进程线程数为 1024),或系统剩余内存不足;
场景:循环创建大量线程(未使用线程池)、线程池参数配置不合理(核心线程数和最大线程数过大)。
Java 中的堆(Heap)和栈(Stack,指虚拟机栈)是 JVM 内存中两个核心区域,主要区别如下:
直接内存(Direct Memory)是 JVM 堆内存之外的操作系统本地内存,由操作系统直接管理,不属于 JVM 运行时数据区,但 Java 可通过 NIO 相关 API(如 java.nio.ByteBuffer)申请和使用。
核心特点:
1.脱离 JVM 内存管理:直接内存不由 JVM 垃圾回收器(GC)直接管理,但其 “引用对象”(如 DirectByteBuffer)存储在堆中,当引用对象被 GC 回收时,JVM 会通过。
适用场景:NIO 编程(如 Socket 通信、文件读写)、大数据处理(如缓存大文件数据)、高频 I/O 操作场景,需减少内存拷贝开销时优先使用。
注意事项:需避免频繁申请和释放直接内存(可能导致操作系统内存碎片),且需确保引用对象被正常 GC(否则直接内存无法释放,引发内存泄漏)。
Java 中的常量池是存储 “编译期生成的字面量和符号引用” 的内存区域,属于方法区的一部分,主要用于优化常量访问效率,分为以下两类:
Class 文件常量池(静态常量池):
定义:存储在 .class 文件中的常量池,包含两类数据:
字面量:如字符串常量(如 "hello")、基本类型常量(如 123、true)、final 修饰的常量字段值;
符号引用:如类和接口的全限定名(如 java/lang/String)、方法和字段的名称及描述符(如 add:(I)V)、接口方法的符号引用。
作用:编译期生成,为类加载时的 “解析” 阶段提供符号引用,后续通过解析将符号引用转换为 JVM 内存中的直接引用(如对象地址、方法入口)。
运行时常量池(动态常量池):
定义:Class 文件常量池加载到 JVM 方法区后形成的常量池,是 Class 文件常量池的运行时体现,支持动态添加常量。
核心特性:
动态性:除了加载 Class 文件时导入的常量,还可通过 String.intern() 方法将运行时生成的字符串动态加入常量池(如 new String("hello").intern(),若常量池无 "hello" 则添加,返回常量池中的引用);
内存位置:JDK 7 前存储在方法区(永久代),JDK 7 及以后移至堆内存(避免永久代内存溢出),JDK 8 后随方法区改为元空间,运行时常量池仍在堆内存。
核心作用:减少常量的重复存储,实现常量的共享访问(如多个 String s = "hello" 指向常量池中的同一个 "hello" 对象),节省内存空间。
Java 类加载器是 JVM 负责加载 .class 文件到内存的组件,通过 “双亲委派模型” 实现类的有序加载和隔离,确保类加载的安全性和唯一性,核心内容如下:
类加载器的核心作用:
加载:将磁盘上的 .class 文件(或网络、内存中的 .class 数据)读取到 JVM 内存(方法区),生成对应的 java.lang.Class 对象;
链接:分为验证(校验 .class 文件格式、安全性)、准备(为类静态变量分配内存并设置默认值,如 static int a 初始化为 0)、解析(将符号引用转换为直接引用)三个阶段;
初始化:执行类的静态代码块(static {})和静态变量的赋值语句(如 static int a = 10),初始化顺序按代码定义顺序执行。
常见的类加载器(按层级划分):
启动类加载器(Bootstrap ClassLoader):
位置:由 C/C++ 实现,属于 JVM 内核组件,无对应的 Java 类(无法通过 getClassLoader() 获取,返回 null);
作用:加载 JDK 核心类库(如 rt.jar、charsets.jar),加载路径由 sun.boot.class.path 系统属性指定(如 JRE/lib/rt.jar)。
扩展类加载器(Extension ClassLoader):
实现类:sun.misc.Launcher$ExtClassLoader;
作用:加载 JDK 扩展类库,加载路径由 java.ext.dirs 系统属性指定(如 JRE/lib/ext 目录下的 jar 包)。
应用程序类加载器(Application ClassLoader):
实现类:sun.misc.Launcher$AppClassLoader;
作用:加载应用程序的类(如项目 src/main/java 目录下的类、第三方依赖 jar 包中的类),加载路径由 java.class.path 系统属性指定(即 classpath),是默认的类加载器(通过 ClassLoader.getSystemClassLoader() 获取)。
自定义类加载器:
定义:继承 java.lang.ClassLoader 并重写 findClass() 方法(或 loadClass() 方法)的类加载器;
作用:实现自定义的类加载逻辑,如加载加密的 .class 文件、从网络加载 .class 文件、实现类的热部署(如 Tomcat 的 WebAppClassLoader)。
双亲委派模型:
核心规则:类加载器加载类时,先委托给父加载器加载,若父加载器无法加载(找不到类),再由当前加载器自行加载;
流程:应用程序类加载器 → 扩展类加载器 → 启动类加载器(顶层),若启动类加载器无法加载,再反向由子加载器尝试加载;
作用:① 避免类重复加载(如 java.lang.String 仅由启动类加载器加载,确保所有类使用同一个 String 类);② 防止核心类被篡改(如自定义 java.lang.String 无法被加载,避免恶意代码替换核心类)。
JIT(即时编译器)是 JVM 执行引擎的核心组件,用于将 “热点代码”(频繁执行的代码)从字节码编译为本地机器码,提升 Java 程序的执行效率,核心特性如下:
JIT 出现的背景:
Java 代码默认通过解释器逐行解释执行,启动快但执行效率低(尤其是频繁执行的代码,需重复解释);
JIT 编译器通过将热点代码编译为机器码,后续直接执行机器码,避免重复解释,弥补解释执行的效率缺陷。
热点代码的识别:
JVM 通过 “热点探测” 识别热点代码,常用探测方式有两种:
基于采样的探测:定期采样线程的执行栈,若某个方法频繁出现在栈顶,则标记为热点代码;
基于计数器的探测(JVM 主流方式):为每个方法设置 “方法调用计数器” 和 “循环回边计数器”,当计数器超过阈值(默认方法调用次数阈值为 10000 次,可通过 -XX:CompileThreshold 调整),则标记为热点代码。
JIT 编译的流程:
解释执行:JVM 启动后,解释器先解释执行字节码,同时记录代码执行次数;
热点识别:计数器超过阈值,代码被标记为热点代码,触发 JIT 编译;
编译优化:JIT 编译器对热点代码进行多层优化(如常量折叠、循环展开、方法内联 —— 将频繁调用的小方法嵌入调用者代码中,减少方法调用开销);
机器码缓存:编译生成的机器码缓存到内存(方法区或本地内存),后续执行该代码时,直接调用缓存的机器码,无需重新解释。
核心优势:
兼顾启动速度和执行效率:解释执行保证快速启动,JIT 编译提升热点代码执行效率;
动态优化:根据代码执行情况动态调整优化策略(如根据实际参数类型优化方法内联),比静态编译(如 C++ 编译)更灵活。
JIT 编译后的本地机器码存储位置随 JVM 版本和实现(如 HotSpot)的不同而变化,主要分为以下两种情况:
JDK 7 及之前(永久代时期):
存储位置:JIT 编译后的机器码存储在方法区的 “永久代” 中,与类信息、常量池等数据共存;
原因:永久代是 JDK 7 及之前方法区的实现,用于存储长期存活的元数据,JIT 编译后的机器码属于类的长期有效数据,因此存入永久代,便于管理和访问。
JDK 8 及之后(元空间时期):
存储位置:JIT 编译后的机器码存储在 “本地内存”(操作系统管理的内存,非 JVM 堆内存)中,而非元空间(元空间仅存储类元数据,如类结构、字段信息);
细分:HotSpot JVM 中,JIT 编译后的机器码存储在 “代码缓存(Code Cache)” 中,代码缓存是一块独立的本地内存区域,专门用于存储 JIT 编译后的机器码和 native 方法的机器码;
代码缓存的管理:
大小配置:默认大小由 JVM 自动调整,可通过 -XX:InitialCodeCacheSize(初始大小)和 -XX:ReservedCodeCacheSize(最大大小)手动配置;
回收机制:当代码缓存满时,JVM 会清理 “非热点代码” 的机器码(如长期未执行的代码),释放空间;若清理后仍不足,会停止 JIT 编译,后续代码仅通过解释执行。
核心目的:将 JIT 编译后的机器码与类元数据分离存储,避免元空间内存限制影响机器码存储,同时通过独立的代码缓存优化机器码的访问速度(本地内存访问无需经过 JVM 内存管理,效率更高)。
AOT(预编译)是 Java 9 引入的编译方式,与 JIT(即时编译)相对,指 “在程序运行前,将 Java 字节码一次性编译为本地机器码”,核心特性如下:
AOT 与 JIT 的核心区别:
AOT 编译的实现工具:
Java 9 及之后提供 jaotc 工具(Java Ahead-of-Time Compiler),支持将 .class 文件或 jar 包编译为本地机器码文件(如 Linux 下的 .so 文件、Windows 下的 .dll 文件);
示例:jaotc --output libHello.so Hello.class,将 Hello.class 编译为 libHello.so 机器码文件,运行时通过 -XX:AOTLibrary=./libHello.so 加载并执行。
AOT 的适用场景:
对启动速度要求高的场景:如命令行工具、微服务(需快速启动响应请求)、嵌入式设备(资源有限,无法承受 JIT 编译开销);
资源受限的环境:如内存小、CPU 性能弱的设备(JIT 编译需占用额外资源,AOT 无运行时编译开销);
无需动态优化的场景:如简单的工具类程序,代码执行逻辑固定,静态编译优化即可满足需求。
AOT 的局限性:
跨平台性差:需为不同操作系统和硬件架构编译不同的机器码文件,增加部署复杂度;
不支持动态特性:如反射、动态代理、动态类生成(这些特性依赖运行时字节码,AOT 静态编译无法处理);
编译时间长:预编译需在运行前完成,大型项目编译耗时较长,影响开发和部署效率。
逃逸分析是 JVM 的一种优化技术,用于分析 “对象的引用是否会逃离当前方法或线程”,若对象未逃逸,JVM 可对其进行针对性优化,减少内存开销和 GC 压力,核心内容如下:
逃逸状态的分类:
无逃逸(栈上分配):对象仅在当前方法内使用,引用未传出方法(如方法内创建的局部对象,仅在方法内调用其方法或访问字段);
方法逃逸:对象引用传出当前方法(如作为方法返回值返回、传入其他方法的参数);
线程逃逸:对象引用传出当前线程(如存入静态变量、共享集合,被其他线程访问)。
逃逸分析的优化手段:
① 栈上分配(Stack Allocation):
原理:若对象无逃逸,JVM 可将对象分配在当前方法的虚拟机栈帧中(而非堆内存),方法执行完后,栈帧出栈,对象随栈帧自动回收,无需 GC 处理;
作用:减少堆内存分配,降低 GC 频率和开销(堆对象需 GC 回收,栈对象自动回收)。
② 标量替换(Scalar Replacement):
原理:若对象无逃逸且可分解为基本类型(如 class Point { int x; int y; }),JVM 可将对象分解为独立的基本类型变量(x 和 y),直接存储在栈帧的局部变量表中,无需创建完整对象;
作用:避免对象创建的内存开销(如对象头、对齐填充的内存占用),同时减少内存访问开销(直接访问局部变量,无需通过对象引用)。
③ 同步消除(Synchronization Elimination):
原理:若对象无逃逸(仅当前线程访问),则对象的同步锁(如 synchronized (obj) {})失去意义,JVM 可移除同步锁,避免锁竞争的开销;
示例:方法内创建 Object obj = new Object(),并在方法内使用 synchronized (obj),JVM 可消除该同步块,直接执行内部代码。
逃逸分析的启用与特点:
启用:JDK 6 及之后默认开启逃逸分析(可通过 -XX:+DoEscapeAnalysis 开启,-XX:-DoEscapeAnalysis 关闭);
特点:逃逸分析是 JIT 编译阶段的优化(运行时动态分析),而非编译期优化,能根据代码实际执行情况精准判断对象逃逸状态;
注意:栈上分配和标量替换仅针对无逃逸对象,若对象发生逃逸,则仍按常规方式在堆内存分配。
Java 中的引用分为四种类型,按 “引用强度从强到弱” 排序为:强引用 > 软引用 > 弱引用 > 虚引用,每种引用对应不同的内存回收策略,用于灵活控制对象的生命周期,具体如下:
强引用(Strong Reference):
定义:最常见的引用类型,如 Object obj = new Object(),obj 是强引用;
特性:对象被强引用指向时,即使内存不足(OOM 边缘),GC 也不会回收该对象;
回收条件:仅当强引用被显式置为 null(如 obj = null),且对象无其他强引用时,GC 才可能回收;
适用场景:常规对象引用(如业务对象、工具类实例),需确保对象在使用期间不被回收。
软引用(Soft Reference):
定义:通过 java.lang.ref.SoftReference 类实现,如 SoftReference<Object> softRef = new SoftReference<>(new Object());
特性:对象仅被软引用指向时,GC 会在 “内存不足且即将发生 OOM” 时回收该对象;若内存充足,GC 不会回收;
辅助类:可结合 ReferenceQueue 使用,当软引用关联的对象被 GC 回收后,软引用会被加入 ReferenceQueue,便于后续清理软引用本身;
适用场景:内存敏感的缓存(如图片缓存、数据缓存),当内存不足时自动释放缓存,避免 OOM。
弱引用(Weak Reference):
定义:通过 java.lang.ref.WeakReference 类实现,如 WeakReference<Object> weakRef = new WeakReference<>(new Object());
特性:对象仅被弱引用指向时,无论内存是否充足,只要发生 GC,就会被回收(弱引用的生命周期比软引用更短);
辅助类:同样可结合 ReferenceQueue 监听对象回收状态;
适用场景:临时缓存、避免内存泄漏的关联场景(如 WeakHashMap,其 key 为弱引用,当 key 无其他强引用时,key 和对应的 value 会被 GC 回收,避免缓存长期占用内存)。
虚引用(Phantom Reference):
定义:通过 java.lang.ref.PhantomReference 类实现,如 PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
特性:
虚引用是最弱的引用,无法通过虚引用获取关联的对象(get() 方法始终返回 null);
仅用于 “监听对象被 GC 回收的事件”,当对象被 GC 回收后,虚引用会被加入关联的 ReferenceQueue;
回收条件:对象仅被虚引用指向时,GC 会直接回收,虚引用本身无阻止回收的作用;
适用场景:管理直接内存(堆外内存)的释放(如 NIO 中 DirectByteBuffer 依赖虚引用的 Cleaner 机制,在对象被 GC 后触发直接内存释放)、记录对象回收日志。
Java 中垃圾收集器(GC)是 JVM 用于回收堆内存的组件,不同收集器针对不同场景(如吞吐量、响应时间)设计,常见收集器及特点如下:
Serial 收集器(串行收集器):
类型:单线程收集器(GC 线程仅 1 个),新生代采用标记 - 复制算法,老年代采用标记 - 整理算法;
特点:实现简单,内存占用小,无线程切换开销;但 GC 时会暂停所有用户线程(“Stop The World”,STW),吞吐量和响应时间差;
适用场景:单 CPU 环境、客户端应用(如桌面程序)、内存较小的嵌入式设备(JVM 默认客户端模式下的新生代收集器)。
ParNew 收集器(并行新生代收集器):
类型:多线程收集器(GC 线程数与 CPU 核心数一致,可通过 -XX:ParallelGCThreads 调整),仅用于新生代,采用标记 - 复制算法;
特点:本质是 Serial 收集器的多线程版本,GC 时仍会 STW,但因多线程并行回收,STW 时间比 Serial 短;可与老年代的 CMS 收集器配合使用;
适用场景:多 CPU 环境的服务端应用(如 Web 服务),需提升新生代回收效率时。
Parallel Scavenge 收集器(并行吞吐量收集器):
类型:多线程收集器,用于新生代,采用标记 - 复制算法;
核心特点:以 “提升吞吐量” 为目标(吞吐量 = 用户线程执行时间 / (用户线程执行时间 + GC 时间)),支持通过参数控制吞吐量:
-XX:MaxGCPauseMillis:设置 GC 最大暂停时间(尽量满足,非绝对);
-XX:GCTimeRatio:设置 GC 时间占比(如 19 表示 GC 时间不超过总时间的 5%);
适用场景:对吞吐量要求高、对响应时间要求不严格的应用(如后台计算任务、数据处理服务)。
Serial Old 收集器(串行老年代收集器):
类型:单线程收集器,仅用于老年代,采用标记 - 整理算法;
特点:GC 时 STW 时间长,效率低;
适用场景:单 CPU 环境、客户端应用,或作为 CMS 收集器失败后的 “备用收集器”(当 CMS 出现 Concurrent Mode Failure 时,切换为 Serial Old 回收老年代)。
Parallel Old 收集器(并行老年代收集器):
类型:多线程收集器,仅用于老年代,采用标记 - 整理算法;
特点:Parallel Scavenge 收集器的老年代版本,支持多线程并行回收,以 “吞吐量” 为目标,可与 Parallel Scavenge 配合实现 “新生代 + 老年代” 全并行回收;
适用场景:多 CPU 环境、对吞吐量要求高的服务端应用(如大数据处理),替代 Serial Old 提升老年代回收效率。
CMS 收集器(并发标记清除收集器):
类型:多线程并发收集器,仅用于老年代,采用标记 - 清除算法;
核心流程(分为 4 个阶段,仅初始标记和重新标记阶段会 STW):
初始标记:快速标记 “GC Roots 直接关联的对象”,STW 时间短;
并发标记:与用户线程并行,遍历标记所有可达对象,无 STW;
重新标记:修正并发标记期间因用户线程操作导致的标记偏差,STW 时间短;
并发清除:与用户线程并行,回收标记的无用对象,无 STW;
特点:以 “低响应时间” 为目标,GC 时 STW 时间短,适合交互性强的应用;但存在内存碎片(标记 - 清除算法导致)、并发开销大(占用 CPU 资源)、“Concurrent Mode Failure” 风险(并发回收时老年代内存不足,需切换为 Serial Old 回收);
适用场景:对响应时间要求高的服务端应用(如 Web 应用、电商系统)。
G1 收集器(Garbage-First 收集器):
类型:多线程并发收集器,用于 “新生代 + 老年代” 全区域回收,采用 “标记 - 整理 + 标记 - 复制” 混合算法;
核心设计:将堆内存划分为多个大小相等的独立区域(Region),每个 Region 可动态标记为新生代(Eden、Survivor)或老年代;优先回收 “垃圾比例高” 的 Region,提升回收效率;
核心流程(类似 CMS,STW 时间可控):
初始标记、并发标记、最终标记(修正偏差)、筛选回收(按 Region 优先级回收,复制存活对象到新 Region);
特点:
支持 “可预测的 STW 时间”(通过 -XX:MaxGCPauseMillis 设置目标暂停时间,G1 会自动调整回收策略满足目标);
无内存碎片(筛选回收阶段采用复制算法,存活对象紧凑排列);
兼顾吞吐量和响应时间,替代 CMS 成为 JDK 9 及以后的默认收集器;
适用场景:大堆内存应用(如堆内存 4GB 以上)、对响应时间和吞吐量均有要求的服务端应用(如大型分布式系统、中间件)。
ZGC 收集器(Z Garbage Collector):
类型:JDK 11 引入的低延迟收集器,用于 “新生代 + 老年代” 全区域回收,采用标记 - 复制算法;
核心特点:
极低的 STW 时间(毫秒级,甚至微秒级),支持 TB 级堆内存;
采用 “着色指针” 和 “读屏障” 技术,实现并发标记和并发移动对象,几乎无 STW;
兼顾低延迟和高吞吐量,适合超大规模内存、低延迟要求的应用(如金融交易系统、实时计算系统);
适用场景:JDK 11+、大内存低延迟场景,是 G1 收集器的进一步优化。
Java 中判断对象是否为垃圾(即 “无用对象”,需被 GC 回收)的核心是 “判断对象是否可达”,常见实现方式有 “引用计数法” 和 “可达性分析算法” 两种:
(1)引用计数法(Reference Counting)
核心原理:为每个对象维护一个 “引用计数器”,记录当前对象被引用的次数:
当对象被新引用指向时,计数器加 1(如 Object obj = new Object(),新对象计数器为 1);
当引用失效时(如 obj = null),计数器减 1;
当计数器值为 0 时,判定为垃圾对象,可被 GC 回收。
优点:实现简单,判断效率高,无需遍历对象图;
缺点:
无法解决 “循环引用” 问题(如 A objA = new A(); B objB = new B(); objA.b = objB; objB.a = objA; objA = null; objB = null,此时 A 和 B 的计数器均为 1,无法回收,但二者已无外部引用,属于垃圾);
每次引用增减都需更新计数器,存在额外性能开销(尤其是频繁引用操作的场景);
应用:早期 JVM(如 JDK 1.0 前)曾考虑使用,现主流 JVM(如 HotSpot)均未采用。
(2)可达性分析算法(Reachability Analysis)
核心原理:以 “GC Roots” 为起点,遍历对象引用图,若对象能通过 GC Roots 直接或间接访问(即 “可达”),则判定为有用对象;若 “不可达”,则判定为垃圾对象;
GC Roots 的常见类型:
虚拟机栈中局部变量表引用的对象(如当前正在执行的方法中的局部变量);
本地方法栈中 native 方法引用的对象;
方法区中类静态变量引用的对象(如 static Object obj = new Object());
方法区中常量引用的对象(如 final Object obj = new Object());
活跃线程引用的对象(如线程对象、线程持有的锁对象);
优点:
可解决循环引用问题(循环引用的对象若无法通过 GC Roots 可达,仍会被判定为垃圾);
判定逻辑更精准,是主流 JVM 的首选方案;
缺点:
需遍历整个对象引用图,GC 时会产生一定开销;
为避免遍历过程中对象引用关系变化(导致判定错误),需暂停所有用户线程(STW),但主流 JVM(如 G1、ZGC)通过优化已大幅缩短 STW 时间;
应用:HotSpot、OpenJ9 等主流 JVM 均采用此算法,是当前 Java 中判断垃圾的标准方式。
(3)两种方式的核心区别:

Java 垃圾收集器将堆分为老年代和新生代,核心依据是 “对象存活周期假说”(即 “大多数对象存活周期短,少数对象存活周期长”),通过 “分代收集” 策略优化 GC 性能,具体原因如下:
(1)基于对象存活周期的差异化优化
新生代(Young Generation):
对象特点:存储新创建的对象,大多数对象存活周期短(如方法内的局部对象、临时变量),创建后很快成为垃圾;
回收算法:采用 “标记 - 复制算法”,该算法在 “存活对象少” 的场景下效率极高(只需复制少量存活对象,无需遍历大量垃圾对象);
内存划分:进一步分为 Eden 区(对象创建的主要区域)和两个大小相等的 Survivor 区(From Survivor、To Survivor),对象在 Eden 区创建,GC 时存活对象复制到 Survivor 区,多次存活后进入老年代;
回收频率:因对象淘汰快,新生代 GC(Minor GC)频率高,但每次回收时间短(存活对象少,复制开销小)。
老年代(Old Generation):
对象特点:存储存活周期长的对象(如静态对象、缓存对象),存活对象占比高(多数对象长期有用,垃圾比例低);
回收算法:采用 “标记 - 清除” 或 “标记 - 整理” 算法,这两种算法在 “存活对象多” 的场景下更高效(无需复制大量存活对象,仅需标记和清理垃圾);
回收频率:因对象存活久,老年代 GC(Major GC/Full GC)频率低,但每次回收时间长(需处理大量存活对象,标记和整理开销大)。
通过分代,GC 可针对不同区域的对象特点选择最优算法,避免对整个堆采用单一算法导致的效率低下(如对全堆用标记 - 复制算法,会因老年代存活对象多导致复制开销过大;用标记 - 清除算法,会因新生代垃圾多导致标记开销过大)。
(2)减少 GC 范围,降低 STW 影响
若堆不分区,每次 GC 需遍历整个堆内存的所有对象,遍历范围大,STW 时间长,严重影响应用响应时间;
分代后,大多数 GC 仅针对新生代(Minor GC),遍历范围小(仅新生代区域),STW 时间短(通常毫秒级),对应用影响小;
老年代 GC(Major GC)虽时间长,但频率低(如几小时甚至几天一次),整体对应用性能的影响可控。
(3)提升内存利用率,减少内存碎片
新生代的标记 - 复制算法虽内存利用率低(需预留一个 Survivor 区),但因新生代垃圾回收频繁,预留区域的闲置时间短,整体内存利用率仍可控;
老年代的标记 - 整理算法可避免内存碎片(存活对象紧凑排列),确保大对象能顺利分配内存(若老年代用标记 - 清除算法产生碎片,大对象可能因无连续内存无法分配,触发 Full GC);
分代后,不同区域的内存管理策略互补,既保证新生代的回收效率,又保证老年代的内存利用率和大对象分配能力。
(4)符合实际应用场景的性能需求
实际 Java 应用中,“短存活对象多、长存活对象少” 的特点极为明显(如 Web 应用中,每次请求创建的临时对象很快失效,而应用配置、缓存等对象长期存活);
分代收集策略精准匹配这一特点,通过高频、快速的新生代 GC 处理短存活对象,低频、高效的老年代 GC 处理长存活对象,兼顾应用的响应时间和吞吐量,成为 Java GC 性能优化的核心设计。