什么是类加载器?
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中 ,从而让Java程序能够启动起来。
类加载的过程
1. 加载
该阶段虚拟机需要完成三件事:
- 通过一个类的全限定类名获取定义类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据区
- 在内存中生成对应该类的 Class 实例,作为方法区这个类的数据访问入口
2. 验证
确保 Class 文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入错误或恶意企图的字节流而导致系统受攻击。
验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证重要带是非必须,如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。
3. 准备
准备阶段正式为类变量(即被 static
修饰的变量)分配内存并设置零值。
如果是 final static
常量则在这一阶段直接赋予代码中指定的值,而非零值,
4. 解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用:以一组符号来描述所引用的目标。比如用字符串
"java/io/PrintStream"
来表示PrintStream
类。它和内存布局无关。 直接引用:直接指向目标的指针】相对偏移量或者是一个能间接定位到目标的句柄。这是和内存布局直接相关的。
解析动作主要针对类或接口、字段、类方法、接口方法等引用进行。这个过程不一定在初始化之前就全部完成,JVM规范允许“懒解析”,即在第一次使用某个符号引用时才去解析它。5. 初始化
这是类加载过程的最后一步,也是真正开始执行程序员编写的 Java 代码的阶段。
目标:执行<clinit>()
方法——该方法是Java编译器自动收集类中所有静态变量的赋值动作和静态代码块(static{}) 中的语句合并产生的。
特性:- 执行时机:只有当类被“主动使用”时才会出发初始化,比如
new
一个对象、访问类的静态字段或静态方法等。 - 父类优先:如果一个类有父类,那么初始化子类之前,JVM会保证其父类已经被初始化。
线程安全:JVM会保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁、同步。即同一个类只会被初始化一次。总结
- 加载:找到
.class
文件,加载进内存。 - 验证:检查文件和代码的安全性。
- 准备:为
static
变量分配内存并设置零值(final static
常量除外)。 - 解析:把代码中的符号名(如类名、方法名)换成直接的内存地址。
- 初始化:执行
static
变量的赋值和static
代码块,完成类的首次启动
类加载器有哪些种类?
- 启动类加载器(加载JAVA_HOME/jre/lib目录下的库)
- 扩展类加载器(加载JAVA_HOME/jre/lib/ext目录下的库)
- 应用类加载器(加载开发者自己编写的Java类)
- 自定义类加载器(实现自定义类加载规则)
什么是双亲委派模型?
当一个类加载器收到加载类的请求时,它不会自己先去尝试加载这个类,而是会把这个请求委派给它的父类加载器去完成。这个过程会一直向上委派,直到到达顶层的启动类加载器(Bootstrap ClassLoader)。只有当父类加载器反馈自己无法完成这个加载请求(即在它的搜索范围内没有找到所需的类)时,子加载器才会自己去尝试加载。
JVM为什么采用双亲委派机制?
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
- 为了安全,保障类库API不会被修改。