目录
2)被判定为不可达的对象,通过finalize方法有一次自救的机会
3. G1(Garbage-First)(JDK9开始默认)
一、JVM内存管理
JVM内存分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分
其中,程序计数器记录线程执行位置,无内存溢出风险;本地方法栈是为虚拟机使用到的native方法使用。这两部分暂不讨论。
1.1 方法区(元空间)
方法区的作用,主要是存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
在JDK7及之前,方法区通过永久代实现,与堆内存物理连接,这样实现的问题在于固定大小,会导致永久代内存有溢出风险(比如加载过多类或动态代理类)
JDK7改进了一些,将字符串常量池和静态变量迁移到堆,避免永久代内存不足触发FullGC
JDK8及之后,使用本地内存来存储,独立于堆,改名为元空间,仅存储类元数据和运行时常量池,这样可以自动扩展,默认无上限(也可以参数设置),减少GC压力,且直接内存访问,减少数据复制
字符串常量池和运行时常量池的区别:
字符串常量池:仅存储字符串对象的引用,全局共享
运行时常量池:包含编译期生成的 字面量(如数值常量、字符串符号引用)和 符号引用(如类全限定名、方法签名),类级独立
1.2 虚拟机栈
虚拟机栈的作用,是Java方法执行的内存模型,存储局部变量表、操作栈、动态链接、方法出口等信息,默认大小是1MB(可以参数调整)
1.3 堆
堆的作用,是存储对象实例和数组,所有线程共享,是GC的主要区域
内存分布
因为几乎98%的对象生命周期都很短,创建出来就会被销毁,而熬过越多次垃圾收集过程的对象就越难以消亡,所以堆内存分为年轻代和老年代
年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)
年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2,Edem : from : to = 8 : 1 : 1
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间
对象分配的过程
1.new的对象先放在Eden区。该区域有大小限制
2.当Eden区域填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden预期进行垃圾回收(Minor GC),将Eden区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到Eden区
3.然后将Eden区中的剩余对象移动到幸存者0区
4.如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区
5.如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区。
6.如果累计次数到达默认的15次,这会进入年老区。可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
7.年老区内存不足是,会再次出发GC:Major GC 进行年老区的内存清理
8.如果年老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常.
二、JVM类加载机制
2.1 类加载过程
目的:通过ClassLoader(类加载器)将class文件从硬盘加载到方法区
类加载执行过程:
1> 加载(Loading):
两种触发加载的时机:
1)预加载:虚拟机启动时加载(加载JAVA_HOME/lib/rt.jar,主要是java.lang.*、java.util.、java.io. 等等)
2)运行时加载:第一次主动使用类时加载
两种类型的加载器:
1)引导类加载器:由c/c++实现
BootStrapClassLoader:启动类加载器,用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。并不继承自java.lang.ClassLoader,没有父加载器
2)自定义加载器:由java实现,派生于抽象类ClassLoader的类加载器
Extension Class Loader:扩展类加载器,从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader。父类加载器为启动类加载器
System Class Loader:系统类加载器,该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库;派生于 ClassLoader。父类加载器为扩展类加载器。通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器
User-Defined ClassLoader: 自定义类加载器,自己定制类的加载方式
2> 验证(Veritication):
确保.class文件的字节流中包含的信息符合当前虚拟机的要求:
文件格式验证
元数据验证
字节码验证
符号引用验证
3> 准备(Preparation)
为类变量(被static修饰的变量)分配内存并设置其初始值(cinit)的阶段,内存在方法区中分配
cinit是赋初始值,比如int的0,而不是程序员定义的值
4> 解析(Resolution)
负责把整个类激活,串成一个可以找到彼此的网,将常量池内的符号引用替换为直接引用的过程
符号引用:是对于类、变量、方法的描述。引用的目标未必已经加载到内存中了
直接引用:是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。引用的目标必定已经存在在内存中了
5> 初始化
是执行类构造器()方法的过程,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的 语句合并产生的
执行顺序:
父静态代码块
子静态代码块
父构造方法
子构造方法
Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步, 如果多个线程同 时去初始化一个类, 那么只会有其中一个线程去执行这个类的()方法, 其他线程都需要阻塞等 待, 直到活动线程执行完毕()方法。 如果在一个类的()方法中有耗时很长的操作, 那就 可能造成多个进程阻塞, 在实际应用中这种阻塞往往是很隐蔽的
2.2 双亲委派模型
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载.
优点:避免类的重复加载;保证核心类库的安全性
打破双清委派的场景
1)SPI服务加载(如JDBC、JNDI)
比如JDBC的DriverManager由启动类加载器加载,但其实现类(如MySQL驱动)位于应用类路径,父加载器无法加载子类路径中的实现类,需通过逆向委托绕过层级限制
2)应用隔离(如Tomcat多Web应用部署)
比如Tomcat同时运行多个Web应用,不同应用可能依赖同一第三方库的不同版本,双亲委派会导致同一类名被不同版本的库覆盖,破坏应用间的隔离性,需通过自定义加载器确保各应用类库独立
3)热部署
比如热部署要求动态更新类,但是双亲委派的父类加载器(应用类加载器)一旦加载某个类,后续所有子加载器的请求都会直接返回已缓存的Class对象,无法加载新版本,需自定义加载器绕过双亲委派的层级限制优先自行加载
2.3 即时编译
JVM的即时编译(JIT,Just-In-Time Compilation)是一种动态编译技术,其核心目标是通过将热点字节码实时转换为本地机器码
出现原因:
尽管类加载子系统已通过解释器将字节码加载到方法区,但解释执行的效率较低。解释器逐行解释字节码,每次执行相同代码都需重复解释过程,而JIT将高频代码(如循环、频繁调用的方法)编译为本地机器码,直接执行,跳过解释步骤,性能提升可达数十倍
JIT编译后的本地机器码存储在 Code Cache(默认240MB,可以用参数配置),若空间不足会触发编译停止,导致性能回退至解释执行,
三、垃圾回收机制及算法
垃圾回收器的工作流程大体如下:
1. 标记出哪些对象是存活的,哪些是垃圾(可回收);
2. 进行回收(清除/复制/整理),如果有移动过对象(复制/整理),还需要更新引用。
3.1 判定对象是否存活
1)通过可达性算法对存活的对象进行标记
基本思路:通过一系列的“GC Roots”的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可达”,即该对象是不可用的
GC Roots的对象包括以下几种:
栈帧中的局部变量表中的reference引用所引用的对象
方法区中static静态引用的对象
方法区中final常量引用的对象
本地方法栈中JNI(Native方法)引用的对象
Java虚拟机内部的引用, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError) 等, 还有系统类加载器。
所有被同步锁(synchronized关键字) 持有的对象。反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等
并发可达性分析算法:三色标记法
原理:通过白(未标记)、灰(标记中)、黑(已标记)三色状态实现并发标记,解决传统标记-清除算法的STW(Stop-The-World)问题
白色:尚未访问过。
黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
灰色:本对象已访问过,但是本对象 引用到 的其他对象尚未全部访问完。全部访问后,会转换为黑色
访问过程:
1. 初始时,所有对象都在 【白色集合】中;
2. 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
3. 从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。
4. 重复步骤3,直至【灰色集合】为空时结束。
5. 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。
问题解决:
1> 多标:假设已经遍历到对象(变为灰色了),此时应用执行了 obj= null;这个对象应该被回收,但是因为已经成为灰色了,会被当做存活对象继续遍历下去,本轮GC不会回收,这部分被称为浮动垃圾
2> 漏标:灰色对象断开了白色对象的引用,且黑色对象重新引用了该白色对象,会导致白色对象漏标,针对这种情况,将白色对象放到一个特定的集合,等GCRoot遍历完,再遍历该集合即可
为了赋予开发者更精细的内存控制能力,JVM设置了四种引用
⑴强引用(StrongReference):垃圾回收器绝不会回收它,内存不足会直接抛出OOM
⑵软引用(SoxReference):允许对象在内存紧张时被回收,适合缓存场景(如浏览器后退页面缓存、图片缓存),避免内存溢出(OOM)
软引用在内存不足时触发二次回收,避免程序崩溃
⑶弱引用(WeakReference):无论内存是否充足,一旦发生GC即被回收,常用于短期缓存(如ThreadLocal、WeakHashMap)
弱引用自动释放无关联对象,减少内存泄漏风险
⑷虚引用(PhantomReference):在任何时候都可能被垃圾回收器回收。仅用于跟踪对象回收状态,无法通过其获取对象,常用于资源清理或堆外内存管理(如NIO的DirectBuffer)
2)被判定为不可达的对象,通过finalize方法有一次自救的机会
假如对象有finalize()方法且没有被调用过,那么该对象将会被放置在一个名为F-Queue的 队列之中, 并在稍后由一条由虚拟机自动建立的、 低调度优先级的Finalizer线程去执行它们的finalize() 方法。 finalize()方法是对象逃脱死亡命运的最后一次机会, 稍后收集器将对F-Queue中的对象进行第二次小规模的标记, 如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可, 譬如把自己(this关键字) 赋值给某个类变量或者对象的成员变量, 那在第二次标记时它将被移出“即将回收”的集合; 如果对象这时候还没有逃脱, 那基本上它就真的要被回收了。
假如对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过,则将会被回收
3.2 垃圾回收算法
常见垃圾回收算法有以下三种:
标记-清除算法:先标记存活对象,再清除未标记对象
缺点:内存碎片化,影响大对象分配,提高GC频次
标记-复制算法:分为两个区,每次只使用一块儿,当用完后,标记存活对象,将存活的对象复制到另一块,然后清空当前块,如此轮流使用
优点:无碎片化,回收高效
缺点:内存利用率仅50%,总体GC更频繁
适合场景:大部分对象朝生夕灭(比如新生代,98%对象熬不过第一轮收集)
标记-整理算法:标记存活对象,让所有存活对象都向内存空间的一端移动,然后清理掉边界以外的内存
优点:避免碎片
缺点:对象移动开销大
适合场景:对象存活数量比较多(比如老年代,从垃圾收集的停顿时间来看, 不移动对象停顿时间会更短, 甚至可以不需要停顿, 但是从整个程序的吞吐量来看, 移动对象会更划算)
3.3 堆GC
GC分为两种:一种是部分收集器(Partial GC)另一类是整堆收集器(Full GC)
部分收集器(Partial GC):指目标不是完整收集整个Java堆的垃圾收集, 其中又分为:
- 新生代收集(Minor GC/Young GC): 指目标只是新生代的垃圾收集。
触发条件:Eden区空间不足:当新对象无法在Eden区分配时触发;
Survivor区空间不足:存活对象无法被Survivor区容纳时,可能直接晋升到老年代或触发Minor GC
- 老年代收集(Major GC/Old GC): 指目标只是老年代的垃圾收集,目前只有CMS收集器会有单独收集老年代的行为。
- 混合收集(Mixed GC): 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
触发条件:年代空间不足:当老年代无法容纳晋升的对象或大对象时触发;
永久代/元空间不足(Java 8前为永久代,Java 8+为元空间):类元数据或常量池占用过高时触发
显式调用:通过System.gc()或Runtime.getRuntime().gc()手动触发
悲观策略:JVM预测下次Minor GC后晋升对象可能导致老年代空间不足,提前触发Full GC
3.4 常见垃圾回收器及对应场景
1. Serial/Serial Old
场景:单线程环境(如客户端应用、嵌入式系统);资源受限的小型应用(堆内存较小,通常几十 MB 到几百 MB)。
特点:单线程工作;回收时暂停所有应用线程
回收过程:
年轻代:使用 复制算法(Copying),将存活对象从 Eden 区复制到 Survivor 区。
老年代:使用 标记-整理算法(Mark-Compact),标记存活对象后整理内存。
2. ParNew/CMS(JDK9弃用)
应用场景:对延迟敏感的应用(如 Web 服务、实时系统);中大型堆内存(4GB 以上),需尽量减少 STW 时间。
特点:ParNew是Serial的多线程并行版本
CMS并发标记,注重低延迟-最短停顿时间为目标(仅初始标记和重新标记阶段停顿),有空间碎片(标记-清除算法)
CMS垃圾收集过程:
1)初始标记(Initial-Mark)阶段:这个阶段程序所有的工作线程都将会因为"Stop-the-Wold"机制而出现短暂的的暂停,这个阶段的主要任务标记处GC Roots 能够关联到的对象.一旦标记完成后就恢复之前被暂停的的所有应用。 由于直接关联对象比较小,所以这里的操作速度非常快。
2)并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要暂停用户线程, 用户线程可以与垃圾回收器一起运行。
3)重新标记(Remark)阶段:由于并发标记阶段,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此,为了修正并发标记期间因为用户继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常比初始标记阶段长一些,但也远比并发标记阶段时间短。
4)清除并发(Concurrent-Sweep)阶段: 此阶段清理删除掉标记判断已经死亡的对象,并释放内存空间。由于不需要移动存活对象,所以这个阶段可以与用户线程同时并发运行。
其中 初始标记 和 重新标记 都需要stopTheWorld(三色标记法),由于最消耗事件的并发标记与并发清除阶段都不需要暂停工作,因为整个回收阶段是低停顿(低延迟)的。
3. G1(Garbage-First)(JDK9开始默认)
场景:大堆内存(4GB~数十 GB),平衡吞吐量和延迟;需要可预测的停顿时间(如 -XX:MaxGCPauseMillis)
特点:
分 Region 管理:将堆划分为多个 Region(默认 2048 个),独立回收。
混合回收:支持同时回收年轻代和老年代。
软实时性:通过预测模型尽量满足最大停顿时间目标。
G1垃圾回收过程:
Young GC:通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销
* 触发条件:当Eden区占满时触发。
* 过程:
1. 根扫描:从GC Roots(栈、寄存器、全局变量等)出发,标记直接关联的存活对象。
2. 复制存活对象:将Eden和Survivor区的存活对象复制到新的Survivor区,年龄达到阈值(默认15)的对象晋升到Old区。
3. Region回收:清空原Eden和Survivor区,变为空白Region。
* 特点:需暂停应用线程(Stop-The-World, STW),但时间较短。
并发标记周期(Concurrent Marking Cycle):为确定老年代Region的回收价值,G1周期性地执行并发标记
1. 初始标记(Initial Mark)
* 触发:伴随一次Young GC,利用Young GC的STW阶段完成。
* 任务:标记GC Roots直接可达的对象(仅需扫描根)。
2. 根区域扫描(Root Region Scanning)
* 任务:扫描Survivor区中引用老年代的对象,确保在并发标记时这些引用被正确处理。
* 要求:必须在下次Young GC前完成。
3. 并发标记(Concurrent Marking)
* 任务:遍历堆,标记所有存活对象(与应用线程并发执行)。
* 机制:使用SATB(Snapshot-At-The-Beginning)算法,记录标记开始时的对象快照,通过写屏障跟踪并发期间的引用变化。
4. 重新标记(Remark)
* STW暂停:处理并发阶段遗漏的引用变化(如SATB日志中的对象)。
* 任务:修正标记结果,确保准确性。
5. 清理(Cleanup)
* 任务:
* 统计各Region的存活对象比例。
* 回收完全空闲的Region。
* 生成回收集(Collection Set)供Mixed GC使用。
* 可能触发STW:部分操作需短暂暂停。
Mixed GC:在用户指定的开销目标范围内尽可能选择收益高的老年代Region
触发条件:并发标记周期完成后,老年代Region的垃圾比例达到阈值(默认45%)
过程:
1. 同时回收年轻代(Eden/Survivor)和部分老年代Region(包含最多垃圾的Region)。
2. 采用复制算法,将存活对象复制到空闲Region。
退化场景:Full GC
* 触发条件:当并发标记速度跟不上对象分配速度,或Mixed GC无法释放足够内存时。
* 过程:退化为单线程的Serial Old GC,全堆回收,导致长时间STW。
* 规避:优化堆大小、调整-XX:InitiatingHeapOccupancyPercent(触发并发标记的老年代占比阈值,默认45%)。
G1通过分Region管理、并发标记、混合回收和RSet机制,实现了可控的停顿时间与高效回收。其核心优势在于:
* 优先回收高垃圾Region(Garbage-First)。
* 并发阶段减少STW时间。
* 适应大内存场景,平衡吞吐量与延迟。
4. ZGC(JDK11+引入)
应用场景:超大堆内存(TB 级别),要求极低延迟(<10ms);云原生、实时交易等场景(如金融系统)
特点:
全并发:所有阶段(标记、转移、重定位)几乎不 STW。
基于 Region:动态调整 Region 大小(2MB~32MB)。
指针染色技术:使用元数据位记录对象状态,减少内存占用。
回收过程:
并发标记:遍历对象图,记录存活对象。
并发预备重定位:确定待清理的 Region。
并发重定位:将存活对象复制到新 Region,并更新引用。
5、Shenandoah(JDK 12+引入)
应用场景:类似 ZGC,但更早支持低延迟。社区支持较少
特点:
并发压缩:在对象移动时,应用线程仍可访问旧地址。
低延迟:设计目标与 ZGC 相似,但实现方式不同(如读屏障机制)。
回收过程:
并发标记:识别存活对象。
并发压缩:移动对象并修复引用,无需长时间 STW。