字节码文件的加载全过程
都知道JAVA会被编译器编译为字节码文件而字节码文件会被类加载器加载进虚拟机中变成其运行时数据,即变成JVM的JAVA类型才能执行,那么这个字节码文件的加载全过程是怎么样的?
加载全过程:
1)加载:把字节码读取入虚拟机内
2)验证:由于JVM是和字节码文件绑定而不是某一门编程语言绑定所以它会对加载的字节码文件进行验证,总的来说会有如下的几部:文件格式验证(即校验这个字节码文件是否是符合JVM规范的)、原数据验证(即校验字节码文件的内容是否符合JAVA语言规范)、字节码验证(方法体的验证)、符号引用验证(即符号所引用的方法或者字段是否存在等)
3)准备:即对静态变量进行分配空间初始化值,以便于对象使用时其属性也可以使用(因为字节码文件的“使用”和“初始化”这两步骤的开始不一定是“初始化”在“使用前”)但“准备”这个步骤是按部就班的开始于使用前的,所以有了“准备”为静态类属性分配空间赋值后再使用阶段即使还没“初始化”也可以使用其静态字段
4)解析:该步骤是将“符号引用”变为“直接引用”
符号应用:可以理解为,比如在一个方法中应用了某个类的字段时,此时并不会直接以指针的形式去指向这个字段的真的内存地址,而是引用字节码文件中常量池中对该字段的符号应用(比如FileType这个常量),而“符号引用”最关键的一点我个人的理解是就好像“面向接口编程”真正的实现类只在运行时才关心,这里的实现类可以类比“直接引用”
直接应用:就是通过符号引用所指向的某一个类或者字段,看是否 的确在内存中存在这个类了或者这个字段了,然后把之前的“符号应用”修改为真实的执行它的内存地址
5)初始化:初始化就是执行我们定义的代码了,而这里的“代码”只是静态变量的赋值、静态语句块,最终会被JVM生成类文件的时候将这些我们定义的“代码”封装到一个类构造器中按源程序定义的顺序去执行。这里还要注意的一点就是类的加载不等效于类的初始化,一个被加载了的类是否还要初始化看一下的几种情况:
1、该类被实例化
2、调用了该类的静态函数或者使用了该类的静态变量(赋值和读取)
3、父类会优先于子类被初始化
4、反射使用该类
5、JVM默认的初始化类(含main函数的类)
这几种情况会导致类的初始化,其他的方式使用该类不会导致该类被初始化。
6)使用
7)类的卸载(对于被启动加载器和扩展加载器以及系统加载器加载的类在JVM的生命周期内是不会被卸载的,而自定义的类加载器的类是可以被卸载的)
类加载器概述
知道了字节码文件的整个加载过程后,我们就指定类加载器的作用了,其实类加载器的作用就是整个加载过程中的“加载”这一小过程,即把二进制的字节码文件加载进JVM中
类与其类加载器的关系
对于类与类加载器的关系,有如下的关系:
1)类被类加载器加载进JVM
2)类会引用加载它的类加载器,而加载它的类加载器也叫做它的定义加载器
3)类加载器会引用被它加载的字节码文件生成的字节码文件对象
4)类的唯一性和加载它的类加载器有关,即类加载器相同的类才可能相同
类加载器的双亲委派加载机制解析
可能大家都知道类加载器的加载机制即“双亲委派机制”,即子类加载一个字节码文件时都会先把它的加载请求委托给它的父加载器先加载,而父加载器加载不了后才有子加载器加载。那么我这里说的关于加载机制有三点细节:双亲委派加载机制的代码体现在哪里?、双亲委派机制的优势?如何破坏双亲委派机制
1)双亲委派机制的代码体现其实就是抽象类ClassLoader中的loadClass方法,代码如下:
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 {
//如果该加载器没有父类或者所父类是null,那么就用启动加载中去加载
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;
}
}
所以从源码分析我们可以知道,所谓的双亲委派机制是如何实现的,所以我们可以总结一句话:“从下往上判断类是否被加载,从上往下尝试加载类”,其实这句话也揭示了我们常说的一句话:“子加载器可以看到父类加载器加载的类,但父类加载器无法查看子类加载的类(仔细体会,不再讲述了)”
2)双亲委派机制的好处:
即为什么要用双亲委派,一、是保证了核心类的一致性,二、保证了软件运行的安全性,如果自己写的程序不符合字节码文件规范,那么在用类加载器加载时先委派父类加载,而父类加载时会依据严格的字节码文件规范加载,对不符合的字节码文件是不会加载的。
3)如何破坏委派机制
1、由于JDK1.2才出现的双亲委派机制但类加载器是JDK1.0就有了的,所以为了兼容以前的类加载器,自定义的类加载器直接覆盖loadClass实现自己的加载思路就行了,但JDK1.2的双亲委派机制的实现都在loadClass中覆盖了就破坏了双亲委派机制了,所以现在都是覆盖findClass来实现自己的加载思路而不是覆盖loadClass
2、当核心类中要使用我们自定义类时由于启动加载器或者扩展加载器等无法加载我们自定义的实现类(比如核心类中只是我们的要实现类的接口),那么此时父类加载器会委派子类加载器去执行这个加载任务,比如线程上下文加载器(上下文加载器只是一个角色,任何加载器都可以充当,它所要做的就是传递给父类加载器,有父类加载器依靠他去加载它不能加载的类)
自定义类加载器
对于自定义的类加载器注意以下几点即可:
1)继承ClassLoader,覆盖findClass方法实现自己的加载思路
2)自定义的类加载器可以指定器父加载器,如果不指定那么系统加载器就默认为父类加载器
编程常识与类加载器
1)在一个类中使用一个不同加载器加载的类时会保错,因为由不同类加载器加载的类是相互不可见的,如果要访问,只有使用反射
2)对于同一个包中的类访问另外一个类的默认(default)属性时可以的,但不同包的其他类访问是不可以,因为类在加载时如果是同一个包下并且又是同一个加载器加载时会有“运行时包”的概念,即默认属性只有同属一个“运行时包”的类才可以访问。
Class.forName()与类加载器的加载的区别
1、Class.forName的加载类,底层会调用它的重载方法,Class.forName(className,true,classloader)即使用调用这句代码的类的类加载器去加载指定的类同样会遵守双亲委派机制,同时会初始化这个类,true是默认指定的即代表会初始化这个类
2、类加载器的加载,不会初始化被加载的类