JVM 基础
内存模型
JVM 内存模型是什么?
(1)JVM 内存模型共分为5个区:Java虚拟机栈、本地方法栈、堆、程序计数器、方法区(元空间)
(2)各个区各自的作用:
a.程序计数器:它是一块很小的内存空间,主要用来记录当前线程执行的字节码的地址,也就是记录当前线程执行的位置。 是虚拟机中没有规定会OutofMemoryError的区域。
b.Java 虚拟机栈:每个方法执行时会在栈中创建一个栈帧,执行完成被销毁,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。(栈里面存的是地址引用,实际指向的是堆里面的对象)
c.本地方法栈:与Java虚拟机栈类似,用于管理本地方法的调用。
d.方法区(Java8叫元空间):用于存放已被虚拟机加载的类信息,常量,静态变量等全局数据信息。
e.堆:Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
(3)线程私有、公有
a.线程私有:每个线程在开辟、运行的过程中会单独创建这样的一份内存,有多少个线程可能有多少个内存 Java虚拟机栈、本地方法栈、程序计数器是线程私有的
b.线程全局共享的 堆和方法区
(4)栈虽然方法运行完毕了之后被清空了,但是堆上面的还没有被清空,所以引出了GC(垃圾回收),不能立马删除,因为不知道是否还有其它的也是引用了当前的地址来访问的
强引用、软引用、弱引用、虚引用
- 强引用
- 实现: 最常见的引用,当我们新建一个对象时就创建了一个具有强引用的对象 例如
Object obj = new Object(); - 回收时机: 只要一个对象具有强引用,垃圾收集器就不会回收它,即使抛出
OutOfMemoryError也不会回收 - 用途: 绝大多数普通对象的引用
- 软引用
- 实现:
java.lang.ref.SoftReference - 回收时机: 一个对象只具有软引用,当内存不够的时候,就会回收这些对象的内存,回收后内存依然不够就会抛出
OutOfMemoryError - 用途: 实现内存敏感的缓存(如图片缓存、大量数据缓存),既能利用缓存提升性能,又能在内存紧张时自动释放。
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get(); // 可能返回 null(若已被回收)
- 弱引用
- 实现:
java.lang.ref.WeakReference - 回收时机: 比软引用具有更短的生命周期,在垃圾回收时,垃圾收集器一旦发现只具有弱引用的对象,不管当前内存是否足够,都会回收它的内存。
- 用途: 弱引用通常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get(); // 下次 GC 后极可能返回 null
- 虚引用
- 实现:
java.lang.ref.PhantomReference - 回收时机: 是最弱的引用类型,如果一个对象仅持有虚引用,这就和没有任何引用一样,在任何时候都可能被垃圾收集器回收。
但在对象被回收前,虚引用会被放入一个队列之中,供程序员进行处理。 - 用途: 虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// phantomRef.get() 永远为 null
// 当对象被回收后,phantomRef 会被加入 queue,可以从中取出并执行清理
可能追问的问题
-
什么是引用队列(
ReferenceQueue)?- 软、弱、虚引用可以关联一个
ReferenceQueue。当引用的对象被回收后,引用本身会被 JVM 添加到该队列,允许我们执行后续清理(例如从缓存中移除无效的键值对)。
- 软、弱、虚引用可以关联一个
类加载机制
JVM 类加载的生命周期包括加载、连接、初始化、使用和卸载,其中连接又分为验证、准备和解析。
加载阶段
- 类加载器根据类的全限定名获取字节码加载到内存
- 将其转换为 JVM 方法区中的运行时数据结构
- 并在堆中生成对应的 Class 对象。
验证阶段检查 Class 文件格式、元数据和字节码是否合法。防止恶意或者错误的字节码危害 JVM。
准备阶段为 static 类变量分配内存并赋默认值,例如 static int age = 18,准备阶段通常先赋值为 0,真正赋值为 18 要等到初始化阶段。编译期可确定的 static final 常量比较特殊,可能直接赋常量值。
解析阶段把常量池中的符号引用转换成直接引用。
初始化阶段
- 执行编译器生成的 类构造器
<clinit>方法,主要包括静态变量的显式赋值和静态代码块。
(类构造器是构造类信息的,不是构造该类对象的构造器,它会按顺序收集类中所有类变量的赋值动作,和静态代码块中的语句,然后合并产生) - 初始化子类前,JVM会先初始化父类,而且同一个类在一个类加载器下通常只初始化一次。
- 虚拟机会保证一个类的
<clinit>方法在多线程环境中被正确加锁和同步(非重点)
例如执行 new Child() 时,JVM会先完成 Child 的加载、验证、准备和解析,然后先初始化 Parent,再初始化 Child,最后才创建对象并调用 Child 的构造方法。
什么时候会发生类初始化(加载、链接、初始化)
类的主动引用(一定会发生类的加载、链接、初始化)
- 虚拟机启动时,会先初始化main方法所在的类
- new 对象
- 调用类的静态成员(除了final常量)和静态方法
- 用反射
- 初始化子类时,父类会先被加载
类的被动引用(可能会加载,但不会触发初始化)
- 访问 final 常量
System.out.println(MyClass.CONST);
如果是:
static final int CONST = 10;
不会触发类初始化(可能连加载都不会发生)
2. 通过数组定义类引用,不会触发此类的初始化
MyClass[] arr = new MyClass[10];
只会加载 [LMyClass; 数组类,不会初始化 MyClass
3. 引用类但不使用
MyClass m;
不会触发加载
final 常量什么时候初始化
编译器常量
满足以下条件的 static final 变量:
- 基本类型 / String
- 直接赋值常量表达式 即字面量
- 不需要运行期计算(调用方法、new对象 就是运行期计算)
static final int A = 10;
static final String S = "hello";
在编译期就确定值 即.java 编译为.class文件时就确认
运行时不参与初始化
运行期final常量
不是编译期常量,例如
static final int A = new Random().nextInt();
static final Integer B = Integer.valueOf(10);
static final String S = new String("hello");
- 准备阶段赋予默认值
- 类初始化阶段(Initialization)执行
<clinit>方法时初始化
什么是Java里的垃圾回收?如何触发垃圾回收?
垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:
- 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
- 手动请求:虽然垃圾回收是自动的,开发者可以通过调用
System.gc()或Runtime.getRuntime().gc()建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。 - JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:
-Xmx(最大堆大小)、-Xms(初始堆大小)等。 - 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。
内存泄漏: 内存泄漏是指在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致内存组件减少的现象。虽然在Java中,垃圾回收机制会自动回收不再使用的对象,但如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,最终可能导致程序的内存使用不断增加。
内存溢出: 程序向虚拟机申请内存的时候,虚拟机剩余内存不够满足申请需求的时候,导致抛出异常或程序崩溃。
判断垃圾的方法有哪些?
在 Java 中,判断对象是否为垃圾(即不再被使用,可以被垃圾回收器回收)主要依据两种主流的垃圾回收算法来实现:引用计数法和可达性分析算法。
引用计数法(Reference Counting)
- 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加 1;当引用失效时,计数器减 1。当计数器为 0 时,表示对象不再被任何变量引用,可以被回收。
- 缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为 0,导致对象无法被回收。
可达性分析算法(Reachability Analysis)

Java 虚拟机主要采用此算法来判断对象是否为垃圾。
- 原理:从一组称为 GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。GC Roots 对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中 JNI(Java Native Interface)引用的对象、活跃线程的引用等。
垃圾回收算法是什么,是为了解决了什么问题?
JVM有垃圾回收机制的原因是为了解决内存管理的问题。在传统的编程语言中,开发人员需要手动分配和释放内存,这可能导致内存泄漏、内存溢出等问题。而Java作为一种高级语言,旨在提供更简单、更安全的编程环境,因此引入了垃圾回收机制来自动管理内存。
垃圾回收机制的主要目标是自动检测和回收不再使用的对象,从而释放它们所占用的内存空间。这样可以避免内存泄漏(一些对象被分配了内存却无法被释放,导致内存资源的浪费)。同时,垃圾回收机制还可以防止内存溢出(即程序需要的内存超过了可用内存的情况)。
通过垃圾回收机制,JVM可以在程序运行时自动识别和清理不再使用的对象,使得开发人员无需手动管理内存。这样可以提高开发效率、减少错误,并且使程序更加可靠和稳定。
垃圾回收算法有哪些?
- 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
- 复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。
- 标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都前移。移动结束后直接清理掉剩余部分。这样使得内存紧凑,避免了内存碎片的产生。
- 分代回收算法:分代回收算法是根据对象的生命周期,把堆内存划分为新生代和老年代,并采用不同的垃圾回收策略。
新创建的对象通常先进入新生代,因为大多数对象存活时间较短,所以新生代一般使用复制算法,回收速度较快(新生代回收称为Minor GC)。经过多次垃圾回收仍然存活的对象,会晋升到老年代。老年代中的对象生命周期较长,通常使用标记清除或标记整理算法(回收老年代通常称为 Major GC)。
分代回收的核心思想是:针对不同生命周期的对象采用不同算法,从而减少每次垃圾回收需要扫描的内存范围,提高垃圾回收效率。
堆的分区
堆从逻辑上划分为新生代和老年代两大区域
新生代:包含 Eden 区、From Survivor、To Survivor,默认占比 8:1:1,用于存放短期存活对象。
其复制算法流程:1. Eden + From Survivor 存活对象复制到 To Survivor,然后清空 Eden 和 From;2. 交换 From/To 角色。(即 To 空间 作为 一个 中转站)
老年代 :存放长期存活、年龄达到阈值的对象;也会存放大对象(直接进入老年代,避免大量复制)。
字符串常量池是堆里独立的一块逻辑区域,不属于新生代,也不属于老年代。
转换
多数对象先在 Eden 创建,Minor GC 存活的对象在两块 Survivor 之间来回转移。
存活达到年龄阈值的对象、大对象,会直接进入老年代长期保存。
涉及对象移动或复制的垃圾回收算法,都需要更新对象引用。
JVM 通常会在对象旧地址保存一个转发指针,记录对象复制后的新地址,并通过 GC Roots、Remembered Set 等结构找到并更新相关引用。
有哪些垃圾回收器
垃圾回收器是实现垃圾回收算法、负责自动查找并回收堆中死亡对象的执行引擎,是 GC 算法的具体落地实现。
1. 新生代垃圾收集器
- Serial 收集器(复制算法):是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。
- ParNew 收集器(复制算法):是新生代并行收集器,其实就是 Serial 收集器的多线程版本。
- Parallel Scavenge 收集器(复制算法):是新生代并行收集器,追求高吞吐量,高效利用 CPU。
2. 老年代垃圾收集器
- Serial Old(标记-整理算法):是Serial收集器的老年代版本,它同样是一个单线程(串行)收集器。
- Parallel Old(标记-整理算法):是Parallel Scavenge收集器的老年代版本,使用多线程。
- CMS 收集器(标记-清楚算法):是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:
- 初始标记(Initial Mark)【STW 】
只标记GC Roots 直接关联的对象,速度很快,停顿短。 - 并发标记(Concurrent Mark)【并发,不 STW】
从初始标记存活对象遍历整个老年代,追踪所有存活对象;
用户线程和 GC 线程同时运行,这段期间会产生浮动垃圾。 - 重新标记(Remark)【STW】
修正并发标记期间,用户线程新产生 / 引用变动的对象;
停顿时间一般长于初始标记,远低于串行收集器。 - 并发清除(Concurrent Sweep)【并发,不 STW】
清理标记出来的垃圾对象,回收内存;和用户线程并发执行。
STW:停止所有用户应用线程,只保留 GC 线程运行;GC 工作完成后,再恢复业务线程。
3. 新生代和老年代垃圾收集器
- G1收集器(标记整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代
细节:
G1(Garbage First)是一款面向大堆内存、低停顿需求的垃圾收集器。
它不再把堆固定划分为连续的新生代和老年代,而是将堆划分为多个大小相等的 Region,每个 Region 可以动态承担 Eden、Survivor 或老年代的角色。
G1 会统计各 Region 的垃圾数量和回收收益,并优先回收垃圾最多、收益最高的区域,因此称为 Garbage First。
它整体采用标记—整理算法,局部 Region 之间采用复制算法,所以回收后不会产生大量内存碎片。
G1 的回收过程主要包括:初始标记、并发标记、最终标记和筛选回收,其中并发标记可以与用户线程同时执行。
它还支持通过 -XX:MaxGCPauseMillis 设置期望的停顿时间,但只能尽量满足,不能保证绝对停顿时间。
面试总结:G1 通过 Region 化管理、并发标记和按收益优先回收,在可控停顿时间内完成垃圾回收,适合大内存和低延迟场景。
G1收集器的优势:
- 独特的分代垃圾回收器,分代GC:分代收集器,同时兼顾年轻代和老年代
- 不再将堆物理划分为连续新生代、老年代;把整个 Java 堆切分成多个大小一致的 Region。
Region 逻辑上分为:Eden、Survivor、Old、Humongous(大对象区域),Region 角色动态变换。 - 并行性:回收期间,可由多个线程同时工作,有效利用多核cpu资源
- 空间整理:回收过程中,会进行适当对象移动,减少空间碎片
- 可预见性:G1可选取部分区域进行回收,可以缩小回收范围,减少全局停顿
G1收集器的阶段分以下几个步骤:
- 初始标记(它标记了从GC Root开始直接可达的对象)【STW】
- 并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象)【并发】
- 最终标记(标记那些在并发标记阶段发生变化的对象,即垃圾,将被回收)【STW】
- 筛选回收(首先对各个Region的回收价值(垃圾占Region比例)和成本(回收时间,包括扫描存活对象,复制对象和更新引用)进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)【STW】
更多推荐




所有评论(0)