前言
Java虚拟机部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的帮助。
系列文章:
JVM-自动内存管理
-
说说虚拟机的运行时内存区域都有什么?
JDK1.8以前
线程私有的:程序计数器、java虚拟机栈和本地方法栈
线程共享的:方法区 和 堆
JDK1.8之后
线程私有的:程序计数器、java虚拟机栈和本地方法栈
线程共享的:元数据区 和 堆
其中,方法区修改为元数据区,并且把元数据区移动到了直接内存里
-
java虚拟机栈可能会抛出什么错误?
- 如果虚拟机栈不可动态扩展,当线程请求的栈深度大于虚拟机允许的最大深度,会抛出StackOverFlowError;
- 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,会抛OutofMemoryError
-
谈谈对象的创建过程?(重要)
下面讨论仅限于HotSpot虚拟机的对象创建过程。
- 首先,虚拟机会检测new指令对应的参数在常量池中是否可以找到对应的符号引用
- 然后,检查这个类是否已经被解析、加载、初始化过了,如果没有,首先启动类的加载过程
- 加载完毕后,就可以确定这个类的内存大小,开始分配内存
- 如果垃圾收集算法带整理功能,会使用指针碰撞算法分配
- 如果垃圾收集算法不带整理功能,会使用空闲列表分配
- 分配内存的过程中,还需要考虑线程安全的问题,可以使用两种方式解决
- CAS和不断重试
- TLAB:针对每个线程预先分配内存,线程在自己的内存区域分配即可。
- 分配完毕内存后,会给每个字段设置零值,然后设置对象头相关参数
- 最后调用init方法
-
你了解对象的内存布局吗?大致谈谈(对象在虚拟机中的数据结构)
对象在虚拟机中的数据结构包括三大部分:
- 对象头:对象头中包括了哈希吗、GC年龄、锁信息等等
- 实例数据:程序中定义的各种数据内容
- 对齐填充:HotSpot管理系统要求对象的起始地址必须为8字节的整数倍,所以在不满足时会进行对齐填充。
-
对象的访问定位有哪两种⽅式?
一种是通过句柄访问,另一种是通过直接指针访问
使用句柄访问的优点是,无论对象如何移动,栈中保存的reference是稳定的。
使用直接内存方式的优点是,和使用句柄方式相比,节省了一次指针定位的开销,可以更快的定位数据。
上面介绍了虚拟机运行时的内存区域布局,对象在内存中的数据结构,总结来看是介绍了产生对象的相关知识,既然产生了对象,在不使用时就需要垃圾收集,所以下面的问题要进入垃圾收集相关的主题了。
-
如何判断对象已死
有两种方式可以判断对象已死:
-
引用计数法:无法解决循环引用的问题
-
可达性分析:由GCROOT开始,向下搜索,搜索走过的路径叫做引用链。当一个对象到GCROOT没有引用链相连时,就证明这个对象是不可用的。
可以作为GCROOT的对象包含以下几种:
- 栈中引用的对象(包括虚拟机栈和本地方法栈)
- 静态属性引用的对象
- 常量引用的对象
-
-
谈谈对finalize方法的理解
第6个问题分析了如何判断对象已死,假如满足已死条件,虚拟机不会立即判对象死亡,而是先判断是否需要调用finalize方法。以下两个条件满足,JVM就不会调用finalize方法
- 没有覆盖finalize方法
- 已经调用过一次了
finalize方法中对象可以拯救自己,但因为运行代价高昂,不确定性大,所以不建议使用。
-
方法区中会进行垃圾回收吗?
在虚拟机规范中说过,虚拟机实现中可以不实现方法区的垃圾收集。在HotSpot虚拟机中,可以通过配置参数来开启或关闭方法区的垃圾收集。
- -Xnoclassgc:关闭类卸载
- -verbose:class:观察类类加载和卸载信息
-
方法区回收的主要是什么类型的数据?如何判断已死?
方法区中主要回收两类数据:废弃常量和无用类
如果没有任何一个引用引用了这个常量,该常量为废弃常量,可以被回收
无用类的判定条件比较多:
- 类产生的实例都被回收
- 加载类的加载器也被回收
- 类的class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-
你了解强引用、软引用、弱引用、虚引用吗?(重要)
问题6中讨论过如何判断对象已死,判断方式都离不开引用。在上面的讨论中,隐含了一个条件,就是对象要么是被引用,要么没有被引用。在JDK1.2以后,Java对引用的概念进行了扩充。引入了强引用、软引用、弱引用、虚引用的概念。
- 强引用:如果引用存在,在内存不足时也不会回收。在代码中写的变量都是强引用。
- 软引用:如果只有软引用存在,那么在内存不足时,才会把只有软引用的对象进行回收
- 弱引用:如果只有弱引用存在,每次垃圾回收时,都会去尝试回收
- 虚引用:简单理解,虚引用完全不影响垃圾回收,它最大的作用是在回收时收到通知。
上面的内容主要介绍了如何判断对象是否已死,具体如何清理已死对象?清理以后是否需要整理?都是下面的垃圾收集算法和实现垃圾收集算法的垃圾收集器考虑的内容。
-
垃圾收集有哪些算法,各⾃的特点是什么?
- 标记-清除算法:标记后,直接清除。缺点:容易产生空间碎片,影响后续分配大对象
- 复制算法:把内存一分为二,每次只在一半分配内存,如果需要垃圾收集,把存活的对象复制到另半边,直接清理原半区即可。缺点:大部分对象都存活时,复制效率过低;内存一分为二,空间利用率差。
- 标记整理算法:对标记清除算法的改进,标记以后,不直接清除,而是把存活的对象移动到一侧,清理端边界即可。
- 分代算法:根据对象存活周期的不同把内存区域划分为多块,每块使用不同的收集算法。
- 新生代:复制算法
- 老年代:标记-清除 或 标记整理算法
-
新生代的垃圾收集器有哪些?分别有什么特点?
新生代垃圾收集器有三种:
- serial: 单线程,stop the world,标记整理算法,适合运行在单CPU上的程序。
- parnew:多线程,stop the world。适合运行在多CPU上的程序,比serial提供了更多的可配置参数
- parallel scavenger:多线程,stop the world。更关注吞吐量,还提供自适应参数。
注:关注吞吐量和关注延迟的区别
吞吐量的定义:系统运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
serial和parnew关注的是每次收集的低延迟,但低延迟可能带来更多的收集次数,在计算吞吐量时,可能更小。
比如:同样的代码,原来每10秒手机一次,停顿100ms;为了更低的延迟,调小了新生代,现在每5秒停顿一次,每次收集70ms,看起来每次的延迟降低了,但是系统整体的吞吐量却降低了。
-
老年代的垃圾收集器有哪些?分别有什么特点?
老年代的垃圾收集器有四种:
- serial old: 单线程,stop the world,标记整理算法,适合运行在单CPU上的程序。
- parallel old:serial的多线程版本,stop the world。标记整理算法。
- CMS:标记清除算法,以最小延迟时间为目标。
- G1:最前沿的收集器,可以以极高概率满足GC停顿时间,还具备高吞吐量的特征
-
CMS收集器的工作流程是怎样的?
- 初始标记:stop the world。只标记GCROOT直接关联的对象,停顿时间很短
- 并发标记:GC ROOT tracing的过程,可以与用户线程一起工作
- 重新标记:stop the world。修正并发标记阶段因为用户线程导致标记出现变化的部分
- 并发清除
-
CMS收集器的缺点?以及如何解决?
- 空间碎片问题(整理一下):因为CMS使用标记清除算法,所以会产生空间碎片。CMS提供一个参数,可以控制执行N次GC后需要做一次空间压缩。 -XX:CMSFullGCsBeforeCompaction=n
- 浮动垃圾问题(用户线程并发不可避免的产生浮动垃圾):CMS在运行过程中,用户线程也在运行,在所有的标记阶段结束以后,产生的垃圾叫做浮动垃圾,这些垃圾只能等到下一次GC才会收集。
- Concurrent Mode Failure问题(用户线程并发,所以需要考虑预留一些空间):因为并发执行,所以老年代中需要预留一些空间给用户线程使用。用于空间担保和正常进阶到老年代的数据。当这个预分配的空间不足时,就会触发Concurrent Mode Failure问题,这时JVM会使用serial收集器来收集,性能反而降低。可以调优这个空间比例参数:-XX:CMSInitiatingOccupancyFraction(这个参数主要关注老年代使用率达到多少比例时,触发垃圾回收,所以为了保证用户线程正常使用,要预留小一点的这个比例,也就是较小的比例会触发CMS垃圾回收)
- cpu资源敏感:CMS默认启动的线程数为 (CPU数量+3)/4,当cpu数量较少时,会使用更大比例的线程用于垃圾回收,间接影响总吞吐量。
-
G1收集器的原理?(待完成)
上面集中讨论了垃圾回收具体的实现,基于此,可以讨论自动内存管理的另一方面--对象分配的原则。
-
说⼀下堆内存中对象的分配的基本策略
当垃圾收集算法为分代算法时,对象分配策略如下:
JVM-执行子系统
在JVM-自动内存管理 的 “谈谈对象的创建过程?”的这个问题中,在创建对象的第二步:检查这个类是否已经被解析、加载、初始化过了,如果没有,首先启动类的加载过程。我们提过类的加载过程,这部分内容会更加详细的讨论类的加载。
-
说说类加载的过程?
类加载的过程主要分为三大步骤:
-
加载
- 通过类的全限定名获取二进制字节流
- 将二进制字节流代表的静态数据结构转换为运行时数据结构
- 生成一个代表这个类的Class对象,作为运行时数据结构的访问入口
-
连接
-
验证:加载Class之前的文件格式验证;加载为运行时数据区后的相关语义验证
-
准备:为类变量分配内存和设置零值的过程
-
解析:加载为运行时数据结构以后,还需要把符号引用解析为直接引用
注:符号引用简单理解就是常量;直接引用就是具体的内存地址或者句柄。在上面第五个问题:对象的访问定位有哪两种⽅式? 中介绍过句柄和直接指针的相关概念
-
-
初始化:执行类的clinit方法,clinit方法包括静态变量的赋值和static代码块中的赋值
-
-
类加载的时机?
JVM虚拟机规范中没有明确规定加载开始的时机,而是规定了几种场景,在这些场景下下必须立即执行初始化步骤,这些场景称为类的主动引用。
- new 创建实例;设置或读取静态字段;调用类的静态方法(通过子类调用父类的静态字段,只一定会初始化父类;final修饰的常量不会触发初始化)
- 反射调用
- 父类还未初始化,会先初始化父类
- 虚拟机启动后第一个执行的主类
- MethodHandle实例解析为 设置或读取静态字段;调用类的静态方法对应的字节码指令(和第一种情况一样)
其他所有的情况称之为被动调用,不会触发类的初始化
-
类加载器有什么作用?java中有哪些类加载器?
在整个类加载的过程中,类加载器只负责 加载 阶段的 通过类的全限定名获取二进制字节流 这个阶段。
java中有三种预定义的加载器:
- 启动类加载器:负责加载lib下的相关jar,由c++实现在虚拟机内部
- 扩展类加载器:负责加载lib/ext下的jar,由java实现
- 应用类加载器:负责加载用户的class path下的jar,由java实现
-
简单说说双亲委派模型?
双亲委派模型的工作过程是:如果类没加载过,先委派给父类加载,如果父类加载失败,再由子类加载。
双亲委派模型可以保证java程序的稳定运行,比如java.lang.Object类,无论哪一个类加载器要加载这个类,最终都会委派给启动类加载器来加载,保证核心类的唯一性。