type
status
date
slug
summary
tags
category
icon
password

JVM面试题

java运行时数据区域

  • Java 运行时数据区域(JDK1.7)
notion image
Java 运行时数据区域(JDK1.8 )
notion image

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

对象的创建过程

1.类加载检查

当JVM遇到一条new指令时,首先会检查这个指令的参数能否在常量池中定位到一个类的符号引用,在检查这个类的是否已经被加载、解析和初始化,如果没有则先执行类加载机制

2.内存分配

类加载检查完后,接下来虚拟机将为新生对象分配内存,对象所需的内存大小在类加载完成后便可以确认,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。如果堆内存空间是规整的则会采用指针碰撞来分配内存,如果不规整则采用空闲列表来分配。
  • 指针碰撞
    • 适用场合:堆内存规整(即没有内存碎片)的情况下。
    • 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
    • 使用该分配方式的 GC 收集器:Serial, ParNew
  • 空闲列表
    • 适用场合:堆内存不规整的情况下。
    • 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
    • 使用该分配方式的 GC 收集器:CMS

内存分配并发问题

虚拟机采用两种方式来保证线程安全:
  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

3.内存初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用

4.设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,包括类型指针(Klass Word)、标记字段(Mark Word)。类型指针指向方法区的类型元数据,JVM通过这个指针可以知道这个对象的类实例,标记字段中存放了对象的哈希码、GC分代年龄和锁状态标识等信息

5.调用构造函数

最后JVM会调用构造函数来初始化对象的实例字段及其他资源

对象内存布局

对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头

  • 标记字段(Mark Word):用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
  • 类型指针(Klass Word):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容

对齐填充

对齐填充仅仅起占位作用,HotSpot 虚拟机要求对象的起始地址必须是8的整数倍

主要进行GC的区域

部分收集 (Partial GC):
  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区

空间分配担保

空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间

死亡对象判断方法

对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)

引用计数法

给对象中添加一个引用计数器:
  • 每当有一个地方引用它,计数器就加 1;
  • 当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的
缺点:无法解决对象之间循环依赖的问题(互相引用对方导致双方的引用计数器不为0)

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收

可作为 GC Roots的对象

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • JNI(Java Native Interface)引用的对象

引用类型

强引用

如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止

软引用

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中
软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

弱引用

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象

虚引用

虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动 虚引用必须和引用队列(ReferenceQueue)联合使用

判断一个常量是无用常量

运行时常量池主要回收的是废弃的常量
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了

判断一个类是无用类

方法区主要回收的是无用的类
  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

标记-清除算法

标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象
  • 效率问题:标记和清除两个过程效率都不高。
  • 空间问题:标记清除后会产生大量不连续的内存碎片
notion image

复制算法

它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收
  • 可用内存变小:可用内存缩小为原来的一半。
  • 不适合老年代:如果存活对象数量比较大,复制性能会变得很差
notion image

标记-整理算法(适合老年代)

标记-整理(Mark-and-Compact)算法分为“标记(Mark)”和“整理(Compact)”阶段:首先标记出所有存活的对象,让所有存活的对象向一端移动,形成一个连续的内存块,然后直接清理掉端边界以外的内存
  • 性能问题:需要整理移动对象存在一定的性能开销
notion image

分代收集算法(HotSpot中采用此垃圾回收算法)

在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集

垃圾收集器

JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看)
  • JDK8之前:Parallel Scavenge(新生代)+ Serial Old(老年代)
  • JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代)
  • JDK 9 ~ JDK22: G1

Serial收集器

Serial(串行)收集器是一个单线程收集器,它只会使用一条垃圾收集线程去完成垃圾收集工作更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束 新生代采用标记-复制算法,老年代采用标记-整理算法 优点:简单而高效(与其他收集器的单线程相比),Serial 收集器由于没有线程交互的开销,可以获得很高的单线程收集效率,Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择

ParNew收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样,运行在 Server 模式下的虚拟机的首要选择
新生代采用标记-复制算法,老年代采用标记-整理算法

Parallel-Scavenge-收集器(JDK1.8 新生代默认收集器

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU),Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略
新生代采用标记-复制算法,老年代采用标记-整理算法

Serial Old收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案

Parallel Old收集器(JDK1.8 老年代默认收集器

Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器

CMS(Concurrent Mark Sweep)收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合在注重用户体验的应用上使用,是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
步骤
  1. 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
  1. 并发标记: 同时开启 GC 和用户线程,会遍历对象图,从 GC Roots 向下追溯,标记所有可达的对象
  1. 重新标记: 再次暂停所有其他线程,标记并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象
  1. 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫
  1. 并发重置:重置GC相关状态,准备下一次垃圾回收
缺点
  • 对 CPU 占用高;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生
CMS 垃圾回收器在 Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除

G1(Garbage-First)收集器(JDK9 后默认收集器)

一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征
  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒
步骤
  1. 初始标记(Initial Marking):这个阶段会暂停所有其他线程(Stop-The-World, STW),标记从 GC Roots 直接可达的对象。这个过程非常快。
  1. 并发标记(Concurrent Marking):在这个阶段,垃圾收集器会从 GC Roots 开始,遍历整个堆中的对象图,标记所有可达的对象。这个过程是并发执行的,不会阻塞应用程序的运行。
  1. 最终标记(Final Marking):再次暂停所有其他线程,处理在并发标记阶段新产生的对象引用关系,确保所有存活对象都被正确标记。这个阶段也比较快。
  1. 筛选回收(Live Data Counting and Evacuation):根据每个区域(Region)的垃圾堆积情况和回收价值进行排序,优先回收垃圾最多的区域。这个过程包括将存活的对象从一个区域复制或移动到另一个区域,并更新相关的引用。这个阶段也是并发执行的,以减少停顿时间
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)

内存分配策略

  1. 大部分新创建的对象都会分配在新生代的Eden区,当Eden区的内存不足时,会进行Minor GC,存活下来的对象会被移动到Survivor区,并将年龄设置为1,之后每经历一次Minor GC存活下来年龄就会增加1,当增加到一定程度(默认是15)的时候,会移动到老年代中
  1. 大对象再分配时,会直接分配到老年代,避免Minor GC对其频繁处理

类加载过程

加载

  1. 通过全类名找到此类对应的二进制字节流
  1. 将二进制字节流转换成方法区运行时的数据结构
  1. 在内存中生成一个该类的Class对象,作为方法区这些数据的访问接口

验证

确保类文件的字节流符合 Java 虚拟机规范的要求,保证代码的安全性
四个检验阶段:
  1. 文件格式验证(Class 文件格式检查)
  1. 元数据验证(字节码语义检查)
  1. 字节码验证(程序语义检查)
  1. 符号引用验证(类的正确性检查)
notion image

准备

为类变量分配内存并设置类变量的初始值(默认的零值),进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量

解析

虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符等符号引用进行

初始化

执行类构造器<clinit>方法,包括静态变量的赋值和静态代码块的执行

类卸载

  • 该类的实例对象都已经被GC,也就是说堆不存在该类的实例对象
  • 该类没有在 任何地方被引用
  • 该类的类加载器实例也已经被GC
在 JVM 生命周期内,由JVM自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的
Redis面试题面试题总结
Loading...
JackJame
JackJame
一个苦逼的码农😘
最新发布
Redis面试题
2025-3-3
面试题总结
2025-2-22
SpringBoot面试题
2025-2-18
JVM面试题
2025-2-18
数据库面试题
2025-2-16
Java并发编程面试题
2025-2-13
公告