类加载的时机
- 创建类的实例:new、反射、反序列化、clone;
- 访问类中的某个静态变量,或者对静态变量进行赋值;
- 主动调用类的静态方法;
- ClassForName(“包类名”),得到的class已经初始化完成,ClassLoader.loadClass(“包类名”)得到的class是还没有链接(验证,准备,解析)的;
- 完成子类的初始化,也会完成对本类的初始化(接口除外);
- 该类是程序引导入口(main入口或者test入口);
类加载过程
class文件中保存着虚拟机将要执行的指令,当需要某个类的时候,java虚拟机会通过类的全限定名从磁盘中加载class 文件,并创建对应的java.lang.Class对象。将class文件加载到虚拟机的内存,这个过程被称为类的加载。
- 加载:ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象,存储在方法区;
- 验证:目的在于确保class文件的字节流信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证、元数据的验证、字节码验证、符号引用验证;
- 准备:为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,在初始化的阶段真正赋值;
- 解析:这里主要的任务是把常量池中的符号引用替换成直接引用,下面就是Java字节码文件中的符号引用;
#1 = Methodref #7.#19 // java/lang/Object."<init>":()V
#2 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Fieldref #6.#22 // intron/classtest/ClassTest.TEST:Ljava/lang/String;
#4 = Methodref #23.#24 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #25 // test
#6 = Class #26 // intron/classtest/ClassTest
#7 = Class #27 // java/lang/Object
- 初始化:为类的静态变量赋值,然后执行累的初始化语句(static代码块);
初始化的详细过程:
- 如果类还没有被加载和链接,那就先进行加载和链接;
- 如果类还存在父类,并且父类还没有初始化,那就先初始化直接父类;
- 如果类中存在初始化语句,顺序执行初始化语句;
类加载器
类加载器是类加载流程的实现者,JDK自带了三个类加载器:Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(扩展类加载器)、Application ClassLoader(应用类加载器)。
BootStrap ClassLoader:
- JVM自带的引导类加载器,由C/C++的语言实现,是C++的对象,在Java中打印该加载器对象为null;
- 加载Java的核心类库,$JAVA_HOME中jre/lib/rt.jar、resource.jar或Java程序运行指定的Xbootclasspath选项jar包;
- 指定加载java、javax、sun等开头的包名类,不能自定义这些包名;
Extension ClassLoader:
- Java语言编写的类加载器:sun.misc.Launcher$ExtClassLoader;
- 指定BootStrap ClassLoader为Parent加载器–> getParent()可以获取Bootstrap ClassLoader;
- 负责加载Java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext或-Djava.ext.dirs指定目录下的jar包(如果我们自定义的class需要交给Ext来加载可以放置在ext的目录下);
Application ClassLoader:
- Java语言编写的类加载器:sun.misc.Launcher$AppClassLoader;
- 该加载器是Java程序默认的类加载器,Java应用的类都是该加载器加载的;
- 指定Extension ClassLoader为parent加载器–> getParent可以获取Extension ClassLoader;
- 负责加载环境变量classpath指定的目录,或者java.class.path指定的目录类库;
双亲委派机制:
如果一个类加载器收到了加载类的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的引导类加载器。如果父加载器能够完成类的加载任务,就会成功返回,倘若父类加载器无法完成任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派机制核心代码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
采用双亲委派模式的好处就是Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类,子类加载器就不需要再加载一次。其次是考虑到安全因素,Java核心API中定义类型不会被随意替换,如果我们人为定义java.lang包并在包内添加一些自定义类,这样做是不允许的,因为java.lang是核心的API包,需要访问权限,强制加载将会报出如下异常。
java.lang.SecurityException:Prohibited package name: java.lang