JVM面试点汇总

时间:2022-12-02 08:04:17

我们会在这里介绍我所涉及到的JVM相关的面试点内容,本篇内容持续更新

我们会介绍下述JVM的相关面试点:

  • JVM内存结构
  • 内存溢出问题
  • 方法区与永久代和元空间
  • JVM内存参数
  • JVM垃圾回收算法
  • GC和分代回收算法
  • 类加载过程
  • 双亲委派
  • 对象调用类型

JVM内存结构

我们将会介绍JVM的整体内存结构的运行流程

JVM内存结构图

我们首先给出JVM的内存结构图:

JVM面试点汇总

JVM内存结构功能

我们针对上述图分别讲解功能部件:

/*Java Source*/

源代码(就是我们书写的代码)
    
/*Java Class*/
    
字节码(由源代码转变过来的,跨平台的关键)
    
/*类加载子系统*/
    
在正式执行代码前,我们需要先将代码使用的类和方法加载,类加载子系统就是做这个的
    
它会将类和方法加载到方法区
    
/*JVM Stacks 虚拟机栈 和 Native Method Stacks 本地方法栈*/
    
JVM Stacks 虚拟机栈:用来存放代码中所使用的我们所定义的方法,属性等局部变量
    
Native Method Stacks 本地方法栈:用于存放本地方法
    
但是上述栈只是定义,具体实现不一定需要分为两个栈,可以统一存放
    
/*PC Register 程序计数器*/
    
我们的程序线程并非一直分配CPU,当我们的线程CPU被剥夺后,我们需要记录继续运行时的继续位置
    
程序计数器用于记录当重新分配CPU后我们需要执行的下一行代码位置
    
/*堆*/
    
存储我们new出来的对象
    
/*方法区*/
    
方法区用于存放我们所使用的类和类方法
    
方法区只是定义,具体实现在不同JDK版本不同:7之前为永久代,8之后为元空间
    
/*GC垃圾回收*/
    
当我们的内存不足或处于一个需要清理的状态,我们调用GC垃圾回收清除掉目前不使用的数据
    
/*解释器*/
    
我们的字节码是不能直接执行的,我们需要先进行解释编译才能执行
    
/*即时编译器*/
    
正常情况下我们的字节码由解释器解释后执行,但会耗费一定时间
    
若使用次数较多,系统会使用即时编译器来编译字节码,并将其储存,每次直接调用,不经过解释器处理

内存溢出问题

我们将会介绍JVM的各部件的内存溢出问题

内存溢出问题

我们分别给出内存溢出的不同情况:

/*内存溢出区域*/

除了程序计数器其他区域均会出现内存溢出
    
/*OutMemoryError问题*/
    
问题产生原因:
    1.堆内存耗尽:对象越来越多,且均在使用,无法进行GC
    2.方法区内存耗尽:加载类越来越多,很多框架都会在执行时动态生成类
    3.虚拟机栈累积:每个线程都会占用1M内存,当线程个数越来越多,长时间不销毁导致错误
    
/**Error问题*/
    
问题产生原因:
    1.虚拟机内部:方法不断执行,执行次数过多

方法区与永久代和元空间

下面我们来介绍方法区与永久代和元空间问题

方法区与永久代和元空间

我们来介绍方法区与永久代和元空间的概念以及注意事项:

/*方法区*/

方法区只是JVM规范中定义的一块内存区域,用来存储类元数据,方法字节码,即时编译器需要的信息等
    
/*永久代*/
    
永久代是JDK1.7之前的方法区存放位置,最开始存放在常量池
    
/*元空间*/
    
元空间是JDK1.8之后的方法区存放位置,存放于堆中
    
/*注意点*/
    
元空间GC条件非常苛刻:
	- 仅当堆中所有的类的对象全部清理之后,才能清理元空间数据

JVM内存参数

下面我们介绍一下面试中常考的JVM内存参数

JVM参数展示

我们根据分区展示JVM常用参数:

/*内存区域*/

-Xmx:最大内存
    
-Xms:最小内存
    
-Xmn:新生代内存(伊甸园+from+to)
    
-XX:Survivor:伊甸园和from 的比率(注意:实际划分应该是伊甸园-from-to:n:1:1)
    
/*元空间*/
    
元空间分为classspace(类基本信息)和non-classspace(类的注释,字节码等信息)

-XX:CompressedClassSpaceSize classspace最大内存空间
-xx:MaxMetaspaceSize 元空间最大内存空间
    
/*代码缓存*/
    
当代码缓存空间小于240,全部都保存在code cache中
    
当代码缓存空间大于240,non-nmthods:JVM代码基本信息;profiled-nmthods:部分优化信息;non-profiled-nmthods:完整信息
    
-XX:ReservedCodeCacheSize 设置代码缓存区内存大小
    
/*线程*/
    
-Xss:线程占用内存大小,默认1M

JVM垃圾回收算法

下面我们介绍面试中常问的三种垃圾回收算法

标记操作

在开始前我们先回顾标记操作:

/*标记操作*/

1. 找到Root根对象(Root根对象就是一定不会被垃圾回收的对象,包括但不限于正在使用的对象,静态对象等)
    
2. 根据Root根对象向下蔓延,标记延申的对象,该类对象将不被回收

标记清除

我们简述标记清除操作:

/*标记清除操作*/

1. 先进行标记处理
    
2. 直接在内存中将未标记的数据清除(实际上就是标记为空白数据)
    
/*优缺点*/
    
1. 执行速度极快
    
2. 但会产生内存碎片,当内存碎片逐渐增多会导致问题
    
注意:基本不再使用该GC处理

标记整理

我们简述标记整理操作:

/*标记整理操作*/

1. 先进行标记处理
    
2. 将未标记的数据清除,同时将标记的数据重新排序,紧密排列
    
/*优缺点*/
    
1. 不会产生内存碎片
    
2. 耗时,需要重新复制粘贴数据更换位置
    
注意:该算法经常用于老年代的GC处理

标记复制

我们简述标记复制操作:

/*标记复制操作*/

1. 准备两块相同大小的区域,分为from和to区域,我们的信息都会存放在from区域
    
2. 首先对from区域进行标记处理
    
3. 直接将from区域的标记数据移动到to区域,按顺序排列,然后对调from和to区域
    
/*优缺点*/
    
1. 速度快,不会产生内存碎片
    
2. 占用双倍内存
    
注意:该算法经常用于新生代的GC处理

GC和分代回收算法

下面我们介绍GC和分代回收算法

GC概述

首先我们对GC做一个简单的总结:

/*GC目的*/

GC的目的在于实现无用对象内存自动释放,减少内存碎片,加快分配速度
    
/*GC要点*/
    
1. 回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
    
2. 判断无用对象,使用可达性分析算法和三色标记法标记存活对象,回收未标记对象
    
3. GC的具体实现称为垃圾回收器
    
4. GC大多数都采用了分代回收思想,分为新生代和老年代,新生代又分为伊甸园,幸存区;不同区域有不同的回收策略
    
5. 根据GC规模可以分为Minor GC,Mixed GC,Full GC
    
/*GC不同规模*/
    
Minor GC:发生在新生代的垃圾回收,暂停时间短
    
Mixed GC:G1垃圾回收器特有,新生代整体垃圾回收,老年代部分垃圾回收,可以设置垃圾回收时间,当时间不足,系统会优先回收回收收益大的
    
Full GC:新生代和老年代的完整垃圾回收,暂停时间长,应极力避免

分代回收思想

我们首先来介绍分代回收思想:

/*分代回收区域*/

新生代:
    伊甸园
    幸存区
    	from
    	to
    
老年代
    
/*具体介绍*/
    
新生代和老年代属于两块大区域
    
新生代:用于存储较新的数据,经过层层筛选进入老年代
    
伊甸园:新生代的一块区域,用于存放所有新进入的数据
    
幸存区:用于存放经过GC的数据,采用from,to也就是标记复制的方法保存
    
老年代:用于存储经过多次GC还未回收的数据,GC条件苛刻
    
/*跳转介绍*/
    
新生代GC:
    新生代的伊甸园内存塞满后,进行一次新生代的GC,进行筛选,将保存的数据放入幸存区的from
    新生代第二次GC,同样筛选,注意伊甸园和幸存区都需要筛选,然后将保存的数据放入幸存区的to,然后调换from和to
 	
新生代->老年代:
    当新生代经过多次GC,数据经历了多次GC仍未被处理,且次数超过一个阈值,就放入老年代中
    当新插入的数据过大,新生代无法存储,就直接放入老年代存储

三色标记和并发漏标问题

我们在标记过程中经常会采用三色标记法来标记:

/*三色标记法*/

黑色-已标记
灰色-标记中
白色-未标记
    
系统会统计Root,然后从Root往下延申,标记过的部分标记为黑色,正在标记的部分未灰色,直到所有Root走完
    
这时我们需要保留的数据为黑色,我们不需要的数据为白色,标记完成
    
/*处理并发漏标问题*/
    
如果我们线程并发处理,我们在GC过程中,另一个线程调用了新的类,这时该类未被标记为黑色,就会导致将我们需要的数据删除
    
存在两种处理方式:
    
1.Incremental Update:
    只要赋值发生,被赋值的对象就会被记录(类的Root),在三色标记结束后重新遍历记录的对象
    
2.Snapshot At The Beginning:
    新加对象会被记录(类)
    被删除引用关系的对象也会被记录
    最后同样在三色标记结束后,也会全部遍历处理

垃圾回收器

我们介绍三种垃圾回收器:

/*Parallel GC*/

eden 内存不足时发生Minor GC,标记复制STW(STW:Stop The World,停止其他线程的操作)
    
old 内存不足时发生Full GC,标记整理STW
  
该垃圾回收器注意吞吐量
    
/*ConcurrentMarkSweep GC*/
    
old 并发标记,重新标记时需要STW,并发清除(并发操作时,其他线程不需要停止操作)
    
Failback Full GC:当垃圾回收失败时,存在保底策略Failback Full GC
    
该垃圾回收器注意响应时间
    
/*G1 GC*/
    
响应时间与吞吐量兼顾
    
划分为多个区域,每个区域都可以充当eden,survivor,old,humongous(存放大型数据的位置,减少复制操作,减少时间损耗)
    
新生代回收:eden内存不足,标记复制STW
    
并发标记:old 并发标记,重新标记时需要STW
    
混合收集:并发标记完成,开始混合收集,参与复制的有eden,survivor,old,其中old会根据暂停时间目标,选择部分回收价值高的区域,复制期间STW
    
Failback Full GC

类加载过程

我们下面来介绍一下类加载过程

类加载流程

类加载主要分为三个步骤:

/*加载*/

1. 将该类的字节码载入方法区,先创建类.class对象
   
2. 如果此类的父类没有加载,先加载父类
    
3. 加载是懒惰执行
   
/*链接*/
    
1. 验证-验证类是否符合Class规范,合法性,安全性检查
    
2. 准备-为static变量分配空间,设置默认值
    
3. 解析-将常量池的符号引用解析为直接引用
    
/*初始化*/
    
1. 执行静态代码块与非final静态变量的赋值
    
2. 初始化是懒惰执行

类加载解释

我们对上述流程的部分内容进行解释:

/*final值处理*/

针对final的基本类型的处理在类的声明阶段就已经进行赋值了(默认为常量)
    
我们如果直接在main方法中调用类的final的基本类型,既不会触发类初始化也不会触发类加载(直接从常量池取数据或者提前保存到底层)
    
/*静态变量处理*/
    
针对静态变量static的声明和分配空间都是在链接阶段进行,但只会赋默认值
    
针对final引用类型,静态变量static和static静态代码块的信息都是在初始化阶段才会赋值
(将其按从上到下的顺序保存到一个static方法中统一执行)
    
针对final引用类型和静态变量在main中引用的,都会触发类的加载和初始化
    
/*链接的符号引用变为直接引用*/
    
在未进行链接前,我们的常量池和底层代码中都会保存类的符号引用(仅仅是一个占位符)
    
在链接之后,该占位符就会变成类的引用地址

双亲委派

下面我们来简单介绍一下双亲委派及相关面试点

双亲委派概述

我们首先介绍双亲委派:

/*双亲委派*/

针对类,优先委派上级类加载器进行加载:
    1.当上级类加载器能找到这个类,由上级加载,加载后该类对下级加载器也可见
    2.当上级类加载器找不到这个类,下级加载器才有资料加载该类

四种类加载器

我们来介绍四种类加载器:

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

我们简单介绍一下运行机制:

  • 首先我们需要知道Bootstrap ClassLoader 是不可访问的,当我们查找到该层级时会显示null
  • 我们如果需要加载一个类,会先检测是否有上级,如果有上级就到上级中去,如果没有就在本层查找是否有该类
  • 意思就是以*的加载器中的类为最高标准,如果同时存在多个类,我们会选择*的类加载器中的类来运行

双亲委派逻辑问题

我们提出一个简单的双亲委派相关的逻辑问题:

/*问题*/

我们能够自己编写类加载器来加载一个假冒的java.lang.System吗?
    
/*解题*/
    
不能
    
1.假设你编写的类加载器走双亲委派的流程,那么就会优先启动真正的Java.lang.System,不会加载自己书写的类加载器
    
2.假设你编写的类加载器不走双亲委派流程,那么你的类加载器加载到假冒的System时,需要先加载父类Java.lang.Object,但没有委派流程,所以你是找不到Objet类的
    
3.此外,在JDK9之后,针对特殊的包名Java.,Java.lang等都已经进行了警告提示,编写是不会被通过的
    
/*双亲委派目的*/
    
1.为了让上级类加载器的类作用于下级类加载器,即瓤你的类能够依赖到JDK提供的核心类
    
2.让类的加载有优先次序,保证核心类优先加载

对象引用类型

下面我们来介绍对象引用类型

四种常见对象引用类型

我们首先介绍四种常见的对象引用类型:

/*强引用*/

普通变量赋值即为强引用:A a = new A();

通过GC Root的引用链,如果强引用不到对象,该对象就可以被回收
    
/*软引用SoftReference*/
    
需要采用软引用对象连接真正的对象:SoftReference a = new SoftReference(new A());

如果仅有软引用引用该对象,则首次GC不会回收该对象,但GC过后若内存仍不足,该对象就会被回收
    
软引用对象本身需要利用引用队列ReferenceQueue来进行回收
    
典型例子是反射数据
    
/*弱引用WeakReference*/

需要采用弱引用对象连接真正的对象:WeakReference a = new WeakReferenec(new A());

如果仅有弱引用引用该对象,则只要发生GC,就直接回收该对象
    
弱引用对象本身需要利用引用队列ReferenceQueue来进行回收
    
典型例子是ThreadLocalMap中的Entry对象(key)
    
/*虚引用PhantomReference*/
    
虚引用也需要虚引用对象来连接真正的对象:PhantomReference a = new PhantomReference(new A());

该虚引用对象必须联合引用队列ReferenceQueue来执行,系统会一直检测是否存在虚引用对象,当引用的真正对象被回收后,虚引用对象就会被放置到ReferenceQueue中,由ReferenceHandler线程释放其关联的资源
    
典型例子是Cleaner释放DirectByteBuffer占用的直接内存
    
/*Cleaner的使用*/
    
1.创建一个Cleaner:
    Cleaner cleaner = Cleaner.create();

2.Cleaner使用:
    Cleaner.register(对象,对象回收后方法);

其中,对象就是我们需要回收的对象;后面的方法我们可以采用Lambda表达式实现() -> {}

Finalize

Finalize也被称为终结器引用,其实是一种已经过时的引用类型:

/*Finalize认知*/

属于Object的方法,子类重写后,在该子类被垃圾回收后就会调用,可以执行一些资源释放和清理工作
    
/*深层认知*/
    
Finalize不适合完成资源释放和清理工作,因为十分影响性能,甚至严重时引起OOM
    
/*具体原因*/
    
// 方法不佳
    
1.FinalizerThread是守护线程,代码有可能还未执行就结束了,导致资源没有释放
    
2.Finalize会吞掉异常,我们无法判断资源释放过程中是否出现异常
    
// 影响性能
    
1.重写了Finalize的对象在第一次GC时不能被回收,会被FinalizerThread调用finalize方法,将他从unfinalized队列去除后才能释放
    
2.GC本身就是因为内存不足调用,但是Finalize由于调用过慢(串行执行,锁)导致不能及时释放内存,导致资源放入老年代,导致Full GC
    
// 质疑
    
1.Finalizer线程的优先级其实为8,相对而言比较高的,但是由于finalize执行过慢导致跟不上主线程的步伐

结束语

目前关于JVM的面试点就总结到这里,该篇文章会持续更新~

附录

参考资料:

  1. 黑马Java八股文面试题视频教程:虚拟机-01-jvm内存结构_代码执行流程_哔哩哔哩_bilibili