持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情
面试官:Java对象类加载的过程了解吗?
JVM加载一个类
一个类从加载到使用,一般会经历下面的这个过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
- 验证阶段:根据Java虚拟机规范,来校验你加载进来的.class文件中的内容是否符合规范。
- 准备阶段:给类分配一定内存空间,给类变量分配内存空间,默认初始值。
- 解析阶段:把符号引用替换为直接引用的过程。
- 初始化阶段:new对象时,或包含main方法的主类,必须立马初始化,如果该类的父类未初始化,必须先初始化他的父类。
类加载器
面试官:类加载器有哪些类型呢?
- 启动类加载器:负责加载Java目录下的核心类 -> lib目录;
- 扩展类加载器:加载lib/ext目录中的类;
- 应用程序类加载器:加载classPath环境变量所指定路径中的类 -> 自己写好的类;
- 自定义类加载器:根据自己的需求加载你的类;
双亲委派机制
面试官:说说双亲委派机制吧
双亲委派,主要出于安全来考虑。
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。
就是假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载,但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。
这样的话,可以避免多层级的加载器结构重复加载某些类。
JVM内存区域
面试官:JVM中有哪些内存区域?
线程共享
- 存放类的方法区:Metaspace,可以认为是“元数据空间”这样的意思。当然这里主要还是存放我们自己写的各种类相关的信息。
- Java堆内存:这里就是存放我们在代码中创建的各种对象的。
线程私有
- 执行代码指令用的程序计数器:我们写好的Java代码会被翻译成字节码,对应各种字节码指令,然后字节码指令一定会被一条一条执行,这样才能实现我们写好的代码执行的效果。所以当JVM加载类信息到内存之后,实际就会使用自己的字节码执行引擎,去执行我们写的代码编译出来的代码指令。那么在执行字节码指令的时候,JVM里就需要一个特殊的内存区域了,那就是“程序计数器”,这个程序计数器就是用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指令。JVM是支持多个线程的,所以其实你写好的代码可能会开启多个线程并发执行不同的代码,所以就会有多个线程来并发的执行不同的代码指令。因此每个线程都会有自己的一个程序计数器,专门记录当前这个线程目前执行到了哪一条字节码指令了。
- Java虚拟机栈:Java代码在执行的时候,一定是线程来执行某个方法中的代码,因此,JVM必须有一块区域是来保存每个方法内的局部变量等数据的,这个区域就是Java虚拟机栈。每个线程都有自己的Java虚拟机栈,比如main线程就会有自己的一个Java虚拟机栈,用来存放自己执行的那些方法的局部变量。如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧,栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等东西。
- 其他内存区域:在JDK很多底层API里,比如IO相关的,NIO相关的,网络Socket相关的,很多地方都不是Java代码了,而是走的native方法去调用本地操作系统里面的一些方法,可能调用的都是c语言写的方法,或者一些底层类库,在调用这种native方法的时候,就会有线程对应的本地方法栈,这个里面也是跟Java虚拟机栈类似的,也是存放各种native方法的局部变量表之类的信息。还有一个区域,是不属于JVM的,通过NIO中的allocateDirect这种API,可以在Java堆外分配内存空间。然后,通过Java虚拟机里的DirectByteBuffer来引用和操作堆外内存空间。其实很多技术都会用这种方式,因为有一些场景下,堆外内存分配可以提升性能。