【JVM】JVM八股总结一: 字节码、类加载、GC垃圾回收

97 阅读20分钟

understanding-the-jvm/Ch1-Java内存管理机制/02-垃圾收集(GC).md at master · TangBean/understanding-the-jvm

其他

Java中什么时候会出现内存泄露问题?(拼多多)

  1. 静态集合引用的对象,没手动释放存在内存泄露。(静态集合的生命周期与JVM一致)
public class MemoryLeak {
    private static List<Object> list = new ArrayList<>();
    
    public void addToList(Object obj) {
        list.add(obj);
    }
}
  1. 数据连接connection、文件IO连接没有及时关闭。会出现泄露
  2. 使用ThreadLocal,由于map中的key是弱引用,引发内存泄露

方法区中可能存在内存泄露吗?(拓展)

  1. 过多的静态变量,导致泄露。(因为静态变量和JVM生命周期相同,可能有的静态变量后续不需要用了,但是jdk1.8静态变量已经移动到了堆中)
  2. 过多的(代理)类信息,动态代理产生的类信息内存泄露(首先方法区很少GC,那么可能有的代理类后续不需要用但一直没被GC导致泄露,从而导致OOM)。

字节码篇

介绍一些JVM,整体上介绍?(算高频)

JVM分为哪几个部分?

36.解释器与JIT编译器_哔哩哔哩_bilibili

类加载系统:加载字节码文件类信息 到 运行时数据区。

运行时数据区

执行引擎(解释器、编译器、垃圾回收器): 方法的执行需要用到执行引擎( 字节码指令 解释/编译为 机器指令 执行

Java为什么跨平台?

  1. Java跨平台依赖于JVM
  2. 不同的平台/操作系统 有不同版本的JVM,Java源码编译后会生成 字节码。而JVM负责将字节码翻译成 特定平台的机器码然后运行。

也就是不同系统安装对应的JVM,就可以运行相同的字节码文件,可以运行相同的Java代码,JVM相当于是一个中间层,对于不同平台来说Java源码是相同的但JVM是不同的。

Java为什么既是编译型又是解释型语言(低频)

36.解释器与JIT编译器_哔哩哔哩_bilibili

执行引擎中有

解释器JIT即时编译器共用

解释器: 起步运行快,逐行翻译。

JIT编译器: 运行起来之后更快。(比如有一个for循环语句那编译器更快,不用每次逐行翻译)

类的加载

什么是类加载?何时类加载?类加载流程

  • 类加载过程,三个过程:
  1. 加载:class字节码文件加载到jvm中
  2. 链接
    1. 验证: 保证加载的字节码文件是否是合法的,是符合规范的。(通常和第一步loading同时进行)
    2. 准备: 为类静态变量分配内存,并赋默认值(数值为0,引用类型为null)、final常量在这时会显示赋值
    3. 解析: 将类、接口、字段、方法符号引用转换为直接引用
  1. 初始化:执行java代码(clinit方法)。静态变量、静态代码块
  • 连接过程又可分为三步:验证->准备->解析。

类加载第一步,加载

主要三步,硬背咯

基本数据类型及数组JVM预先定义,引用数据类型需要加载器加载。

加载是类加载过程的第一步(并且是由类加载器进行加载),主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为 方法区 的运行时数据结构
  3. 堆内存中生成一个代表该类的 Class 模板对象,作为方法区中类数据的访问入口

总结:

将类的字节码文件 读取 并加载到 内存中,在方法区中生成类的信息 并 在堆内存中生成Class模板对象作作为方法区中类信息的访问入口。

类加载第二步,链接。(问得少,字节)

10.链接环节的执行_哔哩哔哩_bilibili

分为:验证、准备、解析

  • 验证:保证加载的字节码文件是否是合法的,是符合规范的。(通常和第一步loading同时进行)
  • 准备:为类静态变量分配内存,并赋默认值(数值为0,引用类型为null)、final常量在这时会显示赋值
  • 解析:将类、接口、字段、方法符号引用转换为直接引用。(如果直接引用存在,那么系统中就存在该类/接口/字段/方法)

类加载第三步,初始化

为类static变量显示赋初始值,并执行静态代码块


  • 执行<clinit()>方法,是系统生成的方法:只有在给类中static变量显示赋值静态代码块赋值,才生成此方法。

知道哪些类加载器。(重要)

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader
  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的

  1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
  2. ExtensionClassLoader(扩展类加载器)由Java写的,主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。用户创建的jar放在这个目录下也会被加载。
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类,包括用户自定义的类也由AppClassLoader加载。ClassLoader.getSystemClassLoader()默认获取系统类加载器。

类加载器之间的关系?

子类加载器和父类加载器采用 组合代替继承的方式。关系是组合关系,父加载器作用子加载器的成员。

类加载器和类的关系?

说一下双亲委派,具体看源码 (重要)

类的加载

每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器

调用parent.loadClass方法

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

双亲委派,或者说类加载器这里,用了什么设计模式?

  • 看源码,典型的模板方法模式。
  1. 首先loadClass是classLoader这个抽象类中的模板方法,是共用的,也就是双亲委派机制。
  2. 那么扩展方法就是findClass方法,每个具体的类加载器的findClass方法不同,findClass才是真正去加载类的方法。
  • 总结,loadClass是模板方法,findClass是子类扩展方法

双亲委派的好处(为什么需要双亲委派)

  1. 避免类重复加载,确保一个类全局唯一。
  2. 安全,防止核心api,核心类库 被篡改。

如何打破双亲委派?(重要)

  1. 自定义加载器,需要继承 ClassLoader
  2. 如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
    1. 但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。为什么是重写 loadClass() 方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释
    2. 重写 loadClass()方法之后,我们就可以改变传统双亲委派模型的执行流程

如何自定义类的加载器?

继承ClassLoader这个抽象类即可,重写findClass或者loadClass方法

如何判断类加载器能否加载这个类?(根据什么机制判断当前这个类加载器能否加载这个类?)

根据双亲委派机制

先委托给父类,调用父类的loadClass方法,看父类能不能加载。

如果父类不能加载,再调用自己的findClass方法尝试加载。

结合 Tomcat 说一下双亲委派(Tomcat 如何打破双亲委托机制?...)

为什么Tomcat要破坏双亲委派机制?_哔哩哔哩_bilibili

Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的 类实现隔离的具体 原理

待完善,tomcat具体如何打破的。

运行时内存/数据区

八股总结二

对象内存布局

Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么

33.创建的对象的内存分配过程_哔哩哔哩_bilibili第六分钟开始

  1. 判断类是否加载、解析和初始化。没有那么先加载类。
  2. 在堆中为对象分配内存 。分配内存有两种方式:指针碰撞或空闲列表。选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"
  3. 处理分配对象空间时的并发安全问题(因为堆是线程共享的,所以分配空间时可能有并发问题)。两种方式,CAS 和 TLAB(线程私有)
  4. 初始化 属性成员 零值(如成员属性)
  5. 设置对象头信息(GC年龄,hashcode,锁状态)
  6. 执行init方法(显示初始化,包括构造方法)

对象访问的两种方式及优缺点?

  1. 句柄访问: Java 堆中将会划分出一块内存来作为句柄池,引用变量 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据对象类型数据各自的具体地址信息。
  2. 直接指针:引用变量 直接 指向对象地址。

优缺点:

使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。

使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销

对象头中有什么信息?

分为标记位mark Word和 klass Word。

MarkWord标记字段(32bit):hashCode、age分代年龄、锁标识等。

klass Word(32bit):标识对象所属类,指向Class对象


GC垃圾回收

understanding-the-jvm/Ch1-Java内存管理机制/02-垃圾收集(GC).md at master · TangBean/understanding-the-jvm

为什么需要 GC?

对象的GC次数存在那,GC年龄?

存在对象头 的markword(标记)字段

空间分配担保

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

有哪些常见的 GC?谈谈你对 Minor GC、还有 Full GC 的理解。Minor GC 与 Full GC 分别在什么时候发生?

Minor GC 会发生 stop the world 现象吗?

大多数情况下,对象在新生代中 Eden 区分配。

MinorGC:当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。Minor GC 会发生STW。

MajorGC:老年代空间不足时,会先Minor GC,如果空间还不足,会触发MajorGC(STW时间更长)

FullGC(整堆收集)

  1. 调用System.gc()方法时。

  2. 方法区空间不足。

  3. 老年代空间不足。

  4. Minor GC后进入老年代对象的平均大小 大于 老年代剩余空间(空间分配担保 :会fullGC确保新生代进入老年代的对象不会大于剩余空间)

垃圾判别/死亡对象 算法

如何判断对象是否死亡(引用计数法可达性分析算法两种方法)?

引用计数法:

可达性分析

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

解决了循环引用的问题。

哪些对象可以作为GC Roots?(记, 重要)

记个四到五个

  1. 虚拟机栈 局部变量表中引用 的对象
  2. 本地方法栈(Native 方法)中引用的对象
  3. 方法区/堆中类静态属性引用的对象
  4. 方法区/堆中常量 引用的对象
  5. 同步锁 持有的 对象

垃圾回收算法

垃圾收集有哪些算法,各自的特点?

详情见垃圾回收篇。

标记清除:

标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

  1. 标记 和 清除要遍历两次,效率低
  2. 产生内存碎片

复制算法

为了解决标记-清除算法的效率和内存碎片问题,复制(Copying)收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

  1. 可用内存变小:可用内存缩小为原来的一半。
  2. 不适合老年代:如果存活对象数量比较多,那几乎每个对象都要进行复制。
  1. 适合新生代这种朝生夕死的地方,复制后存活对象不多的地方。(通常用于新生代如s0和s1区

标记-压缩/整理

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动。然后直接清理掉端边界以外的内存。

优点:解决了内存碎片问题,也解决了复制 算法内存空间问题。

缺点:由于多了整理这一步,效率也不高,适合老年代这种垃圾回收频率不是很高的场景。

分代收集

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

常见的垃圾收集器

垃圾收集器

Serial GC 串行回收

Serial收集新生代、Serial Old收集老年代

这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(STW)

  • 新生代Serial 采用 标记-复制算法
  • 老年代Serial Old 采用 标记-整理算法

ParNew GC 并行回收

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

  • 新生代ParNew GC 采用 标记-复制算法
  • 搭配CMS,CMS负责收集老年代

Parallel GC 并行回收

Parallel Scavenge、 Parallel Old

Parallel是 吞吐量优先的,高效利用cpu,所以相对来说每次STW的时间会比 注重低延时 策略长一点。

并行回收

  • 新生代Parallel Scavenge 采用 标记-复制算法
  • 老年代Parallel Old 采用 标记-整理算法

CMS 并发收集(重要)

  1. 是 HotSpot 虚拟机第一款真正意义上的并发收集器。
  2. 低延迟为主,STW的时间较短。
  3. CMS 收集器是一种 标记-清除 算法实现的,意味着会有内存碎片

CMS 是老年代收集器。 搭配parNew新生代收集器。

CMS收集分为四个步骤

  1. 初始标记暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 。
  2. 并发标记同时开启 GC 和用户线程,从GC ROOT遍历整个对象图,但不会停顿用户线程。
  3. 重新标记: 重新标记阶段就是为了修正 并发标记 期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,会STW。
  4. 并发清除: 与用户线程同时执行,同时 GC 线程开始对 未标记的区域做清扫。

G1并发收集(重要)

G1最大的特征:将大空间分成若干个小区域,能实现一些更复杂、更精细的功能。

以极高概率满足 GC 停顿时间 要求的同时,还具备 高吞吐量性能 特征.

G1收集器是分区的,没有8:1:1的概念了

特点:

  1. 内存区域化布局。
  2. 并行与并发:G1收集器使用并行和并发的方式执行垃圾回收操作。
  3. 混合回收:即同时处理新生代和老年代的垃圾回收。(因为是根据区域来回收,不是分代收集)
  4. 空间整合: 内存回收以region为单位,region之间是复制算法,整体来看是标记-压缩算法
  5. 预测停顿时间(默认200ms,可通过参数设定) G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)

回收步骤

  1. 初始标记:停顿所有的应用程序的线程,识别出GcRoots直接关联的对象,并标记这些对象。
  2. 并发标记:从第一步得到的标记点继续向下遍历对象图,标记所有被引用的存活对象,此步骤与应用程序并发运行。
  3. 最终标记:在并发标记完成后,再次停顿所有的应用程序线程,重新标记被改变的对象和整理存活对象的布局。
  4. 筛选回收:根据回收成本 和 期望的STW时间,制定回收计划,选择region进行回收

上面的步骤除了并发标记其他步骤都需要STW

垃圾收集器相关问题

😖讲一下 CMS 垃圾收集器的四个步骤。CMS 有什么缺点?(重点)

四个步骤上面有

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除(使用标记清除算法不需要移动对象,所以可以和用户线程并发指向)

缺点2个

  1. 无法处理浮动垃圾(标记过程结束之后出现的垃圾,开始不是垃圾,后来是垃圾)
  2. 使用标记-复制算法。有空间内存碎片

G1 垃圾收集器的步骤。有什么缺点?(重点)

新一代垃圾回收器ZGC的探索与实践

  1. 初始标记,STW
  2. 并发标记,并发执行。
  3. 最终标记。STW
  4. 筛选回收(混合回收)。STW
  5. 复制阶段。采用标记-复制算法进行复制。STW时间较长

缺点

复制-阶段STW时间较长。

G1和CMS回收的最后一个步骤的不同点(为什么一个STW一个不需要STW)

我的理解:

G1是筛选回收,需要STW,因为G1采用标记整理算法需要移动对象,所以必须STW回收。

CMS采用标记清除不需要移动对象所以可以进行并发清除不需要STW。

G1GC 的特点。四个特点

特点(四个):

  1. 区域化布局,分区
  2. 并行与并发回收
  3. 混合回收:即同时处理新生代和老年代的垃圾回收。
  4. STW时间可预测,可设置的(G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region)

那CMS和G1的区别?

  1. 采用的算法不同,所以最后一个回收步骤不同
  2. G1可控制停顿时间,通过维护优先级队列
  3. G1采用分区进行收集

默认的垃圾回收器是哪一个?ZGC 了解吗?

JDK8: Parallel Scavenge(新生代)+ Parallel Old(老年代)

JDK 9 ~ JDK20: G1收集器

😖 并发标记要解决什么问题?并发标记带来了什么问题?如何解决并发扫描时对象消失问题?

xxx

强 软 弱 虚 引用

108_ThreadLocal之虚引用_哔哩哔哩_bilibili

被强引用引用的对象,用于不会被GC回收。

被软引用引用的对象,在内存不足时会被GC回收。

只被弱引用引用的对象,只要有GC就会回收。

虚引用必须和引用队列联合使用,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。就是说虚引用对象被回收后 将被引用队列保存。(跟踪对象的回收状态,做某些事情的通知机制)