1 描述JVM类加载过程
这道题想考察什么?
了解JVM是如何加载类的,并且通过JVM类加载过程能更直观了解掌握如APT注解处理器执行、热修复等技术的本质
考察的知识点
JVM类加载过程
考生如何回答
类加载的本质
一般情况下,类的数据都是在Class
文件中。将描述类的数据 从Class
文件加载到内存 同时 对数据进行校验、转换解析 和 初始化,最终形成可被虚拟机直接使用的Java
使用类型。
类加载过程
java类加载过程:加载–>验证–>准备–>解析–>初始化,之后类就可以被使用了。绝大部分情况下是按这
样的顺序来完成类的加载全过程的。但是是有例外的地方,解析也是可以在初始化之后进行的,这是为了支持
java的运行时绑定,并且在一个阶段进行过程中也可能会激活后一个阶段,而不是等待一个阶段结束再进行后一个阶段。
1.加载
加载时jvm做了这三件事:
1)通过一个类的全限定名来获取该类的二进制字节流
2)将这个字节流的静态存储结构转化为方法区运行时数据结构
3)在内存堆中生成一个代表该类的java.lang.Class对象,作为该类数据的访问入口
2.验证
验证、准备、解析这三步可以看做是一个连接的过程,将类的字节码连接到JVM的运行状态之中
验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会威胁到jvm的安全
验证主要包括以下几个方面的验证:
1)文件格式的验证,验证字节流是否符合Class文件的规范,是否能被当前版本的虚拟机处理
2)元数据验证,对字节码描述的信息进行语义分析,确保符合java语言规范
3)字节码验证 通过数据流和控制流分析,确定语义是合法的,符合逻辑的
4)符号引用验证 这个校验在解析阶段发生
3.准备
为类的静态变量分配内存,初始化为系统的初始值。对于final static修饰的变量,直接赋值为用户的定义值。如下面的例子:这里在准备阶段过后的初始值为0,而不是7:
public static int a=7
4.解析
解析是将常量池内的符号引用转为直接引用(如物理内存地址指针)
5.初始化
到了初始化阶段,jvm才真正开始执行类中定义的java代码
1)初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集
类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
2)当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化。
3)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
2 请描述new一个对象的流程
这道题想考察什么?
对JVM的理解
考察的知识点
JVM 对象分配、并发安全
考生应该如何回答
JVM创建对象的过程如下图:
虚拟机遇到一条new指令时,首先检查是否被类加载器加载,如果没有,那必须先执行相应的类加载过程。类加载就是把class加载到JVM的运行时数据区的过程。
检查加载
首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查类是否已经被加载、解析和初始化过。
符号引用:以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
假设People类被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址。
分配内存
完成类的加载检查后,虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
指针碰撞
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为—指针碰撞。
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为—空闲列表。
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
- 如果是Serial、ParNew等带有压缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。
- 如果是使用CMS这种不带压缩(整理)的垃圾回收器的话,理论上只能采用较复杂的空闲列表。
并发安全
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决这个问题有两种方案:
CAS
对分配内存空间的动作进行同步处理—实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
分配缓冲
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。
JVM在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。
TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。
TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。
默认情况下启用允许在年轻代空间中使用线程本地分配块(TLAB)。要禁用TLAB,需要指定-XX:-UseTLAB
。
内存空间初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。(如int值为0,boolean值为false等等)。
设置
完成空间初始化后,虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes在Java hotspot VM内部表示为类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
对象初始化
在以上工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化(构造方法),这样一个真正可用的对象才算完全产生出来。
3 Java对象会不会分配到栈中?
这道题想考察什么?
创建的对象是否都在堆中,如果不是,对照JVM运行时数据区堆栈相关内容,能够把控对象不在堆中对程序的影响
考察的知识点
逃逸分析
考生应该如何回答
Java对象可能会分配到栈中。
逃逸分析
逃逸分析指的是分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。
如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高JVM的效率。如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。
逃逸分析的触发前提条件必须触发JIT执行
public class EscapeAnalysisTest{public static void main(String[] args){long start = System.currentTimeMillis();for (int i = 0; i < 50000000; i++){allocate();}System.out.println((System.currentTimeMillis() - start) + " ms");}static void allocate(){Object obj = new Object();}
}
在上述代码中,Object对象属于不可逃逸,JVM可以做栈上分配。在启动JVM时候,通过 -XX:-DoEscapeAnalysis
参数可以关闭逃逸分析(JVM默认开启)。
开启逃逸分析:
关闭逃逸分析:
测试结果可见,开启逃逸分析对代码的执行性能有很大的影响!
最后
此面试题会持续更新,请大家多多关注!!!!
有需要以上面试题的朋友,可以扫描下方二维码免费领取~~
扫码还可以享受ChatGPT机器人的服务,可不要错过!!!