JVM秋招总结
JVM是Java相关知识中重要的一大块,这里记录一下自己的学习思路,以及印象比较深刻的知识点和面试问题
个人总结思路
思路顺着一次Java程序运行中,涉及到的JVM部分总结
- 首先为什么要有JVM,会有什么好处
- 程序运行中,JVM会把javac编译后的class文件加载到JVM内存中
- 类加载会加载到JVM内存结构中的哪些区域,指令执行时又会涉及哪些区域呢
- 类加载后,可能会创建对象,又涉及到堆内存的结构、堆内存分配
- 运行一段时间后,无可用堆内存创建新对象,需要进行GC
一. 为什么要有JVM
简单来说就是一句话:一次编译,多平台运行
被问到过的问题
- java和c++、python语言的比较
- 在32位机和64位机上,java创建一个int变量,分别需要多少bit空间
javac编译后生成的字节码是平台无关的
无论是32位机还是64位机,什么操作系统,JVM中int就是32bit,long就是64bit
- 编译型语言和解释性语言的区别,java是哪种语言
编译型语言:c、c++、golang...编译结果是一个可执行文件,后面直接运行即可,但存在跨平台的编译环境不同的问题
解释型语言:python、js...运行时才通过解释器解析代码,性能较差,但是可以通过解释器解决跨平台的问题
java:介于编译型语言和解释型语言之间
1. 通过JVM作为中间层,先进行一次编译过程,将java代码编译为字节码(编译型语言的特性)
2. 运行时动态加载、解释,存在跨平台特性(解释型语言特性)
从而在语言有跨平台特性的同时,性能好于大部分解释型语言
二. 类加载相关
关键点
- class文件的结构
可以稍微关注魔数、版本号,常量池计数器、常量池等和类加载相关的部分
- 类加载的详细过程
类加载过程 广义上是【加载】【链接】【初始化】三步,其中【链接】又分为【验证】【准备】【解析】
实际的加载过程不一定是按照上述步骤顺序来的
1. 【加载】将class文件字节流加载到JVM内存中
2. 【验证】验证魔数是否正确、版本号是否兼容
3. 【加载】将静态的class文件转化为方法区中的运行时数据结构
4. 【加载】在堆中生成类的class对象,提供访问方法区运行时数据结构的入口
5. 【验证】元数据验证
6. 【验证】字节码验证
7. 【准备】申请类的静态变量内存区域,并初始化0值
8. 【解析】(不确定在哪一步完成,JVM可能进行优化)将符号引用转化为直接引用,比如代码 a = func(), 这一步就会把符号引用func转化为该函数在方法区中的地址
9. 【初始化】静态变量赋值,执行静态代码块
- 双亲委派过程以及如何破坏
被问到过的问题
- 类加载过程
- 双亲委派以及其好处
- 怎么破坏双亲委派,JDBC为什么要破坏
jdbc提供了SPI接口,即存在多个第三方实现的具体类。
在加载时,DriverManager是由启动类加载器加载的,但第三方的类无法用启动类加载器加载,故破坏双亲委派
- 双亲委派安全性、如何加载自定义的JDK核心类
https://codeantenna.com/a/rifbXvDsup
如果自定义一个java.lang.String,并破坏双亲委派机制用自定义类加载器加载,会被JVM的安全检查拦截
如果破坏JVM的安全检查,是可以加载成功的
此时JVM堆中就会有两个java.lang.String的class对象,但是JVM通过【全限定名+类加载器】保证唯一性从而分辨这两个类
- 什么时候JVM会立即对类进行初始化
1. 四条字节码指令
- new一个实例对象
- 读取类的静态成员变量
- 设置类的静态成员变量
- 调用类的静态方法
2. 反射。如Class.forName()
3. 类加载时,父类未加载
- 数组加载和类加载的区别,new数组会不会立即对类初始化
- JVM如何唯一确定一个类
全限定名+类加载器
三. JVM内存结构
关键点
- JVM各个版本下,方法区的位置变化
- 各个区域的作用以及联系
被问到过的问题
- 常量池和运行时常量池的区别、位置
- 永久代和元空间的关系和区别
- 访问类信息的两种方式
堆中对象的结构
1. 16字节的对象头:包括8字节的markword和8字节的class pointer
2. 若干字节的成员变量空间
3. 填充
这里的class pointer就是指向方法区类信息的指针
寻址有两种方式:句柄池 vs 直接指针
- JVM如何实现多态
虚函数:不仅仅是抽象方法,普通的public方法也是虚函数
非虚函数:用final限定的函数
每个类中都包含一个虚函数表,记录类中各个方法的实际入口
类加载是迭代加载的过程,加载子类前先加载父类
加载父类时会覆盖子类的虚函数表,子类加载时再覆盖方法地址为其重写的方法地址
- 栈帧中为什么要有动态链接
如策略模式,运行时才能确定父类引用指向哪个子类,调用哪个子类的方法。
类加载的解析过程是静态解析,只能把确定的符号引用修改为直接引用,而这里是不确定的,只能动态解析
故栈帧中需要存动态链接,调用时根据动态链接找到对应类的方法
- JVM逃逸分析之标量替换
逃逸分析其实是通过判断对象的生命周期而进行的一系列优化手段
- 锁消除
- 标量替换:若创建的对象只在本方法/代码块中使用,就可以将对象的成员变量拆开存到栈中,就没必要在堆中分配空间,影响后续GC
四. 堆内存结构
关键点:
- 常见的jvm堆空间分配模型
- 晋升老年代条件
- 线程堆内存分配
被问到过的问题
- 为什么要有新生代和老年代,为什么新生代要划分为eden:s1:s2 = 8:1:1
需要结合对象存活的时间分布,以及垃圾回收算法
- 进入老年代的条件
1. 年满15周岁(默认参数值,可调整)
2. 大对象(超过最大对象限制,同样是参数可调整)
3. S区中相同年龄的对象所占S区空间超50%,大于等于该年龄的对象晋升
4. eden区存活对象 > S区,直接晋升老年代
- 多线程分配堆内存时,如何保证线程安全,如何在安全的同时保证效率
保证线程安全:加锁(JVM通过乐观锁)
指针碰撞:每次创建对象时,线程CAS申请空间
解决效率低下:TLAB(Thread Local Alloc Buffer)
当多线程并发分配内存量大时,大量CAS失败会影响性能
故采用Buffer,一个线程每次不只是申请一个对象的空间,而是申请一大块空间,用完之后再CAS申请,以此减少乐观锁失败次数
五. GC相关以及调优
关键点
-
minorGC和FullGC的区别以及发生的条件
-
空间分配担保
-
Java四种引用类型
-
可达性分析/如何分析跨代引用
-
垃圾回收算法以及垃圾回收器
垃圾回收器主要关注CMS和G1
- GC异常的原因以及调优方法
被问到过的问题
- 空间分配担保判断的过程
- GC Roots都有哪些
1. 栈帧中的本地变量
2. 静态变量、常量引用对象
- 四种引用类型以及何时被回收
强引用、软引用、弱引用、虚引用
一般相关会问使用场景和案例,比如弱引用和ThreadLocal
- 三色标记法,以及JVM在并发修改时如何纠错
纠错方式:增量更新 vs 原始快照
增量更新:记录新增引用的黑色对象,一次标记后对增量的黑色对象分析。整体算法时间较久
原始快照:算法开始时记录内存快照,并根据快照扫描,不关注期间的引用关系修改。可能会产生浮动垃圾
- CMS的优缺点,G1收集器优缺点。为什么你们不用G1
JDK9之前默认的垃圾收集器是CMS,之后是G1
- MinorGC太频繁的原因,怎么办
- FullGC频繁的原因,怎么办
1. 大对象阈值太低,晋升老年代的大对象多。可修改大对象的阈值
2. 大对象太大。需要从业务逻辑出发去控制修改
3. S区太小,导致对象晋升快。可以调整S区的大小
4. 可能存在内存泄露
5. 调整晋升年龄