JVM

1. 什么是JVM

定义

JVM 指的是Java虚拟机,本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件,作用是为了支持跨平台特性。

功能

  1. 解释和运行:解释执行字节码指令。
  2. 内存管理:管理内存中对象的分配,完成自动的垃圾回收。
  3. 即时编译:优化热点代码提升执行效率。

组成

类加载子系统、运行时数据区、执行引擎、本地接口

常见的JVM

Oracle的Hostpot、GraalVM、龙井等。

2. 了解过字节码文件的组成吗?

字节码有五部分组成

  1. 基本信息:包括魔数(校验文件扩展名)、字节码文件对应的Java版本号、访问标识(public final)父类和接口

  2. 常量池:保存了字符串常量、类或接口名、字段名主要在字节码指令中使用。作用是避免相同的内容重复定义,节省空间。

  3. 方法:是存放字节码指令的核心位置、当前类或接口声明的方法信息
  4. 字段:当前类或接口声明的字段信息
  5. 属性:类的属性,比如源码的文件名 内部类的列表等

3. 说一下运行时数据区

运行时数据区指的是JVM所管理的内存区域,其中分成两大类:

线程共享 —- 方法区、堆

线程不共享 —- 本地方法栈、虚拟机栈、程序计数器

pAYjgYt.png

程序计数器

每个线程会通过程序计数器记录当前要执行的的字节码 指令的地址。

作用:

  1. 控制程序指令的进行,实现分支、跳转、异常。
  2. 在多线程情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续解释运行。

虚拟机栈

Java虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出 ,每一个方法的调用使用一个栈帧来保存。 每个线程都会包含一个自己的虚拟机栈,它的生命周期和线程相同。

栈帧的组成:

  1. 局部变量表:在方法执行过程中存放所有的局部变量。
  2. 操作数栈,虚拟机在执行指令过程中用来存放临时数据的一块区域。
  3. 帧数据,主要包含动态链接、方法出口、异常表等内容。

动态链接:方法中要用到其他类的属性和方法,这些内容在字节码文件中是以编号保存的,运行过程中需要替换成 内存中的地址,这个编号到内存地址的映射关系就保存在动态链接中。

方法出口:方法调用完需要弹出栈帧,回到上一个方法,程序计数器要切换到上一个方法的地址继续执行,方法出 口保存的就是这个地址。

异常表:存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。

本地方法栈

  • 本地方法栈存储的是nativ本地方法的栈帧。
  • 在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。

  • 一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。
  • 堆是垃圾回收最主要的部分
  • 栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实 现对象在线程之间共享。

方法区

方法区是Java虚拟机规范中提出来的一个虚拟机概念,在HotSpot不同版本中会用永久代或者元空间来实现。方法区主要存放的是基础信息,包含:

  • 1、每一个加载的类的元信息(基础信息)。
  • 2、运行时常量池,保存了字节码文件中的常量池内容,避免常量内容重复创建减少内存开销。
  • 3、字符串常量池,存储字符串的常量。

4. 哪些区域会出现内存溢出,会有什么现象?

内存溢出:内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存 时因空间不足而失败,虚拟机一般会抛出指定的错误。

堆:溢出之后会抛出OutOfMemoryError,并提示是Java heap Space导致的。

栈:溢出之后会抛出StackOverflowError。

方法区:溢出之后会抛 OutOfMemoryError,JDK7及之前提示永久代,JDK8及之 后提示元空间。

直接内存:溢出之后会抛出OutOfMemoryError。

5. JVM在JDK6-8之间在内存区域上有什么不同

方法区的实现:

  • JDK7及之前的版本:

    将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。

  • JDK8及之后的版本:

    将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不 超过操作系统承受的上限,可以一直分配。也可以手动设置最大大小。

使用元空间替换永久代原因:

  • 提升内存上限:元空间使用是操作系统的内存,不是JVM的内存,如果不设置上限。只要不超过操作系统内存 上限,就可以持续分配。而永久代在堆中,可使用的内存上限是有限的。所以使用元空间可以有效减少OOM情况 的出现。
  • 优化垃圾回收的策略:永久代在堆上,一般使用老年代的垃圾回收方式,不够灵活。而元空间专门设置了一套时候方法区的垃圾回收机制。

字符串常量池的位置

JDK7之前:运行时常量池逻辑包含字符串常量池

JDK7:字符串常量池被从方法区拿到了堆中, 运行时常量池剩下的东西还在永久代

JDK8之后:hotspot移除了永久代用元空间 (Metaspace)取而代之, 字符串常量池 在堆

字符串常量池从方法区移动到堆的原因:

  • 垃圾回收优化:移动到堆之后,就可以利用对象的垃圾 回收器,对字符串常量池进行回收。
  • 让方法区大小更可控:如果字符串常量池在方法区中,会让方法区的空间大小变得不可控。
  • intern方法的优化:字符串保存在堆上,把字符串的引用放入 字符串常量池,减少了复制的操作。

6. 类的生命周期

加载阶段

  • 类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。
  • 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中。在方法区生成一个 InstanceKlass对象,保存类的所有信息。
  • 在堆中生成一份与方法区中数据类似的java.lang.Class对象, 作用是在Java代码中去获取类的信息。

连接阶段

  • 验证:是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。
  • 准备:为静态变量(static)分配内存并设置初值。final修饰的基本数据类型的静态变量,准备阶段直接会将 代码中的值进行赋值。
  • 解析:解析阶段主要是将常量池中的符号引用替换为直接引用。

初始化阶段

  • 初始化阶段会执行静态代码块中的代码,并为静态变量赋值。
  • 初始化阶段会执行字节码文件中clinit部分的字节码指令。

使用阶段

卸载阶段

7. 什么是类加载器,有哪些常见的类加载器

类加载器负载在类的加载过程中将字节码信息以流的方式获取并加载到内存中

  1. 启动类加载器(Bootstrap ClassLoader)加载核心类
  2. 扩展类加载器(Extension ClassLoader)加载扩展类
  3. 应用程序类加载器(Application ClassLoader)加载应用classpath中的类
  4. 自定义类加载器,重写findClass方法。

JDK9及之后扩展类加载器(Extension ClassLoader)变成了平台类加载器(Platform ClassLoader)

8. 什么是双亲委派机制

类加载有层级关系,上一级称之为下一级的父类加载器。

双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会向上查找是否加载过,再由顶向下进行加载。

双亲委派机制的作用:保证类加载的安全性,避免重复加载。

9. 如何打破双亲委派机制

打破双亲委派机制的唯一方法就是实现自定义类加载器 重写loadClass方法,将其中的双亲委派机制代码去掉。

10. 如何判断堆上的对象有没有被引用?

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减 1,存在循环引用问题所以Java没有使用这种方法。

Java使用的是可达性分析算法来判断对象是否可以被回收。

可达性分析将对象分为两 类:

垃圾回收的根对象(GC Root)和普通对象。

可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。最 常见的是GC Root对象会引用栈上的局部变量和静态变量导致对象不可回收。

11. JVM 中都有哪些引用类型

  • 强引用:JVM中默认引用关系就是强引用,即是对象被局部变量、静态变量等GC Root关联的对象引用,只要这层关系存在,普通对象就不会被回收。

  • 软引用:软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。软引用主要在缓存框架中使用。

  • 弱引用:弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收,弱引用主要在ThreadLocal中使用。

  • 虚引用(幽灵引用/幻影引用):不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了 虚引用来实现。

12. ThreadLocal中为什么要使用弱引用?

当threadlocal对象不再使用时,使用弱引用可以让对象被回收;因为仅有弱引用没有强引用的情况下,对象是可以被回收的。

弱引用并没有完全解决掉对象回收的问题,Entry对象和value值无法被回收,所以合 理的做法是手动调用remove方法进行回收,然后再将threadlocal对象的强引用解除 。

13. 有哪些常见的垃圾回收算法?

标记清除算法

标记清除算法的核心思想分为两个阶段:

  1. 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出 所有存活对象。

  2. 清除阶段,从内存中删除没有被标记也就是非存活对象。

优缺点:

优点

实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。

缺点

  1. 碎片化问题

    由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一 个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。

  2. 分配速度慢

    由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才 能获得合适的内存空间。

复制算法

复制算法的核心思想是:

  1. 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。
  2. 在垃圾回收GC阶段,将From中存活对象复制到To空间。
  3. 将两块空间的From和To名字互换。

优缺点

优点

  1. 吞吐量高

    复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理 算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法, 因为标记清除算法不需要进行对象 的移动。

  2. 不会发生碎片化

    复制算法在复制之后就会将对象按顺序放 入To空间中,所以对象以外的区域都是可 用空间,不存在碎片化内存空间。

缺点

内存使用效率低

每次只能让一半的内存空间来为创 建对象使用

标记整理算法

标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。 核心思想分为两个阶段:

  1. 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出 所有存活对象。
  2. 整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。

优缺点:

优点

  1. 内存使用效率高

    整个堆内存都可以使用,不会像复 制算法只能使用半个堆内存

  2. 不会发生碎片化

在整理阶段可以将对象往内存的一侧进行 移动,剩下的空间都是可以分配对象的有效空间。

缺点

整理阶段的效率不高

分代垃圾回收算法

分代垃圾回收将整个内存区域划分为年轻代和老年代:

分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。

随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为 Minor GC或者Young GC。 Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。

接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。 此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。

注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。

如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。 当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个 堆进行垃圾回收。

如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。

优缺点:

优点

  1. 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
  2. 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法效率高、不会产生内存碎片,老年 代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。
  3. 分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收 (full gc),STW(Stop The World)由垃圾回收引起的停顿时间就会减少。

14. 有哪些常用的垃圾回收器

pAYv49x.png

Serial垃圾回收器 + SerialOld垃圾回收器

Serial是是一种单线程串行回收年轻 代的垃圾回收器。

回收年代和算法

  • 年轻代:复制算法
  • 老年代:标记-整理算法

优点

  • 单CPU处理器下吞吐量非常大。

缺点

  • 多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程 处于长时间的等待

适用场景

Java编写的客户端程序或者硬件 配置有限的场景

Parallel Scavenge垃圾回收器 + Parallel Old垃圾回收器

PS+PO是JDK8默认的垃圾回收器,多线程并行回收, 关注的是系统的吞吐量。具备自动调整堆内存大小的 特点。

回收年代和算法

  • 年轻代:复制算法
  • 老年代:标记-整理算法

优点

  • 吞吐量高,而且手动可控。 为了提高吞吐量,虚拟机会 动态调整堆的参。

缺点

  • 不能保证单次的停顿时间

适用场景

后台任务,不需要与用户交互,并且容易产生大量的对象 比如:大数据的处理,大文件导出

ParNew垃圾回收器+CMS垃圾回收器

CMS垃圾回收器关注的是系统的暂停时间, 允许用户线程和垃圾回收线程在某些步骤中 同时执行,减少了用户线程的等待时间。

缺点:

  • CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停
  • 无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收。、
  • 如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。
  • 并发阶段会影响用户线程执行的性能

G1 – Garbage First 垃圾回收器

  • 优点:对比较大的堆如超过6G的堆回收 时,延迟可控 不会产生内存碎片 并发标记的SATB算法效率高

  • 缺点:JDK8之前还不够成熟

Shenandoah

Shenandoah 是由Red Hat开发的一款低延迟的垃圾收集器,Shenandoah 并发执行大部分 GC 工作,包括并 发的整理,堆大小对STW的时间基本没有影响。

是ZGC

ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延 迟的应用。支持几百兆到16TB 的堆大小,堆大小对STW的时间基本没有影响。

15. JVM内存模型里的堆和栈有什么区别?

用途

  • 栈主要用于存储局部变量,方法调用的参数、方法返回地址以及一些临时数据。
  • 堆用于存储对象的实例。

生命周期

  • 栈数据有确定的生命周期,当方法调用结束时,其对应的栈帧就会被销毁。
  • 堆中对象生命周期不确定,对象会在垃圾回收机制检测的情况下才会被回收。

存储速度

栈存储速度通常比堆快,因为栈遵循先进先出。堆存取速度相对较慢

存储空间

栈空间相对较小,且固定。堆空间较大,动态扩展。

可见性

栈中数据对线程私有的,每个线程有自己的栈空间。

堆中数据对线程是共享的,所有线程都可以访问堆上的对象。

16. 栈中存的到底是指针还是对象?

栈中存储的不是对象,而是对象的引用

它指向堆中分配给对象的内存区域。

17. 堆分为哪几部分呢?

这需要看JVM和不同垃圾回收器的实现。

通常可以分为一下几部分

  • 新生代:

    新生代分为Eden Space和Survivor Space。当Eden区满时,会触发一次Minor GC。在Survivor Spaces中,通常分为两个相等大小的区域,称为S0和S1在每次Minor GC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。这两个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。

  • 老年代

    存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代的空间通常比新生代大,以存储更多的长期存活对象。

  • 元空间

    用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的内存溢出问题。

18. 程序计数器的作用,为什么是私有的?

作用

  1. 控制程序指令的进行,实现分支、跳转、异常。
  2. 在多线程情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续解释运行。

在多线程条件下,线程切换需要每个线程记录指令执行在哪里了,这个时候就需要程序计数器来记录。

19. String保存在哪里呢?

String 保存在字符串常量池中,不同于其他对象,它的值是不可变的,且可以被多个引用共享。

20. 弱引用了解吗?举例说明在哪里可以用?

弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收,弱引用主要在ThreadLocal中使用。

弱引用的使用场景:

  • 缓存系统:弱引用常用于实现缓存,特别是当希望缓存项能够在内存压力下自动释放时。如果缓存的大小不受控制,可能会导致内存溢出。使用弱引用来维护缓存,可以让JVM在需要更多内存时自动清理这些缓存对象。
  • 对象池:在对象池中,弱引用可以用来管理那些暂时不使用的对象。当对象不再被强引用时,它们可以被垃圾回收,释放内存。
  • 避免内存泄露:当一个对象不应该被长期引用时,使用弱引用可以防止该对象被意外地保留,从而避免潜在的内存泄露。

21. 内存泄漏和内存溢出的理解?

内存泄漏:运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。

内存溢出:内存溢出是指Java虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发OutOfMemoryError。这通常发生在堆内存不足以存放新创建的对象时。

22. 有具体的内存泄漏和内存溢出的例子么请举例及解决方案?

  1. 静态属性导致的内存溢出

会导致内存泄露的一种情况就是大量使用static静态变量。

在Java中,静态属性的生命周期通常伴随着应用整个生命周期

如何优化呢?第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。

  1. 未关闭的资源

无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。

忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。特别是当程序发生异常时,没有在finally中进行资源关闭的情况。这些未正常关闭的连接,如果不进行处理,轻则影响程序性能,重则导致OutOfMemoryError异常发生。

如果进行处理呢?

第一,始终记得在finally中进行资源的关闭;

第二,关闭连接的自身代码不能发生异常;

第三,Java7以上版本可使用try-with-resources代码方式进行资源关闭。

  1. 使用ThreadLocal

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程安全的特性。

image-20240820112835783

ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。

如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

如何解决此问题?

  • 第一,使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;
  • 第二,不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。
  • 第三,最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。

23. 创建对象的过程?

  1. 类加载检测:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
  2. 分配内存
  3. 初始化零值
  4. 进行必要设置
  5. 执行init方法

24. 双亲委派模型的作用

  • 保证类的唯一性:确保了所有加载请求都会传递到启动类加载器,避免了不同类加载器重复加载相同类的情况。
  • 保证安全性:由于Java核心库被启动类加载器加载,而启动类加载器只加载信任的类路径中的类,这样可以防止不可信的类假冒核心类,增强了系统的安全性。
  • 支持隔离和层次划分:双亲委派模型支持不同层次的类加载器服务于不同的类加载需求
  • 简化了加载流程:大部分类能够被正确的类加载器加载,减少了每个加载器需要处理的类的数量,简化了类的加载过程,提高了加载效率。

25. 什么是Java里的垃圾回收?如何触发垃圾回收?

垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。

触发垃圾回收

  • 内存不足时
  • 手动请求
  • JVM参数
  • 对象数量或内存使用达到阈值

文章结束!