一、概述
描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
Java类加载器是Java运行时环境(JRE)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。每个Java类必须由某个类加载器装入到内存,比如平常的.class文件就是通过这个加载器加载到内存中的。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。
二、JVM的3个默认的类加载器
①引导(Bootstrap)类加载器。存储在JAVA_HOME/jre/lib中。由原生代码(如C语言)编写,不继承自java.lang.ClassLoader。负责加载核心Java库,加载JAVA_HOME/jre/lib/rt.jar里所有的class,不是ClassLoader子类。
②扩展(Extensions)类加载器。用来在JAVA_HOME/jre/lib/ext或java.ext.dirs中指明的目录中加载 Java的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。该类由sun.misc.Launcher$ExtClassLoader实现。
③Apps类加载器(也称系统类加载器)。根据 Java应用程序的类路径(java.class.path或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
每个类装载器有一个父装载器(parent class loader)。
三、五个过程
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。
(1)加载
把class字节码文件从各个来源通过类加载器装载入内存中。
字节码来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
类加载器:一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器(有自定义类加载器一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载)。
(2)校验
主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。
①文件格式验证:基于字节流验证。
对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
②元数据验证:基于方法区的存储结构验证。
对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
③字节码验证:基于方法区的存储结构验证。
对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
④符号引用验证:基于方法区的存储结构验证。
对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
校验主要是为了保证加载进来的字节流符合虚拟机规范,不造成安全问题。
(3)准备
为类变量分配内存,并将其初始化为默认值。
此时为默认值,在初始化的时候才会给变量赋值,即在方法区中分配这些变量所使用的内存空间。
public static int value = 1;
此时在准备阶段过后的初始值为0而不是1。将value赋值为1的指令是程序被编译后,存放于类构造器方法之中。
public static final int value = 1;
此时value的值在准备阶段过后就是1。
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
比如8种基本类型的初值默认为0,引用类型初值为null,常量的初值即为代码中设置的值,例如final static n = 2, 那么该阶段n的初值就是2。
(4)解析
把类型中的符号引用转换为直接引用。
主要有:类或接口的解析、字段解析、类方法解析、接口方法解析。
符号引用:即一个字符串,但是这个字符串给出了一些能够唯一识别一个方法,一个变量,一个类的相关信息。
直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。
例如,调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
(5)初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
只对static修饰的变量或语句进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
对于初始化阶段,有且只有以下五种情况才会对要求类立刻初始化:
①使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
②初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
③使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
④虚拟机启动时,用户会先初始化要执行的主类(含有main)
⑤jdk1.7后如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
四、双亲委派模型
可以自己写String类吗?
答:不可以,因为 根据类加载的双亲委派机制,会去加载父类,父类发现冲突了String就不再加载了。
import java.lang.String;
public class StringTest {
private char[] value;
public void String() {
this.value = new char[0];
System.out.println("自写String测试");
}
public static void main(String[] args) {
String test = new String();
test = "系统String测试";
System.out.println(test);
}
}
结果输出了系统String,这就是因为有类加载器的委托机制。
在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
类加载器有加载顺序,加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个加载器已加载就视为已加载此类,保证此类所有ClassLoader加载一次。而加载的顺序是自顶向下,因此自己写的String是被Bootstrap ClassLoader加载了,所以Apps ClassLoader就不会再去加载我们写的String类了,导致我们写的String类是没有被加载的。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载。此外,这种机制能更好地保证java平台的安全。