类加载详解
通过上篇文章我们知道了class文件是怎样存储数据的,那么今天我们就学习他们是怎么被加载进我们的虚拟机的呢?还有每一个加载过程他们都在做什么事情呢?内容很多,干货满满,一定要耐心看完,为了方便下次阅读也可以收藏。
一、类加载的时机
1.遇到new、getstatic、 putstatic 和invokestatic这四条指令(字节码指令)时,如果对应的类没有初始化,则要对对应的类先进行初始化。
public class Student{
private static int age ;
public static void method(){
}
}
//Student.age
//Student.method();
//new Student();
2.使用java.lang.reflect包方法时对类进行反射调用的时候。
Class c = Class.forname("com.wjx.Student");
3.初始化一个类的时候发现其父类还没初始化,要先初始化其父类
4.当虚拟机开始启动时,用户需要指定一个主类(main) ,虚拟机会先执行这个主类的初始化。
二、类加载的过程
【加载】–class文件–>Class对象
Class文件对应的Class对象1.7之前放在方法区,1.7之后放在堆中
加载的过程
在加载的过程中,JVM主要做3件事情:
●全限定名–>二进制字节流(class文件)
●字节流的静态存储结构–>方法区的运行时数据结构
●创建java.lang.Class对象
加载源
●本地class文件
●zip包
Jar、War、Ear等
●其它文件生成
JSP文件中生成对应的Class类
●数据库中
将二进制字节流存储至数据库中,然后在加载时从数据库中读取。有些
中间件会这么做,用来实现代码在集群间分发
●网络
从网络中获取二进制字节流。典型就是Applet.
●运行时计算生成
动态代理技术,用ProxyGenerator.generateProxyClass 为特定接口生成形式为"$Proxy’的代理类的二进制字节流.
类和数组加载的区别
数组也有类型,称为“数组类型”。如:
String[] str = new String[10];
这个数组的数组类型是[Ljava. lang. String,而String只是这个数组的元素类型。
数组类和非数组类的类加载是不同的,具体情况如下:
●非数组类:是由类加载器来字成。
●数组类:数组类本身不通过类加载器创建,它是由java虚拟机直接创建,
但数组类与类-加载器有很密切的关系,因为数组类的元素类型最终要靠类加载器创建。
加载过程的注意点
加载阶段和链接阶段是交叉的
类加载的过程中每个步骤的开始顺序都有严格限制、
但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始:
加载-> 链接(验证、准备、解析)-> 初始化
但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。.
[验证]-各种检查
验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被反复使用和验证过,那么可以使用
-xverify: none
参数关闭,以缩短类加载时间。
验证的目的
保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。
验证的过程
●文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理.
本验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存到方法区
后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流。
验证[加载和验证]是交叉进行的:
1.加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区
2.而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区
也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,
二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作
●元数据验证
对字节码描述信息进行语义分析,确保符合Java语法规范
●字节码验证
本阶段是验证过程的最复杂的一个阶段。
本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。
字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过字
节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全。
●符号引用验证
发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解
析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行。
[准备]–为静态成员变量分配内存并初始化0值
1.7之前方法区
1.7之后堆
准备阶段主要完成两件事情:
●为在内存中的类的静态成员变量分配内存
●为静态成员变量设置初始值,初始值为0、false、 null等
仅仅为类变量(即static修饰的字段变量)分配内存,并且设置该类变量的初始值,即零值
这⾥不包含⽤final修饰的static,因为final在编译的时候就会分配了(编译器的优化),同时这⾥也不会为实例变量分配初始化。
类变量(静态变量)会分配在[⽅法区]中,⽽[实例变量]是会随着对象⼀起分配到Java堆中。
比如:
public static int x = 1000;
注意:
实际上变量x在准备阶段过后的初始值为0,而不是1000
将x赋值为1000的putstatic指令是程序被编译后,存放于类构造器方法之中
但是如果声明为:
public static final int x = 1000;
在编译阶段会为x生成Constantvalue属性,在准备阶段虚拟机会根据Constantvalue属性将x赋值为1000。
[解析–将符号引用替换为直接引用]
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的
CONSTANT_ Class_ info、 CONSTANT_ Fieldref_info、
info、CONSTANT_ InterfaceMethodref_ info 四种常量类型。
1.类或接口的解析:
判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2.字段解析:
1.会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段
2.如果有,则查找结束
3.如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口
4. 还没有,则按照继承关系丛上往下递归搜索其父类,直至查找结束。(优先从接口来,然后是继承的父类。理论上是按照上述顺
序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。 如果有一个同名字段同时出
现在该类的接口和父类中,或同时在自已或父类的接口中出现,编译器可能会拒绝编译)
3.类方法解析:
对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,
而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
4. 接口方法解析:
与类方法解析步骤类似,只是搜索的是接口不会有父类,因此,只递归向上搜索父接口就行了。
[初始化—调用方法
针对于类级别
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码(初始化成为代码设定的默认值)。
在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和
其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器方法的过程。
其实初始化过程就是调用类初始化方法的过程,完成对static修饰的类变量的手动赋值还有主动调用静态代码块。
初始化过程的注意点:
●方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的
●静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问程。
public class Test {
static {
i=0;
System.out.println(i);//编译失败:"⾮法向前引⽤"
}
static int i = 1;
}
●实例构造器需要显式调用父类构造函数,而类的不需要调用父类的类构
造函数,虚拟机会确保子类的方法执行前已经执行完毕父类的方法.因此
在JVM中第-一个被执行的方法的类肯定是java.lang.Object.
●如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操
作,那么编译器就不会为此类生成方法.
●接口也需要通过方法为接口中定义的静态成员变量显示初始化。
接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接
口与类- -样都会生成方法.不同的是,执行接口的方法不需要先执行父接
口的方法只有当父接口中的静态成员变量被使用到时才会执行父接口的方法
●虚拟机会保证在多线程环境中一个类的方法被正确地加锁,同步当多条
线程同时去初始化一个类时,只会有一个线程去执行该类的方法,其它
线程都被阻塞等待,直到活动线程执行方法完毕.其他线程虽会被阻塞,
只要有一个方法执行完,其它线程唤醒后不会再进入方法同一个类加载
器下,一个类型只会初始化一次.
使用静态内部类的单例实现:
public class Student {
private Student() {}
/*
* 此处使⽤⼀个内部类来维护单例 JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
*/
private static class SingletonFactory {
private static Student student = new Student();
}
/* 获取实例 */
public static Student getSingletonInstance() {
return SingletonFactory.student;
}
}
三、类加载器介绍
自定义类加载器:圈定一个范围,进行类的加载,比如在D盘中的jar包
都必须继承ClassLoader
我们的代码一般都是应用类加载器加载的
自定义类加载器
为什么要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class
如果我们想加载其它位置的类或jar时,就只能自定义一个ClassIoader类了。
比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的
ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader
如何定义类加载器?
继承ClassLoader类,重写findClass()方法,loadClass()方法
自定义类加载器注意事项:
自定义类加载器需要去继承ClassLoader类。
JDK1.2之前是重写ClassLoader类中的loadClass方法
JDK1.2以后是重写ClassLoader类中的findClass方法
自定义类加载器的实现,不要去覆盖ClassIoader类的loadClass方法,去实现findClass方法,为什么呢?
因为有双亲委派机制