JVM初级面试题
JVM初级面试题
一. JVM的构成
-
类加载器
-
运行时数据区(内存)
-
程序计数器
记录下一条指令的地址, 简单来说就是线程执行到了哪, CPU时间片切换回来程序需要知道从哪开始执行, 在物理上是通过寄存器实现
-
方法区
方法区是一种规范, 规定了存储类相关的信息, 永久代和元空间是他的实现
JDK8之前是在堆内存中分一块区域(逻辑)叫做永久代, 用来存放运行时常量池和类信息
JDK8的时候把永久代修改成了元空间, 在本地内存(操作系统内存)中开辟一块区域, 不再使用堆内存的一部分, 运行时常量池仍然放在堆中
什么是常量池, 什么是运行时常量池?
常量池就是一张常量表, 存在class文件中, 通过常量符号可以找到对应的方法名, 参数类型, 字面量等信息
运行时常量池就是在运行中, class中的常量池信息会加载到内存中
-
堆
线程共享的内存区域, 存放对象
-
虚拟机栈(线程栈)
每个线程默认1M的栈内存用来存储栈帧
-
本地方法栈
-
-
执行引擎
-
解释器
解释class文件, 翻译成机器码
-
即时编译器
会对热点代码优化
-
GC
垃圾收集器
-
-
本地方法接口
二. 类的加载
-
加载
加载进内存, 生成对应的C++结构instanceKlass, 生成代表这个类的Class对象, 作为对外暴露类的入口
-
连接
-
验证
验证格式, 安全性检查
-
准备
给静态变量分配内存, 设置默认值, 如果是final的基本类型, 或者字符串, 赋值在准备阶段完成
-
解析
将常量池中的符号引用解析成直接引用, 就是把原本class中存储的常量符号, 比如方法名, 字面量等信息, 解析成真正的内存地址
-
-
初始化
执行类的构造方法, 就是静态代码块
初始化的情况
main方法的类总是被首先初始化
首次访问静态变量或静态方法
子类初始化时, 父类还没初始化, 会跟着初始化
Class.forName()
new对象
不会初始化的情况
访问final的静态常量, 基本类型或者字符串
类.class
创建类的数组的时候
手动使用加载器加载类的时候, 比如: classLoad.loadClass("xxx.xxx.xxx"), 只会加载类, 不会初始化
Class.forName()第二个参数为false的时候
三. 双亲委派
类加载器分四种
- BootstrapClassLoader启动类加载器, C++实现, 无法访问, 加载jre/lib下的类
- ExtensionClassLoader扩展类加载器, 上级为BootstrapClassLoader, 加载jre/lib/ext下的类
- ApplicationClassLoader应用程序加载器, 上级为ExtensionClassLoader, 加载classpath下的类
- 自定义加载器, 继承抽象类ClassLoader, 重写findClass方法, 上级为ApplicationClassLoader
双亲委派指的是类加载器加载1个类的时候, 会优先从上级获取, 如果没有才自己加载
四. 引用的类型
一般来说有四种, 但还听说过第五种
-
强引用
普通对象的引用, 平时我们一般都使用这个
-
软引用
当GC发生的时候, 并且内存不够, 此时会把软引用的对象释放, 可以配合引用队列使用
-
弱引用
当GC发生的时候, 不管内存够不够, 都会释放该引用的对象, 可以配合引用队列使用
-
虚引用
虚引用主要是为了跟踪垃圾回收过程, 必须配合引用队列使用, 在对象被垃圾回收的时候, 会将这个引用加入队列, 在虚引用出队列前, 不会彻底销毁该对象, 一般用来释放堆外内存(直接内存)
-
终结器引用
虚拟机会给我们的对象创建终结器引用, 当对象被回收时, 会把终结器引用放入引用队列, 等待finalizeHander线程调用finalize方法
五. 垃圾回收算法
先了解1下如果怎么确定一个对象是否是垃圾: 引用计数, 可达性分析(根可达), java使用后者, 听说最早版本的python是使用引用计数
- 标记清除, 优点: 速度快, 缺点: 内存碎片
- 标记整理, 优点: 没有内存碎片, 缺点: 速度慢
- 复制, 优点: 没有内存碎片, 缺点: 浪费一部分空间
六. 垃圾收集器
-
新生代
- Serial, 单线程, 使用复制算法
- ParNew, 多线程并行, 使用复制算法
- Parallel, 多线程并行, 吞吐量优先
-
老年代
-
SerialOld, 单线程, 使用标记清除整理算法
-
ParallelOld, 多线程并行, 吞吐量优先, 使用标记清除整理算法
-
CMS, 多线程并行+并发, 响应时间优先, 使用标记清除清除算法
CMS垃圾回收的4个步骤
- 初始标记(不能并发)
- 并发标记(可以并发)
- 重新标记(不能并发)
- 并发清除(可以并发)
由于使用标记清除算法, 所以无法整理内存碎片, 当内存无法满足程序需求的时候, 会启动SerialOld垃圾收集器, 导致一次非常慢的FullGC, 可以修改配置使FullGC的时候采用MSC算法压缩堆内存, 但是使用MSC算法合并整理内存的时候, 不能并发
-
-
整堆
G1, 多线程并行+并发, 同时注重吞吐量和响应时间, 将整个堆分成多个Region区域(512K), 整体上是标记整理算法, 两个区域之间是复制算法
G1垃圾收集器的三种情况(网上资料计较混乱, 无法验证真伪)
- 新生代垃圾收集
- 混合垃圾收集(老年代内存占用达到45%, 可以通过参数设置)
- FullGC
整个过程: 初始标记(不能并发), 并发标记(三色标记), 最终标记(不能并发), 垃圾清除
七. MinorGC/YoungGC和FullGC
MinorGC/YoungGC是清理新生代的垃圾收集
FullGC是清理老年代的垃圾回收, JDK8之前永久代内存不够也会触发FullGC
八. 新生代老年代
-
新生代
默认占用堆内存的三分之一
- 伊甸区, 默认新生代的十分之八, 新建的对象放在伊甸区, 每次发生YongGC时, 会把不需要清除的对象放入幸存区from, 并且年龄加1
- 幸存区from, 默认新生代的十分之一, 每次发生YongGC时, 会把幸存区from的对象复制到幸存区to, 然后幸存区to和幸存区from身份交换, 幸存的对象年龄加1, 当达到16岁(默认, 可以配置)时, 扔到老年代
- 幸存区to, 默认新生代的十分之一
当创建的对象特别大, 伊甸区内存不够时, 或者幸存的对象超过幸存区50%大小的时候, 直接扔到老年代
-
老年代
老年代里的对象一般来说是存活比较久的, 当老年代满了的时候会发生FullGC
九. happen-before
- 程序次序规则: 在一个线程内哪怕指令重排序, 但代码产生的结果要保证和不重排序一致
- 管程锁定规则: 解锁之前的改动要对获取锁的线程可见
- volatile变量规则: 对volatile变量写的操作一定对读的操作可见
- 线程启动规则: 在线程启动之前的操作, 一定要对被启动的线程可见
- 线程终止规则: 线程结束的时候, 确保结束之前的操作对其他线程可见
- 线程中断规则: 线程中断的时候, 确保中断之前的操作对该线程可见
- 传递规则: A线程可见B线程的操作, B线程可见C线程的操作, 那么要确保A线程可见C线程的操作
- 对象终结规则: 确保对象的构造方法的操作对finalize方法可见
十. GC调优
其实这个没啥好说的, 根据不同的情况再调整参数, 此处针对Web场景补充1点点
一般我们Web项目的对象都是垃圾, 都是数据库查出来, 返回给前端, 然后就可以回收了, 所以对应的应该把新生代调大一些, 最好伊甸区大于: 预估并发量 * 请求平均消耗的内存