类加载机制
虚拟机把描述类的数据从class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类的初始化
一般Java程序的class
文件经过加载、连接后,就进入初始化阶段,顺序执行static
语句,为静态变量赋予正确的值,执行static代码块,初始化类。
1. 主动初始化的7种方式
(1)创建对象实例
(2)访问某个类或接口的静态变量,或者对该静态变量赋值
(3)调用类的静态方法
(4)通过class文件反射创建对象
(5)初始化一个类的子类:使用子类的时候先初始化父类
(6)java虚拟机启动时被标记为启动类的类:比如main()方法所在的类
(7)jdk1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle
实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic
句柄对应的类没有初始化,则初始化。
注意:java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则是在需要的时候才加载。一个JVM实例里,一个类只能被加载一次。按需加载,节省内存的开销。
2.不会进行初始化的情况
(1)在同一个类加载器下只能初始化类一次,如果已经初始化就不必初始化了
(2)在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化,比如final修饰的静态变量
下面简单总结类加载过程:
一、加载阶段
类的加载是指查找并加载类的二进制数据,即加载class文件,另外在java堆中也创建一个java.lang.Class类的对象 —— < 加载过程的产物: class对象
>
1.类加载器
JDK提供三种类加载器:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用类加载器(App ClassLoader)
每一种类加载器都有其指定的类加载路径:
(1)启动类加载器,负责加载的虚拟机的核心类库,比如java.lang.*
等。其主要加载路径为JAVA_HOME/jre/lib里的jar包
,该目录下的所有jar包都是运行JVM时所必需的jar包。根类加载器本身的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并没有实现java.lang.classLoader类
。
注意: Bootstrap ClassLoader的类加载器是由C++语言
进行开发的,如果一个类的类加载器是BootstrapClassLoader,那么该类的getClassLoader()方法返回值为null
。
(2)扩展类加载器,负责加载java核心扩展类,即JAVA_HOME/jre/lib/ext目录下的类库
,从系统属性java.ext.dirs
所指定的目录中加载类库。扩展类加载器是纯Java类,它继承于java.lang.classLoader类
。
(3)应用类加载器,从环境变量classPath
或者从系统属性java.class.path
所指定的目录下加载类,它是用户自定义类加载器的默认父加载器。系统类加载器是纯Java类,它继承于java.lang.classLoader类
。
2.双亲委派模型
类加载器之间采用双亲委派模型
要求:
除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
- 工作过程如下:
当一个类接受到类的加载请求时,先不对其进行加载
(1)首先从当前类加载器中查询此类是否已经被加载,若已加载则返回此类的class对象
;
(2)若没有找到,就委托当前类加载器的父类加载器去尝试加载。
父类加载器采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就继续委托它的父类加载器尝试加载,直至委托到启动类加载器为止。(如果父类加载器为空,就代表使用启动类加载器作为父加载器去加载该类)
(3)如果启动类加载器加载失败,就会一级级传递下来,使用扩展类加载器来尝试加载,继续失败则会使用应用类加载器加载,继续失败就会抛出ClassNotFoundException
异常
双亲委派模型优点:
(1)安全性,避免用户自己编写的类动态替换 Java 的一些核心类。
如果不采用双亲委派模型的加载方式进行类的加载工作,那我们就可以随时使用自定义的类来动态替代 Java 核心 API 中定义的类。
(2)避免类的重复加载
JVM 判定两个类是否是同一个类,不仅仅根据类名是否相同进行判定,还需要判断加载该类的类加载器是否是同一个类加载器,相同的 class 文件被不同的类加载器加载得到的结果就是两个不同的类。
3.自定义类加载器
- 首先简单构建一个类,用于测试.class文件
public TestDemo{
public static void main(String[] args){
System.out.println("it's a testdemo.");
}
}
我们把它编译成的.class
文件放在d:\t
的根目录下
- 构建自定义的类加载器
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
//用户自定义文件路径
String classPath="D:\\t\\"+name+".class";
File file=new File(classPath);
byte[] buff=null;
InputStream in;
try {
in = new FileInputStream(file);
buff=new byte[in.available()];
//将class文件读到byte数组中
in.read(buff);
in.close();
//调用defineClass方法将byte数组加载成class对象
Class<?> aClass=defineClass(name, buff,0,buff.length);
return aClass;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
}
该类继承自ClassLoader
,重写了findClass
方法,根据指定类的全限定名查找类,并将class文件读到byte数组中,然后通过调用defineClass
方法加载这个类,并返回一个Class对象
。
- 下面用一个简单的程序测试一下自定义的类加载器
public class TestClassLoader {
public static void main(String[] args) throws Exception {
MyClassLoader mloader = new MyClassLoader();
Class<?> aClass = mloader.findClass("TestDemo");
//输出加载该类的加载器
System.out.println(aClass .getClassLoader());
}
}
运行后显示:
jtest.classlorder.MyClassLoader@17327b6
可以看到的确是使用我们自定义的类加载器MyClassLoader
来加载TestDemo
类的。
二、连接阶段
将已经加载到内存中的类的二进制数据合并到虚拟机的运行时环境中。
该阶段包含以下三块内容:
(1)验证
确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段大致会完成以下检验动作:
1.文件格式验证:验证字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理
2.数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
3.字节码验证:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
4.引用符号验证:对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验
字节码文件的前4个字节我们将其称为
魔数(0xCAFEBABE)
魔数的作用:标记字节码文件的类型
MD5
就是专门将用来标记字节码文件类型的魔数进行加密操作
的。
(2)准备
为类的静态变量分配内存并设置类变量初始值阶段,并将其初始化为默认值
(3)解析
虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。 各种虚拟机实现的内存布局可以各不相同,但他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的class文件格式中。
直接引用:
可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已在内存中存在。
三、初始化阶段
为类的静态变量赋予正确的初始值