加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(unloading)
下面主要详细的讲类加载的过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
加载
“加载”是"类加载"过程的一个阶段,在加载阶段虚拟机需要完成三件事情:
- 通过一个类的全限定名来定义这个类二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行数据结构
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各个数据访问入口
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量(被static修饰的变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。)初始值的阶段,这些变量所使用的内存 都将在方法区中进行分配。
需要说明的一点事,这里所说的初始值“通常情况”下是数据类型的零值,
例如:
public static int value=123;
在准备阶段过后的初始化value是0 而不是123,因为这个时候尚未执行Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放在类构造器方法中的,所以把value赋值123的动作将在初始化阶段才会执行。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,类或接口的解析、字段解析、类方法解析、接口方法解析
初始化
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问;
static {
i = 0;//编译时可以赋值
System.out.println(i);/编译时提示Illegl forward reference
}
static int i=1;
<clinit>()方法与类的构造函数或者说实例构造器<init>()方法不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.object。
由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块优先于子类的变量赋值操作
类加载器
在类加载阶段中提到——“通过一个类的全限定名来定义这个类二进制字节流”,这个动作是放在Java虚拟机外部实现的,以便程序自己决定如何去获取所需要的类,实现这一动作的代码模块就是"类加载器"。
类加载器的作用
类加载器虽然说只是用于类的加载,但还起到另一层作用——通过类加载器和这个类本身一同确定其在Java虚拟机的中唯一性,每个类加载器也有一个独立的类空间。换一句话说就,比较两个类是否相等,不只是这两个类是否是是来自同一个class文件,还要由同一个类加载器来完成加载过程,否则这两个类不相等。
下面通过一段代码来演示这一过程:
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 is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.captain.wds.classLoad.ClassLoadTest").newInstance();
System.out.println(obj.getClass());
ClassLoader classLoader = ClassLoadTest.class.getClassLoader();
System.out.println("classLoader :" + classLoader.toString());
System.out.println("myLoader :" + myLoader.toString());
System.out.println(obj instanceof com.captain.wds.classLoad.ClassLoadTest);
}
打印结果:
class com.captain.wds.classLoad.ClassLoadTest
classLoader :sun.misc.Launcher$AppClassLoader@74a14482
myLoader :com.captain.wds.classLoad.ClassLoadTest$1@330bedb4
false
可以发现,虽然是同一个类,但通过instanceof关键字的结果是false,一个是通过系统应用程序的类加载器加载而成,另一个是我们自定义的类加载器,虽然都是来之同一个class文件,得到的对象却不同等。
双亲委派模型
从Java虚拟机的角度上讲,只存在两种不同的类加载器:一种启动类加载器,是虚拟机的一部分,由C/C++来实现的:另一类就是其他所有类加载器,这类加载器独立于虚拟机,由Java代码实现,并且全部继承至Java.lang.ClassLoader。
类加载器之间的这种层次关系,称为类加载器的双亲委派模型(ParentsDelegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载,而是把这个请求委派给父类加载器完成,依次类推,每个类加载请求都会传递到顶层的启动类加载中,只有当父类加载器无法完成这个类加载请求时,子类加载器才会尝试自己去加载。
在java.lang.ClassLoader这个类中,实现双亲委派的主要代码也相当简单,先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己findClass()方法进行加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//首先先检查这个类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//如果父类加载器抛出ClassNotFoundException
//说明父类加载器无法完成这个加载请求
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//在父类加载器无法完成的情况下,调用自身的findClass来进行加载请求
c = findClass(name);
}
}
return c;
}
好了关于类加载机制的内容就学习到这了😄😄😄
风后面是风,天空上面是天空,而你的生活可以与众不同