1- 类加载的时机
与其他语言不一样,java的类的加载、连接和初始化都是运行期间完成的,在初始化之前,必须要完成加载、验证、准备。所以说要对类进行初始化就是要对类进行加载的时机。
1.1- 以下为5种会触发类初始化的情况
有且只有这5种情况会触发类的初始化,也成为对一个类的主动引用,其他的引用类的方式都不会触发初始化,称为被动引用。
- 创建类的实例、也就是new一个对象的时候
- 访问某个类或接口的静态变量、或者对该静态常量赋值,调用静态方法(除了被final修饰,以及已在编译期间把结果放入常量池的静态字段)
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有加载,就会首先触发其初始化。
- 当初始化一个类的子类,会首先初始化子类的父类。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会首先初始化这个类。
1.2- 几个典型的被动引用类的例子
- 通过子类引用父类的静态字段,不会导致子类的初始化,而是对父类进行初始化
- 通过数组定义来引用类,不会触发此类的初始化
MyClass [] a = new MyClass[10];
这将会加载数组类的初始化,并不会触发MyClass类的加载 - 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。通过常量传播优化,这两个类实际上已经没有任何关联了。
这里区分一下动态加载和静态加载,new创建对象时静态加载类,因为类必须在编译阶段提供,forName动态加载类,编译阶段不必提供。比如new一个对象,则编译的时候必须提供这个类的定义及文件在一起编译,否则会抛出ClassNotFound的异常,而用forName在进行编译是不用提供这个类的定义以及文件,而是在运行时虚拟机去指定路径上,比如classpath上主动搜寻这个类的定义文件(也可以从网络上获取class文件的二进制流)。
2- 类加载的过程
将二进制字节码文件转换成JVM中的Class对象,初始化不是类加载时必须触发的。类加载的各个阶段都是按照顺序开始的,但是在同一时间可能会出现多个阶段混合执行的情况。
2.1- 加载
获取类的class文件中的二进制数据,读到内存中。
将其代表的静态存储结构转化成为方法区的运行时数据结构。
-
创建一个这个类的Class对象,作为方法去此类数据的访问入口。
注意:Class对象封装了类在方法区内的数据结构,并向java程序员提供了访问方法区内的数据结构的接口,hotSpot虚拟机的Class对象在方法区。
数组类本身不通过类加载器创建,它是java虚拟机直接创建的,递归采用数组元素的类加载过程去加载这个数组元素
2.2- 验证
确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全(文件格式,元数据、字节码、符号引用验证)
2.3- 准备
为类变量分配内存并设置类变量初始值(各种数据类型的零值)的阶段,这些内存将在方法区中进行分配。但是如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会初始化为ConstantValue属性指定的值。
2.4- 解析
- 将常量池内的符号引用替换成直接引用(类或接口,字段,方法等)
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面常量,只要使用时能无歧义地定位到目标即可。符号引用于虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用目标必定存在与内存中。
2.5- 类的初始化
执行类构造器<clinit>()方法的过程:
<clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,不需要先初始化父类构造器,也非必须。
2.6- 类加载器(双亲委派模型)
- Bootstrap ClassLoader
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类,是虚拟机的一部分,HotSpot启动时会初始化此ClassLoader,开发者无法直接获取启动类加载器的引用,所以不允许直接通过引用进行操作。其他的ClassLoader都是由java实现的,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。当运行一个程序时,JVM启动,运行Bootstrap ClassLoader,该ClassLoader加载Java核心API(Extension ClassLoader、App ClassLoader包含在内,也被加载),然后调用Extension ClassLoader加载拓展API,最后调用App ClassLoader加载CLASSPATH目录下定义的Class。
- Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或者被java.ext.dir系统变量所指定的路径中的所有类库。
- App ClassLoader
负责加载classpath中指定的jar包目录中的class,被称为系统加载器,是ClassLoader.getSystemClassLoader()方法的返回值,如果用户没有自定义过自己的类加载器,一般情况下这就是程序中默认的类加载器。AppClassloader加载器的父加载器是ExtClassloader
- Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat,jboss都会根据j2ee规范自行实现ClassLoader;用户自定义的无参加载器的父类加载器默认是AppClassloader加载器
双亲委派模型
委派过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父类加载器反馈自己无法完成这个加载请求,搜索范围内没有找到所需的类时,子加载器才会尝试自己加载。
双亲委派模型使得类具有带有优先级的层次关系,避免同一个类在不同类加载器环境中发生混乱,导致出现不同类加载器都加载一个类的情况。比如java.lang.Object存在于启动类的搜索路径上,所以无论哪个类加载器要加载这个类最终都会委派给启动类加载器,因此Object类在程序的各种类加载器环境下都是同一个类。
比较两个类是否相等:
由同一个类加载器加载,类的全限定名相等
其中相等的含义是指:通过** equals()、isAssignableFrom()、isInstance()、instanceof关键字 **做对象所属关系判定情况。
package jvm;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by zhanglbjames@163.com on 2017/4/11.
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoader myLoader = new ClassLoader(){
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
try{
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream in = getClass().getResourceAsStream(fileName);
if (in == null){
return super.loadClass(name);
}
byte[] b = new byte[in.available()];
in.read(b);
return defineClass(name,b,0,b.length);
}catch (IOException e){
throw new ClassNotFoundException();
}
}
};
// 由自定义加载器加载的
Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
// jvm.ClassLoaderTest位于classpath上,所以App ClassLoader会加载这个类
// 所以会存在两个由不同加载器加载进来的jvm.ClassLoaderTest,进行判别返回false
System.out.println(obj instanceof jvm.ClassLoaderTest);
}
}
/*
* output
*
* class jvm.ClassLoaderTest
* false
*
* */
这个自定义的类加载器覆写了loadClass方法,方法内没有遵循类加载委派的模型规范,而是首先自己加载,找不到则由父类加载器进行加载。
一种特殊的类加载器Thread Context ClassLoader(线程上下文加载器)
使用Thread Context ClassLoader能破坏双亲委派的类加载机制,被广泛用于JNDI,用来对资源进行集中管理和查找,解决双亲委派自身模型的缺陷(用户代码可以调用基础API,但是基础API无法调用用户代码)
先获取线程上下文加载器,然后指定自定义的类加载器,然后就会使用用户自定义类加载器加载指定类,此时不会委派给父类加载器。