类加载机制
一、概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
整个类的生命周期从类被加载到虚拟机内存开始,到卸载出内存为止要经历:加载(Loading)、验证(Verification)、准备(Preparation)、解析
(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。
二、类加载
2.1 加载阶段
类加载过程干了什么?
主要有以下3件事:
- 通过一个类的全限定名来获取定义此类的二进制流
- 将这个字节流所代表的静态存储结构转换成方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.long.Class对象,作为方法区这个类的各种数据的访问入口
2.2 连接阶段
验证
验证的目地就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1)文件格式验证
这过程是基于二进制字节流进行的,只有通过该过程的验证后,字节流才会进入内存的方法区进行存储,所以后面的3个验证全部基于方法区的存储结构进行的,不会再直接操作字节流。
主要包括对下面这些内容的验证:
2)元数据验证
该过程的目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。(判断是否符合java语言的语法)
可能包括下面的这些内容的验证:
- 这个类是否有父类(除了java.long.Object之外,所有得类都应该有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
3)字节码验证
是整个过程最复杂的,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在对元数据信息中的数据类型做完校验后,该过程将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事。
可能包括下面这些内容的验证:
- 保证
4)符号引用验证
该过程发生在虚拟机将符号引用转换成直接引用的时候,这个转化动作将在连接的第三阶段–解析阶段中发生,可以看做是对类自身以外(常量池中各种符号引用)的信息进行匹配性校验。
可能包括下面这些内容的校验:
- 符号引用中通过字符串描述的 全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性(public、protected、public、default)是否可被当前类访问
准备
该阶段是正式为类变量(被static修改的成员变量)分配内存并设置类初始化值(各个数据类型的零值)的阶段,这些变量所使用的内存全部在方法区进行分配。如果类变量是常量(被static final修饰的成员变量)public static final int age = 12;
则在该阶段直接初始为指定的值。
解析
是虚拟机将常量池内的符号引用替换成直接引用的过程。
符号引用:
1)类或接口的解析
2)字段解析
3)类方法解析
4)接口方法解析
2.3 初始化阶段 (类变量的初始化)
类初始化是类加载的最后一步,整个类加载过程除了加载阶段用户可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化过程是执行类构造器方法的过程
- 方法是由编译器自动收集类的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由源文件中出现的顺序决定的。
public class Initialization {
/**
* FLAG在类加载过程的准备阶段已经初始化为flag了,由于是类的常量
*/
public static final String FLAG = "flag";
public static Integer count = 10;
private int temp = 6;
public Initialization() {
System.out.println("显示创建的默认构造器执行了");
}
public Initialization(int number) {
count++;
System.out.println("实例化了一个Initialization对象" + "count = " + count);
System.out.println("temp = " + temp);
temp = number;
System.out.println("通过构造器赋值后" + "temp = " + temp);
}
//类的初始化不会执行该语句块内的内容
{
count++;
System.out.println("源文件中第一个语句块" + "count = " + count);
}
static {
count++;
System.out.println("源文件中第一个静态语句块!!!"+ "count =" + count);
}
//类的初数化不会执行该语句块内的内容
{
count ++;
System.out.println("源文件中第二个语句块" + "count = " + count);
}
static {
count++;
System.out.println("源文件中第二个静态语句块!!!" + "count = " + count);
}
}
public class TestInitialization {
public static void main(String[] args) {
System.out.println("------触发com.keminapera.jvm.classload.Initlize加载------");
System.out.println(Initialization.FLAG);
System.out.println("------加载com.keminapera.jvm.classload.Initlize完毕------");
}
}
有结果可以看到JVM将Initialization加载到了虚拟机,并且输出了常量FLAG的值,但是没有执行静态块,说明没有执行<clinit>方法,也就说明没有走到初始化这个阶段,原因很简单,因为类的常量是在类加载过程中的准备阶段附上常量的值了。
public class TestInitialization {
public static void main(String[] args) {
System.out.println("------触发com.keminapera.jvm.classload.Initlize加载------");
System.out.println(Initialization.FLAG);
System.out.println("------加载com.keminapera.jvm.classload.Initlize完毕------");
//让其输出类的变量
System.out.println(Initialization.count);
}
}
这会可以看到静态语句块里面的内容执行了,并且是按照源文件中定义静态语句块的顺序执行的
public class TestInitialization {
public static void main(String[] args) {
System.out.println("------触发com.keminapera.jvm.classload.Initlize加载------");
System.out.println(Initialization.FLAG);
System.out.println("------加载com.keminapera.jvm.classload.Initlize完毕------");
//让其输出类的变量
System.out.println(Initialization.count);
System.out.println("-----------------无参构造----------------");
//通过无参构造器创建对象
Initialization initialization1 = new Initialization();
System.out.println("----------------有参构造-----------------");
//通过有参构造器创建对象
Initialization initialization2 = new Initialization(15);
}
}
从结果可以看出每次创建一个该类的实例化对象都会在执行构造方法之前执行构造语句块,由此得出结论:类的静态构造块的执行次数和该类的class文件被加载进JVM的次数相等(不同类加载器加载同一个class文件情况),而构造语句块是每次创建该类对象都会执行一次。
类的变量是在类加载的初始化阶段完成的,所有在没有该类的对象也能直接使用,而类的常量是在类加载的准备阶段完成的,第一个例子已经证明,在没有类加载的初始化阶段,程序就已经可以访问类的常量了。
- 方法与类的构造方法不同,它不需要显示调用父类构造器,虚拟机会在保证方法之前,父类的方法已经执行完毕。
public class Parent {
public static String name;
public static int age;
static{
name = "zhangsansan";
age = 18;
System.out.println("父类构造块执行了!!!");
}
}
public class Son extends Parent {
public static double money = 1200;
static {
System.out.println("子类静态构造块执行了!!!");
}
}
public class TestItem2 {
public static void main(String[] args) {
System.out.println(Son.money);
}
}
- 由于父类的方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
- 方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有变量的赋值操作,那么编译器可以不为这个类生成方法。
- 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样会生成方法。但接口与类不同的是,执行接口的方法不需要先执行父接口的方法,只有当父接口中定义的变量被使用时,父接口才会初始化,另外,接口的实现类在初始化时也一样不执行接口的方法。
- 虚拟机保证一个类的方法在多线程环境中被正确得加锁、同步,如果多个线程同时区初始化一个类,那么只有一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行方法执行完毕。如果在一个类的方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
三、类加载器
从JVM角度来说: 只存在两种不同的类加载器 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现(HotSpot虚拟机中),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这类加载器都是由JAVA语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。
从开发者角度来说:可以划分为
-
启动(Bootstrap)类加载器:负责将JAVA_HOME/lib的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许通过引用直接操作。
-
标准扩展(Extension)类加载器:是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将JAVA_HOME/lib/ext或者由系统变量java.ext.dir指定位置的类加载到内存,开发者可以直接使用标准扩展类加载器。
-
应用类加载器(Application)类,是由Sun的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将系统路径(CLASSPATH)中指定的类库加载到内存。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中getSystemClassLoader()方法返回值,因此称系统(System)类加载器。
-
除此之外,还有自定义类加载器,他们之间的关系称为类加载器的双亲委派模型,该模型除了顶层的启动类加载器外,其余的类加载器都应该有自己的父加载器,这种父子关系一般通过组合关系来实现,而不是继承。
四、类加载流程源码分析
java.lang.ClassLoader#loadClass()方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查该类是不是已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//该类没有被加载
long t0 = System.nanoTime();
try {
if (parent != null) {
//调用父加载器的loadClass方法
c = parent.loadClass(name, false);
} else {
//调用启动类加载器查找是否已加载,如果没有尝试加载,加载失败返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 非启动类加载器会抛出ClassNotFoundException
}
if (c == null) {
//父类加载器加载不了该类,再调用该类加载器进行加载
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 该类加载器先判断已加载的Class中是否有该name的class对象
- 如果该类加载器没有在自己已加载的类中没有找到,判断是否有父加载器,如果有,调用父加载器的loadClass(String,boolean)方法,否则通过启动类加载器查找是否已加载,如果没有加载然后尝试通过该name查找并加载,如果没有找到返回null。
- 然后执行该类的findClass(name)方法,即在父类的加载范围无法加载该类再交由自己尝试加载。
大致类的加载流程如下:
在看源码时发现java.lang.ClassLoader#loadClass(String name, boolean resolve)方法的注解有个建议:子类推荐重写findClass(String name)方法,而不是loadClass()方法。
下面是findClass()方法。默认是抛出ClassNotFoundException异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
为什么推荐重写findClass方法而不是重写loadClass方法呢,原因在于loadCalss方法是保证类的加载流程满足上图的关键,也就是双亲委派模型。
java.lang.ClassLoader类
对原有注解的理解:ClassLoader是一个抽象类,而类加载器的职责就是将指定的类加载到内存;通过给定一个类的“binary name”定位该类或者生成该类的组成信息。
ClassLoader主要的属性:
private final Classloader parent;
private final Vector<Class<?>> classes = new Vector<>();
ClassLoader主要方法:
void addClass(Class<?> c) { classes.addElement(c);}
由虚拟机,用来记录该类加载器已加载的每个java.lang.Class对象public Class<?> loadClass(String name) throws ClassNotFoundException
sun.misc.Launcher类
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//创建扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//创建应用类加载器,并将该类加载器赋值给Launcher的loader成员变量
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//给当前线程设置上下文类加载器为应用类加载器
Thread.currentThread().setContextClassLoader(this.loader);
//后面省略
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
//省略
}
}
1.类加载器的初始化过程
2.上图类加载器继承关系图
3.发现AppClassLoader和URLClassLoader类重写了loadClass()方法,之前说过该方法是保证双亲委派模型的关键,那重写了之后还能满足双亲委派模型吗?
- AppClassLoader类重写的loadClass()方法
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
//如果系统类加载器已经加载了该类,则直接返回该类的Class对象
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
//如果没有找到直接抛出异常
throw new ClassNotFoundException(var1);
}
} else {
//调用父加载器(ExtClassLoader)的loadClass()方法
return super.loadClass(var1, var2);
}
}
- URLClassLoader类重写的loadClass()方法
public final Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First check if we have permission to access the package. This
// should go away once we've added support for exported packages.
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
int i = name.lastIndexOf('.');
if (i != -1) {
//检查包的可访问性
sm.checkPackageAccess(name.substring(0, i));
}
}
//调用父加载器的loadClass方法(由于URLClassLoader的父类SecureClassLoader没有重写该方法,所以直接调用ClassLoader类的loadClass()方法)
return super.loadClass(name, resolve);
}
可以看出AppClassloader和URLClassLoader虽然重写了loadClass()方法,但是没有破坏双亲委派模型,只是做了一些安全及包访问权限的校验,最终还是调用ClassLoader类的loadClass()方法
4.之前说过java官方推荐子类加载器最好重写findClass()方法,但是AppClassLoader和ExtClassLoader这两个类中没有找到,而ClassLoader的默认实现是什么都不干直接抛出异常,所以猜测在URLClassLoader或者SecureClassLoader类中重写了findClass方法
- 发现在URLClassLoader类中重写了该方法,具体实现如下:
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}