类装载器子系统,作为JVM最底层的一部分。
用途:用来加载Class文件到JVM的方法区,在方法区中创建一个java.lang.Class
对象(后面简称类对象)通过此实例的newInstance()
方法就可以创建出该类的一个对象。
比较两个类是否相等,只有当这两个类由同一个加载器加载才有意义;否则即使同一个 class 文件被不同的类加载器加载,那这两个类必定不同,即通过类的 Class 对象的 equals 执行的结果必为false。
JVM提供三种类加载器:
Java_Home\lib
中,或通过-Xbootclasspath
参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的class文件。Java的核心类都由该ClassLoader加载。在Sun JDK中,这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。Java_Home\lib\ext
目录下,或通过java.ext.dirs
系统变量指定路径中的class文件;ClassLoader.getSystemClassLoader()
来获取。可通过继承java.lang.ClassLoader
类的方式实现自定义类装载器。ClassLoader源码简略版:
public abstract class ClassLoader {
// 返回该类加载器的父类加载器
public final ClassLoader getParent();
// 加载名称为name的类,返回java.lang.Class类的实例
public Class<?> loadClass(String name);
// 查找名称为name的类,返回java.lang.Class类的实例
protected Class<?> findClass(String name);
// 查找名称为name的已经被加载过的类,返回java.lang.Class类的实例
protected final Class<?> findLoadedClass(String name);
// 把字节数组b中的内容转换成Java类,返回java.lang.Class类的实例
protected final Class<?> defineClass(String name, byte[] b, int off, int len);
// 链接指定的Java类
protected final void resolveClass(Class<?> c);
}
Parent Delegation Model。
工作过程:如果一个类加载器收到加载类的请求,它首先将请求交由父类加载器加载;若父类加载器加载失败,当前类加载器才会自己加载类。
作用:像java.lang.Object
这些存放在rt.jar
中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的Object
类都是同一个。
原理:双亲委派模型的代码在 java.lang.ClassLoader.loadClass()
方法中实现:
java.lang.ClassLoader.loadClass
源码:
// 提供class类的二进制名称,加载对应class,加载成功则返回表示该类对应的Class instance 实例
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;
}
}
故加载过程:
jvm.dll
,初始化 JVMJAVA_HOME/jre/lib
下的Java核心API类,都在 rt.jar 里。JAVA_HOME/jre/lib/ext
下的扩展类双亲委派机制的优点:
以java.开头的是核心API包,需要访问权限,强制加载会抛出异常,任何以java.开头的包都会报错:Exception in thread "main" java.lang.SecurityException: Prohibited package name
涉及到“类相等”的方法有:Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法以及instanceof对象所属关系判定。
如果想要自己加载类,不使用双亲委派机制?可以用线程上下文加载器来加载这些类。
类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。
其中,类加载过程:加载、链接(验证、准备、解析)、初始化。
C/C++ 在运行前需要完成预处理、编译、汇编、链接;在Java中,类加载 (加载、连接、初始化) 是在程序运行期间完成的。
在程序运行期间进行动态类加载会稍微增加程序的开销,好处:提高程序的灵活性,可以节省内存空间、灵活地从网络上加载类,可以通过命名空间的分隔来实现类的隔离,增强整个系统的安全性。
灵活性体现在它可以在运行期动态扩展,即在运行期间动态加载和动态连接。
其中加载、验证、准备、初始化的开始顺序是依次进行的,这些步骤开始之后的过程可能会有重叠。而解析过程会发生在初始化过程中。这是为了支持Java语言的运行时绑定(又动态绑定或晚期绑定)
流程:
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。JVM提供3种类加载器。
加载的二进制字节流的来源:
加载过程的注意点:
加载、连接、初始化
,但结束顺序无所谓,因此由于每个步骤处理时间的长短不一,就会导致有些步骤会出现交叉。类和数组加载过程的区别?
数组也有类型,称为 “数组类型”,String[] str = new String[10];
此数组类型是Ljava.lang.String
,String只是这个数组中元素的类型。
当程序在运行过程中遇到 new 关键字创建一个数组时,由 JVM 直接创建数组类,再由类加载器创建数组中的元素类。
而普通类的加载由类加载器完成。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器。
为了确保Class文件符合当前虚拟机要求,需要对其字节流数据进行验证,主要包括格式验证、元数据验证、字节码验证和符号引用验证。
为什么需要验证?
编译器和虚拟机是相互独立的,虚拟机只认二进制字节流,不管获得的二进制字节流的来源。为防止字节流中有安全问题,因此需要验证。
验证阶段比较耗时,它非常重要但不一定必要,若所运行的代码已经被反复使用和验证过,可使用-Xverify:none
参数关闭,以缩短类加载时间。
在准备阶段,为类变量(static修饰)在方法区中分配内存并设置初始值。如private static int var = 100;
准备阶段完成后,var 值为0,而不是100。在初始化阶段,才会把100赋值给val。
final,特殊情况:private static final int VAL= 100;
在编译阶段会为VAL生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VAL赋值为100。
解析阶段是可选的,将常量池中的符号引用替换为直接引用的过程,两者的不同之处:
初始化类中的静态变量,并执行类中的static代码、构造函数。类初始化的过程是不可逆的,如果中间一步出错,则无法执行下一步。JVM规范严格定义何时需要对类进行初始化:
初始化阶段就是执行类构造器clinit()
的过程。clinit()
方法由编译器自动产生,收集类中static{}
代码块中的类变量赋值语句和类中静态成员变量的赋值语句。
在准备阶段,类中静态成员变量已经完成默认初始化,而在初始化阶段,clinit()
方法对静态成员变量进行显示初始化。
该过程的注意点:
clinit()
方法中静态成员变量的赋值顺序是根据 Java 代码中成员变量的出现的顺序决定的;init()
需显示调用父类构造函数,而类的构造函数clinit()
不需要调用父类的类构造函数,因为虚拟机会确保子类的clinit()
方法执行前已经执行父类的clinit()
方法;clinit()
方法;clinit()
方法为接口中定义的静态成员变量显示初始化;clinit()
方法前,虚拟机不会确保其父接口的clinit()
方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()
方法;clinit()
方法加锁,因此当多条线程同时执行某一个类的clinit()
方法时,只有一个方法会被执行,其它的方法都被阻塞。只要有一个clinit()
方法执行完,其它的clinit()
方法就不会再被执行。因此在同一个类加载器下,同一个类只会被初始化一次。JVM 规范中只定义类加载过程中初始化过程开始的时机,加载、连接过程都应该在初始化之前开始 (解析除外),这些过程具体在何时开始,JVM 规范并没有定义,不同的虚拟机可以根据具体的需求自定义。
在运行过程中遇到如下字节码指令时,如果类尚未初始化,那就要进行初始化:new、getstatic、putstatic、invokestatic。这四个指令对应的 Java 代码场景是:
java.lang.reflect
进行反射调用时,如果类没有初始化,那就需要初始化。当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类;当虚拟机启动时,虚拟机会首先初始化带有main方法的类,即主类。
JVM 规范中要求在程序运行过程中,当且仅当出现上述4个条件之一的情况才会初始化一个类。如果间接满足上述初始化条件是不会初始化类的。直接满足上述初始化条件的情况叫做主动使用;间接满足上述初始化过程的情况叫做被动使用。
类加载过程的最后一个阶段,到初始化阶段,才真正开始执行类中的Java程序代码。
只有这四种情况才会触发类的初始化,称为对一个类进行主动引用,除此之外所有引用类的方式都不会触发其初始化,称为被动引用。
VM规范严格规定有且只有四种情况必须立即对类进行初始化:
类加载的用途:类层次划分、OSGI、热部署、代码加密
把一个方法的调用与方法所在的类 (方法主体)关联起来,对Java来说,绑定分为静态绑定和动态绑定:
ExtClassLoader与AppClassLoader,都在sun.misc.Launcher
类内部定义,是其静态内部类。JVM负责调用已经加载在方法区的类sun.misc.Launcher
的静态方法getLauncher()
,获取sun.misc.Launcher
实例,且sun.misc.Launcher
使用单例设计模式,保证一个JVM内只有一个Launcher实例。
线程上下文类加载器是从线程的角度来看待类的加载,为每一个线程绑定一个类加载器,可以将类的加载从单纯的双亲加载模型解放出来,进而实现特定的加载需求。
Class.forName
和 ClassLoader.loadClass
区别一个 Java类加载到 JVM 中会经过三个步骤,装载(查找和导入类或接口的二进制数据)、链接(校验:检查导入类或接口的二进制数据的正确性;准备:给类的静态变量分配并初始化存储空间;解析:将符号引用转成直接引用;)、初始化(激活类的静态变量的初始化 Java 代码和静态 Java 代码块)。
Class.forName(className)
方法,内部实际调用方法Class.forName(className, true, classloader);
// name:要加载 Class 的名字,initialize:是否要初始化,loader:指定的 classLoader
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
ClassLoader.loadClass(className)
方法,内部实际调用方法ClassLoader.loadClass(className, false);
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
/**
两个参数的含义分别为:
name:class 的名字
resolve:是否要进行链接
*/
通过传入的参数可以知道 Class.forName 方法执行之后已经对被加载类的静态变量分配完存储空间,而 ClassLoader.loadClass 方法并没有一定执行完链接这一步;当想动态加载一个类,且这个类又存在静态代码块或者静态变量,而你在加载时就想同时初始化这些静态代码块,则应偏向于使用 Class.forName 方法。
深入探讨Java类加载器
类加载机制
JVM类加载器机制与类加载过程