Bootstrap

【Java 面试 八股文】JVM 虚拟机篇

1. JVM组成

1.1 JVM由那些部分组成,运行流程是什么?

难易程度:☆☆☆
出现频率:☆☆☆☆

JVM是什么
Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境)

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收机制

在这里插入图片描述
从图中可以看出 JVM 的主要组成部分

  • ClassLoader(类加载器)
  • Runtime Data Area(运行时数据区,内存分区)
  • Execution Engine(执行引擎)
  • Native Method Library(本地库接口)

运行流程:

  1. 类加载器(ClassLoader)把Java代码转换为字节码
  2. 运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
  3. 执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。

1.2 什么是程序计数器?

难易程度:☆☆☆
出现频率:☆☆☆☆

程序计数器(PC Register):线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
javap -verbose xx.class 打印堆栈大小,局部变量的数量和方法的参数。

1.3 你能给我详细的介绍Java堆吗?

难易程度:☆☆☆
出现频率:☆☆☆☆

线程共享的区域:Java堆 (Heap) 是Java虛拟机中内存管理的一个重要区域,主要用于存放对象实例数组。当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
在这里插入图片描述

  • 新生代(Young Generation):新生代分为Eden Space和Survivor Space。在Eden Space中,大多数新创建的对象首先存放在这里。Eden区相对较小,当Eden区满时,会触发一次 Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为 S0(Survivor 0) 和 S1(Survivor1)。在每次Minor GC后,存活下来的对象会被移动到其中一个Sunvivor空间,以继续它们的生命周期。
  • 老年代(Old Generation/Tenured Generation):存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major Gc(也称为Ful GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor Gc长。老年代的空间通常比新生代大,以存储更多的长期存活对象。
  • 元空间保存的类信息、静态变量、常量、编译后的代码。

为了避免方法区出现OOM,所以在java8中将堆上的方法区【永久代】给移动到了本地内存上,重新开辟了一块空间,叫做元空间。那么现在就可以避免掉OOM的出现了。
在这里插入图片描述

1.4 Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

1.4.1 Java Virtual machine Stacks (java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出。
  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存。每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

1.4.2 栈和堆的区别

  • 在JVM内存模型中,栈(Stack)主要用于管理线程的局部变量和方法调用的上下文,而堆(Heap)则是
    用于存储所有类的实例和数组。堆会GC垃圾回收,而栈不会。
  • 栈内存是线程私有的,而堆内存是线程共有的。
  • 两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
    • 栈空间不足: java.lang.StackOverFlowError。
    • 堆空间不足: java.lang.OutOfMemoryError。

1.4.3 垃圾回收是否涉及栈内存?

垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放。

1.4.4 栈内存分配越大越好吗?

未必,默认的栈内存通常为1024k。栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半。

1.4.5 方法内的局部变量是否线程安全?

  • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

1.4.6 栈内存溢出情况

  • 栈帧过多导致栈内存溢出,典型问题:递归调用
  • 栈帧过大导致栈内存溢出(不容易出现)

1.5 能不能解释一下方法区?

难易程度:☆☆☆
出现频率:☆☆☆

1.5.1 概述

  • 方法区(Method Area)是各个线程共享的内存区域
  • 主要存储类的信息、运行时常量池
  • 虚拟机启动的时候创建,关闭虚拟机时释放
  • 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace

1.5.2 运行时常量池

  • 常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
    在这里插入图片描述

1.5.3 方法区中的方法的执行过程?

当程序中通过对象或类直接调用某个方法时,主要包括以下几个步骤:

  • 解析方法调用:JVM会根据方法的符号引用找到实际的方法地址
  • 栈帧创建:在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    执行方法:执行方法内的字节码指令,涉及的操作可能包括局部变量的读写、操作数栈的操作、跳转控制、对象创建、方法调用等。
    返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境。

1.6 你听过直接内存吗?

难易程度:☆☆☆
出现频率:☆☆☆

  • 并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存
  • 常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理

2. 类加载器

难易程度:☆☆☆☆
出现频率:☆☆☆

2.1 什么是类加载器

JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

2.2 类加载器种类

类加载器根据各自加载范围的不同,划分为四种类加载器:

  • 启动类加载器(BootStrap ClassLoader):该类并不继承ClassLoader类,其是由C++编写实现。负责加载Java的核心库(如JAVA_HOME/jre/lib目录下的类库)。
  • 扩展类加载器(ExtClassLoader):该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
  • 应用类加载器(AppClassLoader):该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
  • 自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。

2.3 什么是双亲委派模型?

难易程度:☆☆☆☆
出现频率:☆☆☆☆

如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
在这里插入图片描述

2.4 JVM为什么采用双亲委派机制(作用)

难易程度:☆☆☆
出现频率:☆☆☆

  1. 保证类的唯一性:通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
  2. 保证安全性:为了安全,保证类库API不会被修改。例如,恶意代码无法自定义一个Java.lang.System类并加载到IM中,因为这个请求会被委托给启动类加载器,而启动类加载器只会加载标准的Java库中的类。

2.5 说一下类装载的执行过程

在这里插入图片描述

  1. 加载:查找和导入class文件
    • 通过类的全名,获取类的二进制数据流。
    • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
    • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
  2. 验证:保证加载类的准确性
    • 格式检查:文件格式是否错误、语法是否错误、字节码是否合规
      • 文件格式验证
      • 元数据验证
      • 字节码验证
    • 符号引用验证:Class文件在其常量池中通过字符串记录自己将要使用的其它类或者方法,检查它们是否存在
  3. 准备:为类变量分配内存并设置类变量初始值
    • static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
    • static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成
    • static变量是final的引用类型,那么赋值也会在初始化阶段完成
  4. 解析:把类中的符号引用转换为直接引用
    • 比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
  5. 初始化:对类的静态变量,静态代码块执行初始化操作
    • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
    • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
  6. 使用:JVM 从入口方法开始执行用户的程序代码
    • 调用静态类成员信息(比如:静态字段、静态方法)
    • 使用new关键字为其创建对象实例
  7. 卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象
    • 最后负责运行的 JVM 也退出内存

3. 垃圾回收

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

3.1 对象什么时候可以被垃圾器回收

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。

如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法

  • 引用计数
    • 原理: 为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。
    • 缺点: 不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。
  • 可达性分析算法
    • 原理: 从一组称为GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象用的其他对象,以此类推。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对不可达),那么这个对象就被认为是不可达的,可以被回收。
    • GC Roots对象包括: 虚拟机栈(栈帧的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中JNI (Java Native Interface)引用的对象、活跃线程的引用等。

3.2 VM 垃圾回收算法有哪些?

难易程度:☆☆☆
出现频率:☆☆☆☆

3.2.1 标记清除算法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。

  1. 根据可达性分析算法得出的垃圾进行标记
  2. 对这些标记为可回收的内容进行垃圾回收

标记清除算法也是有缺点的:通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的

3.2.2 标记整理算法

标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题

优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响

3.2.3 复制算法

将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。 缺点:内存利用率不足。

3.3 分代回收算法

堆被分为了两份:新生代和老年代【1:2】
在这里插入图片描述
分代回收是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15)后,如果对象还存活,那么该对象会进入老年代。

MinorGC、 Mixed GC 、 FullGC的区别是什么

  • MinorGC【young GC】
    • 发生在新生代的垃圾回收
    • 当 Eden 区空间不足时,JVM 触发依次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年区。
    • 频率高,暂停时间短(STW)
  • Mixed GC
    • 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
  • FullGC
    • 新生代 + 老年代完整垃圾回收
    • 暂停时间长(STW),应尽力避免

STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成

3.4 说一下 JVM 有哪些垃圾回收器?

难易程度:☆☆☆☆
出现频率:☆☆☆☆

在jvm中,实现了多种垃圾收集器,包括:

  • 串行垃圾收集器
  • 并行垃圾收集器
  • CMS(并发)垃圾收集器
  • G1垃圾收集器

3.4.1 串行垃圾收集器

SerialSerial Old 串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑

  • Serial 作用于新生代,采用复制算法
  • Serial Old 作用于老年代,采用标记-整理算法
  • 垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
  • 优点是简单高效

3.4.2 并行垃圾收集器

Parallel NewParallel Old 是一个并行垃圾回收器,JDK8默认使用此垃圾回收器

  • Parallel New作用于新生代,采用复制算法
  • Parallel Old作用于老年代,采用标记-整理算法
  • 垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

3.4.3 CMS(并发)垃圾收集器

CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。

3.4.4 G1垃圾回收器

  • 应用于新生代和老年代,在JDK9之后默认使用G1
  • 划分成多个区域(弱化了分代的概念),每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
  • 采用复制算法
  • 响应时间与吞吐量兼顾
  • 分成三个阶段:新生代回收、并发标记、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

3.5 强引用、软引用、弱引用、虚引用的区别?

难易程度:☆☆☆☆
出现频率:☆☆☆

  • 强引用: 只要所有 GC Roots 能找到,就不会被回收
  • 软引用: 需要配合SoftReference使用,当垃圾多次回收,内存依然不够的时候会回收软引用对象
  • 弱引用: 需要配合WeakReference使用,只要进行了垃圾回收,就会把弱引用对象回收
  • 虚引用: 必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

4. JVM 实践

4.1 JVM 调优的参数都有哪些?

通常在linux系统下直接加参数启动springboot项目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
参数 & :让命令在后台执行,终端退出后命令仍旧执行。

1)设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。
-Xms:设置堆的初始化大小
-Xmx:设置堆的最大大小

2)设置虚拟机栈的位置
3)年轻代中 Eden 区和两个 Survivor 区的大小比例
4)设置垃圾回收器

4.2 java内存泄露的排查思路?

第一,可以通过jmap指定打印他的内存快照 dump文件,不过有的情况打印不了,我们会设置vm参数让程序自动生成dump文件
第二,可以通过工具去分析 dump文件,jdk自带的VisualVM就可以分析
第三,通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
第四,找到对应的代码,通过阅读上下文的情况,进行修复即可

4.3 服务器CPU持续飙高,你的排查方案与思路?

第一可以使用使用 top 命令查看占用cpu的情况
第二通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个 进程id
第三可以通过ps 查看当前进程中的 线程信息,看看哪个线程的cpu占用较高
第四可以jstack命令打印进行的id,找到这个线程,就可以进一步定位问题代码的行号

;