JVM秋招总结

JVM是Java相关知识中重要的一大块,这里记录一下自己的学习思路,以及印象比较深刻的知识点和面试问题

个人总结思路

思路顺着一次Java程序运行中,涉及到的JVM部分总结

  1. 首先为什么要有JVM,会有什么好处
  2. 程序运行中,JVM会把javac编译后的class文件加载到JVM内存中
  3. 类加载会加载到JVM内存结构中的哪些区域,指令执行时又会涉及哪些区域呢
  4. 类加载后,可能会创建对象,又涉及到堆内存的结构、堆内存分配
  5. 运行一段时间后,无可用堆内存创建新对象,需要进行GC

一. 为什么要有JVM

简单来说就是一句话:一次编译,多平台运行

被问到过的问题

  1. java和c++、python语言的比较
  2. 在32位机和64位机上,java创建一个int变量,分别需要多少bit空间
javac编译后生成的字节码是平台无关的
无论是32位机还是64位机,什么操作系统,JVM中int就是32bit,long就是64bit
  1. 编译型语言和解释性语言的区别,java是哪种语言
编译型语言:c、c++、golang...编译结果是一个可执行文件,后面直接运行即可,但存在跨平台的编译环境不同的问题
解释型语言:python、js...运行时才通过解释器解析代码,性能较差,但是可以通过解释器解决跨平台的问题

java:介于编译型语言和解释型语言之间
1. 通过JVM作为中间层,先进行一次编译过程,将java代码编译为字节码(编译型语言的特性)
2. 运行时动态加载、解释,存在跨平台特性(解释型语言特性)
从而在语言有跨平台特性的同时,性能好于大部分解释型语言

二. 类加载相关

关键点

  1. class文件的结构
可以稍微关注魔数、版本号,常量池计数器、常量池等和类加载相关的部分
  1. 类加载的详细过程
类加载过程 广义上是【加载】【链接】【初始化】三步,其中【链接】又分为【验证】【准备】【解析】
实际的加载过程不一定是按照上述步骤顺序来的
1. 【加载】将class文件字节流加载到JVM内存中
2. 【验证】验证魔数是否正确、版本号是否兼容
3. 【加载】将静态的class文件转化为方法区中的运行时数据结构
4. 【加载】在堆中生成类的class对象,提供访问方法区运行时数据结构的入口
5. 【验证】元数据验证
6. 【验证】字节码验证
7. 【准备】申请类的静态变量内存区域,并初始化0值
8. 【解析】(不确定在哪一步完成,JVM可能进行优化)将符号引用转化为直接引用,比如代码 a = func(), 这一步就会把符号引用func转化为该函数在方法区中的地址
9. 【初始化】静态变量赋值,执行静态代码块
  1. 双亲委派过程以及如何破坏

被问到过的问题

  1. 类加载过程
  2. 双亲委派以及其好处
  3. 怎么破坏双亲委派,JDBC为什么要破坏
jdbc提供了SPI接口,即存在多个第三方实现的具体类。
在加载时,DriverManager是由启动类加载器加载的,但第三方的类无法用启动类加载器加载,故破坏双亲委派
  1. 双亲委派安全性、如何加载自定义的JDK核心类
https://codeantenna.com/a/rifbXvDsup
如果自定义一个java.lang.String,并破坏双亲委派机制用自定义类加载器加载,会被JVM的安全检查拦截
如果破坏JVM的安全检查,是可以加载成功的
此时JVM堆中就会有两个java.lang.String的class对象,但是JVM通过【全限定名+类加载器】保证唯一性从而分辨这两个类
  1. 什么时候JVM会立即对类进行初始化
1. 四条字节码指令
- new一个实例对象
- 读取类的静态成员变量
- 设置类的静态成员变量
- 调用类的静态方法
2. 反射。如Class.forName()
3. 类加载时,父类未加载
  1. 数组加载和类加载的区别,new数组会不会立即对类初始化
  2. JVM如何唯一确定一个类
全限定名+类加载器

三. JVM内存结构

关键点

  1. JVM各个版本下,方法区的位置变化
  2. 各个区域的作用以及联系

被问到过的问题

  1. 常量池和运行时常量池的区别、位置
  2. 永久代和元空间的关系和区别
  3. 访问类信息的两种方式
堆中对象的结构
1. 16字节的对象头:包括8字节的markword和8字节的class pointer
2. 若干字节的成员变量空间
3. 填充

这里的class pointer就是指向方法区类信息的指针
寻址有两种方式:句柄池 vs 直接指针
  1. JVM如何实现多态
虚函数:不仅仅是抽象方法,普通的public方法也是虚函数
非虚函数:用final限定的函数

每个类中都包含一个虚函数表,记录类中各个方法的实际入口
类加载是迭代加载的过程,加载子类前先加载父类
加载父类时会覆盖子类的虚函数表,子类加载时再覆盖方法地址为其重写的方法地址
  1. 栈帧中为什么要有动态链接
如策略模式,运行时才能确定父类引用指向哪个子类,调用哪个子类的方法。
类加载的解析过程是静态解析,只能把确定的符号引用修改为直接引用,而这里是不确定的,只能动态解析
故栈帧中需要存动态链接,调用时根据动态链接找到对应类的方法
  1. JVM逃逸分析之标量替换
逃逸分析其实是通过判断对象的生命周期而进行的一系列优化手段
- 锁消除
- 标量替换:若创建的对象只在本方法/代码块中使用,就可以将对象的成员变量拆开存到栈中,就没必要在堆中分配空间,影响后续GC

四. 堆内存结构

关键点:

  1. 常见的jvm堆空间分配模型
  2. 晋升老年代条件
  3. 线程堆内存分配

被问到过的问题

  1. 为什么要有新生代和老年代,为什么新生代要划分为eden:s1:s2 = 8:1:1
需要结合对象存活的时间分布,以及垃圾回收算法
  1. 进入老年代的条件
1. 年满15周岁(默认参数值,可调整)
2. 大对象(超过最大对象限制,同样是参数可调整)
3. S区中相同年龄的对象所占S区空间超50%,大于等于该年龄的对象晋升
4. eden区存活对象 > S区,直接晋升老年代
  1. 多线程分配堆内存时,如何保证线程安全,如何在安全的同时保证效率
保证线程安全:加锁(JVM通过乐观锁)

指针碰撞:每次创建对象时,线程CAS申请空间

解决效率低下:TLAB(Thread Local Alloc Buffer)
当多线程并发分配内存量大时,大量CAS失败会影响性能
故采用Buffer,一个线程每次不只是申请一个对象的空间,而是申请一大块空间,用完之后再CAS申请,以此减少乐观锁失败次数

五. GC相关以及调优

关键点

  1. minorGC和FullGC的区别以及发生的条件

  2. 空间分配担保

  3. Java四种引用类型

  4. 可达性分析/如何分析跨代引用

  5. 垃圾回收算法以及垃圾回收器

垃圾回收器主要关注CMS和G1
  1. GC异常的原因以及调优方法

被问到过的问题

  1. 空间分配担保判断的过程
  2. GC Roots都有哪些
1. 栈帧中的本地变量
2. 静态变量、常量引用对象
  1. 四种引用类型以及何时被回收
强引用、软引用、弱引用、虚引用
一般相关会问使用场景和案例,比如弱引用和ThreadLocal
  1. 三色标记法,以及JVM在并发修改时如何纠错
纠错方式:增量更新 vs 原始快照
增量更新:记录新增引用的黑色对象,一次标记后对增量的黑色对象分析。整体算法时间较久
原始快照:算法开始时记录内存快照,并根据快照扫描,不关注期间的引用关系修改。可能会产生浮动垃圾
  1. CMS的优缺点,G1收集器优缺点。为什么你们不用G1
JDK9之前默认的垃圾收集器是CMS,之后是G1
  1. MinorGC太频繁的原因,怎么办
  2. FullGC频繁的原因,怎么办
1. 大对象阈值太低,晋升老年代的大对象多。可修改大对象的阈值
2. 大对象太大。需要从业务逻辑出发去控制修改
3. S区太小,导致对象晋升快。可以调整S区的大小
4. 可能存在内存泄露
5. 调整晋升年龄
posted @ 2022-11-23 20:04  BuptWade  阅读(28)  评论(0编辑  收藏  举报