java之类加载机制

 

1.Java类加载机制 双亲委托模式 

java之类加载机制 类加载时序图

加载阶段

类加载阶段是由类加载器负责根据一个类的全名类读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区内,然后将其转换为一个与目标类型对应的java.lang.Class
对象实例,这个Class对象在日后就会作为方法区中的该类的各种数据的访问入口。

JVM支持两种类型的类加载器,分别为 引导类加载器(BootStrap ClassLoader) 和 自定义类加载器(User-Defined Classloader)

我们常用的包括:Extension ClassLoader、 Application ClassLoader这三个类加载器。

BootStrap ClassLoader:

      启动类加载器,由C++语言编写,并嵌套在JVM内部,主要负责加载JAVA_HOME/lib目录中的所有类型。

   根类加载器由C++编写,负责加载$JAVA_HOME中jre/lib里所有的class

Extension ClassLoader:

     ExtClassLoader派生于ClassLoader,  采用Java语言编写,负责加载 ext文件夹(jre\lib\ext)内的类;

它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。 

Application ClassLoader:

     AppClassLoader派生于ClassLoader, 采用Java语言编写,负责加载 应用程序级别的类路径,提供的环境变量路径等。

    它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中。

 

2.双亲委派机制:
其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
如果父类加载器可以完成类加载任务,就成功返回, 倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

双亲委派机制的优势:
采用双亲委派模式的是 好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,
通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,
通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

双亲委托模式

一种被JVM设计者制定的类加载器的加载机制。按照双亲委托模式的规则,除了启动类加载器之外,程序中每一个类加载器都应该拥有一个超类加载器,比如Application ClassLoader
的超类加载器就是Extension ClassLoader,开发人员自定义的加载器的超类就是Application ClassLoader,当一个类加载器收到一个加载任务时,并不会立即展开加载
,而是将加载任务委派给它的超类加载器去执行,每一层的加载器都采用这种方式,直到委派给顶层的启动类加载器为止,如果超类无法加载该类,则会将类的加载内容退回给它的下一层
加载器去加载。双亲委托模式的优点就是:能有有效的确保一个类的全局唯一性。

  java之类加载机制 双亲委托模式

注意:Java虚拟机并没有明确要求类加载器的加载机制一定要使用双亲委托模式,只是建议这样做,而在Tomcat中,当默认的类加载器接收到一个加载任务时,首先会由
它自动加载,当加载失败,才会将类委派给它的超类加载器去执行,这是Servlet规范推荐的一种做法。

连接阶段

连接阶段由验证、准备、解析3个阶段构成。

验证

验证主要任务就是验证类信息是否符合JVM规范,是否是一个有效的字节码文件,而验证的内容涵盖了类数据信息的格式验证、语义分析、操作验证等

准备

准备阶段主要任务就是为类中所有静态变量分配内存空间,并为其设置一个初始值(由于对象还没有产生,因此实例变量将不在此操作范围内)

解析

解析阶段主要任务就是将常量池中所有的符号引用全部转换为直接引用,由于Java虚拟机规范中并没有明确要求解析阶段一定要按照顺序执行,因此解析阶段可以等到初始化
以后再执行。

初始化阶段

初始化阶段中,JVM会将一个类中所有被static关键字标识的的代码统统执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖掉之前的准备阶段中JVM为其设置的初始值,
如果执行的是static代码块 JVM就将会执行static代码中的所有操作。

 ======

JVM类加载
类加载过程
加载
这一步骤通常由JVM提供的类加载器来完成,我们也可以通过实现ClassLoder来自定义一个类加载器。
指将类的class文件读入内存当中,并为之创建一个java.lang.Class的对象。
连接
验证
验证是连接阶段的第一步,这一步是为了保证当前Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
1. 文件格式验证
- 是否以魔数(0xCAFEBABE)开头
- 主次版本号是否在当前虚拟机处理范围内
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
- ......
2. 元数据验证
- 验证这个类是否有父类,除了java.lang.Object外,其他类必须有父类。
- 这个类是否继承了不允许被继承的类(如被final修饰的类)
- 如果此类不是抽象类,是否实现了父类或接口的所有要求实现的方法。
- 类中的属性是否与父类中的属性产生了冲突(如覆盖了父类的final属性、不符合规则的方法重载,例如方法名相同但参数列表一致)
- ......
3. 字节码验证
主要目的是通过数据流和控制流来分析其语义是合法的、符合逻辑的。这个阶段将对方法体进行校验(元数据验证主要是对类进行验证)。
保证被校验的类在运行时不会对虚拟机产生危害。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。说白了就是操作数栈中放置了int类型的数据,本地变量表却存放了long类型的数据。
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中转换是有效的。例如把父类对象赋值给子类类型、把对象赋值给与这个对象毫不相关的类型等。
- ......
4. 符号引用验证
符号引用就是在一个类中,对其他类的引用,在将代码编译成class文件时,编译器并不知道引用类的实际内存地址,这时只能用一个唯一的符号来进行表示。
在解析阶段会将符号引用替换成直接引用。
这一步骤主要时为了保证符号引用类能被正确访问到,不会出现类无法访问、非法访问等情况。
- 符号引用通过字符串的全限定名是否能找到对于的类。
- 在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的访问性(public、private、protected、default)是否可被当前类访问。
- ......

准备
这一阶段主要是将类中的静态变量分配内存,并设置默认初始值。
注意:设置的默认初始值并不是代码中实际的值,例如int = 1,这时设置的值并不是1,而是0(因为是int,默认0)。
解析
这一阶段主要就是将类中的符号引用替换成直接引用。
直接引用:指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定要加载进来的。
初始化
这一阶段主要是将静态变量进行赋值,此处赋的值与准备过程中赋的值不同,准备中赋的值为默认初始值,而这里是代码中真实的值。
注意:初始化是有顺序的,按照写的代码的顺序向下初始化。且不能访问还未被变量初始化的静态变量。

// 此写法中,初始化完毕后i最终的值为0。因为先将i赋值为1,然后static代码块将i又赋值为0。
static int i = 1;
static {
i = 0;
}

// 此写法中,初始化完毕后i最终的值为1。因为static代码块将i赋值为0。然后将i又赋值为1。
static {
i = 0;
}
static int i = 1;

// 此写法中,将在System.out.print(i)这一行报错。
// 因为不能访问还未被变量初始化的值。但可以赋值(i=0赋的值将被变量赋值那一行覆盖掉,如案例2中结果一样)
static {
i = 0;
System.out.print(i);
}
static int i = 1;

 

上一篇:阅读 Flink 源码前必会的知识 SPI 和 Classloader


下一篇:java如何防止反编译