java文件在编译时会被JVM编译成.class字节码文件,这篇主要讲解的是JVM如何将.class文件加载的加载过程。
虚拟机的类加载过程,其实就是将.class文件加载到内存中,并对数据进行校验、准备、初始化,最后经过初始化形成可以被我们看懂的Java类型。
完整的类加载过程如下图:
一、类生命周期
类从被加载到到虚拟机中,到卸载出内存为止,总共需要经过7个阶段,分别是加载—>验证—>准备—>解析—>初始化—>使用—>卸载。
完整的类从加载到被回收的生命周期如下图:
二、类加载过程
· 加载阶段(理解即可)
加载的目的就是为了将被编译后.class文件转换成二进制字节流,从磁盘读取到内存。
类加载的三种方式:
(1)加载本地磁盘上的java文件路径。
(2)从网络上获取,读取二进制字节流。
(3)从网上下载class文件,如:jar包。
如何确定一个类是唯一的?
(1)通过java文件的全路径名。
(2)通过加载类的类加载器。
· 连接阶段
1、验证(理解即可)
验证是连接阶段的第一步,主要目的是为了验证字节码的正确性。
文件格式验证:
(1).class文件是否以魔数(OxCAFEBABE)开头。
(2)主、次版本号是否在当前虚拟机处理范围之内。
(3)常量池的常量中是否含有不被支持的常量类型。
...
元数据验证
(1)这个类是否有父类(除了Object,其他类都有父类)。
(2)这个类的父类是否继承了不允许被继承的类(被final修饰的类不允许被继承)。
...
字节码验证
(1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现这样的情况:在操作栈放置了一个int类型的数据,使用时按照double类型来载入本地变量表中。
(2)保证跳转指令不会跳转到方法体以外的字节码指令上。
...
2、准备(理解即可)
验证通过后,说明当前的.class文件是安全的,就进入连接的第二阶段,准备阶段,该阶段主要是为给类的被static修饰的变量分配内存并设置变量的初始值的阶段,因为是静态变量或常量所以所使用的内存都在方法区分配。
示例一:static int a;
表面在准备阶段,该变量a在方法区中被分配了4byte的内存,并设置了默认值为0。
示例二:static int b = 123;
表面在准备阶段,该变量b在方法区中被分配了4byte的内存,但是此时设置的默认值还是0,而b=123的赋值操作是在后续的初始化阶段来完成。所以准备阶段的默认值是0,不是123。
实例三:static final int c= 100;
如果变量是一个常量,那么在准备阶段,该变量b在方法区中被分配了4byte的内存,并且在此时会把该常量的默认值设置为100。
3、解析(理解即可)
解析阶段是虚拟机将符号引用替换为直接引用的过程。
· 初始化(重要)
初始化阶段表明类需要被使用到了才会去初始化这个类。
对于静态变量、静态块、非静态变量、非静态块、构造器类的初始化顺序:
父类静态块和父类静态变量—>子类静态块和子类静态变量—>父类非静态块和父类非静态变量—>父类构造器—>子类非静态块和子类非静态变量—>子类构造函数
初始化代码如下:
代码语言:javascript复制public class Test {
public static String testStaticStr = "我是父类Test静态变量";
public String testStr = "我是父类Test非静态变量";
{
System.out.println("父类非静态块初始化开始");
System.out.println("父类-打印父类非静态变量是否被初始化:" testStr);
System.out.println("父类非静态块初始化结束");
}
static {
System.out.println("父类静态块初始化开始");
System.out.println("父类-打印父类静态变量是否被初始化:" testStaticStr);
System.out.println("父类静态块初始化结束");
}
public Test() {
System.out.println("父类构造函数初始化");
}
public static void main(String[] args) {
sonTest sonTest = new sonTest();
}
}
class sonTest extends Test {
public static String sonTestStaticStr = "我是子类Test静态变量";
public String sonTestStr = "我是子类Test非静态变量";
{
System.out.println("子类非静态块初始化开始");
System.out.println("子类-打印子类非静态变量是否被初始化:" sonTestStr);
System.out.println("子类非静态块初始化结束");
}
static {
System.out.println("子类静态块初始化开始");
System.out.println("子类-打印子类静态变量是否被初始化:" sonTestStaticStr);
System.out.println("子类静态块初始化结束");
}
public sonTest() {
System.out.println("子类构造函数初始化");
}
}
返回结果:
三、双亲委派(重点)
· ClassLoader
在JVM中存在ClassLoader,就是类加载器,他的作用就是将.class文件加载到java虚拟机中,但是在JVM启动时并不会加载所有的.class文件,而是当我们需要用到对应的类时才会进行加载(即时编译)。
在Java类加载时会有三个自带的加载器,分别是Bootstrap ClassLoader、Extension ClassLoader、Appliaction ClassLoader。
Bootstrap ClassLoader是启动类加载器,这个类加载器负责将存放在JAVA_HOME中lib下的rt.jar、resources.jar、charsets.jar等类库。这个加载器由于本身就是虚拟机的一部分,所以他是由C/C 来编写的。因此在程序中使用通用的getClassLoader来获取该加载器的时候,可能会为null,下面会在源码中体现出来。
Extension ClassLoader是扩展类加载器,这个类加载器负责加载JAVA_HOMElibext目录中的类库。在Java中由ExtClassLoader实现。
Application ClassLoader是系统类加载器,负责加载用户类路径上所指定的类库,我们自己开发的Java类一般都是由该加载器进行加载,是程序中默认的加载器。在Java中由ApplicationClassLoader实现。
类加载的加载器模型如图所示:
示例一:
如图所示,LoaderController类的类加载器为AppLcassLoader,可以看出我们平时在编写代码时编写的类都是用AppClassCloader加载器来加载的。
示例二,查看String类是由哪个加载器加载出来的?
如上图所示,当查询String类的类加载器时,运行后却出现了空指针异常。那是因为前面的概念已经说过,Bootstrap加载器的作用是加载JAVA_HOME中lib内的类库,而String类正好属于rt.jar,所以String理论上是由Bootstrap加载器进行加载的,为空指针是因为Bootstrap加载器是由C /C编写的,无法查询到Bootstrap加载器。
示例三,应用程序加载器和扩展类加载器的关系?
如上图所示,当前类的加载器为AppClassLoader,而当前类加载器的父类为ExtClassLoader。说明AppClassLoader的父类加载器是ExtClassLoader。
示例四,应用程序加载器和扩展类加载器和启动类加载器的关系?
如上图所示,当寻找ExtClassLoader的父类加载器时,又出现了空指针异常。那么这里就有了一个疑问,示例四种的空指针异常的原因是不是与查询String类加载器异常的原因一样呢?
接下来我们带着疑问来看源码。首先查看ExtClassLoader和AppClassLoader的源码,发现他们实际上都是在sun.misc.Launcher中。
从源码中可以看到,他们之间的关系,其实是
代码语言:javascript复制AppClassLoader extends URLClassLoader
代码语言:javascript复制ExtClassLoader extends URLClassLoader
代码语言:javascript复制URLClassLoader extends SecureClassLoader
代码语言:javascript复制SecureClassLoader extends ClassLoader
关系结构如下图:
从关系结构图来看,应用加载器和扩展类加载器并不是父子关系,而他们两个继承的也不是Bootstrap加载器。所以我们继续带着问题看源码,查看加载器的父类的话是调用getParent()方法,所以可以在对应的类中找该方法,发现在这几个类中,只有ClassLoader中存在getParent方法。
代码语言:javascript复制private final ClassLoader parent;
上图中ClassLoader中的getParent()方法的目的其实就是返回了ClassLoader变量parent,找到parent中赋值的地方在构造函数中,如下图:
代码语言:javascript复制protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
上图构造函数说明parent可以由外部传入,然后返回外部传入的ClassLoader作为parent。
代码语言:javascript复制
代码语言:javascript复制protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
代码语言:javascript复制public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
该构造函数说明,当没有外部传入类加载器时,ClassLoader的parent就是系统的AppClassLoader,即一个类的父类加载器如果没有指定那么默认就是AppClassLoader。这段代码的说明其实就是AppClassLoader、ExtClassLoader、BootstrapLoader加载器依赖关系的说明的核心。
接下来查看AppClassLoader、ExtClassLoader是如何被创建的。其实就是在Launcher()类的构造函数中。
先看第一个红框,ExtClassLoader的创建
跟到底后发现,调用了ClassLoader中外部传入加载器的构造函数。
并且这里的parent为(ClassLoader)null,这里为null的加载器其实就是Bootstrap加载器。这一段源码说明,ExtClassLoader的父类就是Bootstrap加载器。
继续查看AppClassLoader的创建。同理:
AppClassLoader中也调用了ClassLoader中外部传入加载器的构造函数,并且这里的parent变量其实就是前一行刚创建出来的ExtClassLoader。因此,这一段代码说明了AppClassLoader的父类就是ExtClassLoader。
· 双亲委派 终于来了 面试必问
前面看了这么多加载器被创建出来的过程铺垫,其实就是为了证明类被加载时是需要经过双亲委派的。理论上加载器之间的关系为:AppClassLoader—>ExtClassLoader—>BootstrapClassLoader。
查看类被加载时候的源码,个人翻译后:
结合加载器的结构和这段源码其实可以看出类加载过程 - 双亲委派就是那么简单。
当类被加载时,AppClassLoader查看自己是否加载过该类,如果没有则委托给父类ExtClassLoader(这其实是一段递归),ExtClassLoader查看自己是否加载过该类,如果没有则委托给父类BootstrapClassLoader,BootstrapClassLoader查看自己是否加载过该类,如果没有,则去自己lib类库中搜寻是否有该类。如果没有则交给ExtClassLoader搜寻lib/ext类库中是否有该类。如果仍没有则交给AppClassLoader来加载,如果AppClassLoader加载失败,则会抛出ClassNotFound异常。
双亲委派的特点:委托自下而上,加载自上而下。
· 为什么类加载的时候需要遵循双亲委派规则?
在类加载中确定是否是一个类是根据类的全路径名和加载该类的类加载器是否是同一个这两条件来决定的。而在实际开发中,如果我们命名一个与类库中相同路径名的类 - 假设我们命名了java.lang.String,该String类与rt.jar中的java.lang.String路径名相同,如果不遵循双亲委派,那么在经过第一层AppClassLoader中发现了存在自己开发的java.lang.String类,就加载了该类,从而没有加载方法更全的需要被BootstrapClassLoader加载出来的在rt.jar中的java.lang.String类,间接导致String类库被替换。如果遵循了双亲委派规则,委托到BootstrapClassLoader中就会把正确的String类加载出来,这样使得类库不会随意被篡改,更加安全。
· 打破双亲委派规则
虚拟机中的加载规则是按需加载的,即需要使用到需要的类时才会加载该类,并且在加载该类时所用的是什么加载器,加载对应需要被引用的类也是那个加载器。因此在类加载中并不是所有的类加载都遵循双亲委派规则。比如我们最常用的JDBC。
在JDBC中会加载DriverManager类,源码如下:
代码语言:javascript复制static {//这里会加载Driver驱动
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
发现在加载DriverManager时需要依赖Driver,因此对Driver驱动进行加载的时候引入了,ServiceLoader的机制。
代码语言:javascript复制ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
发现:
代码语言:javascript复制public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Driver类是通过线程的上下文加载器来进行加载的。这个线程上下文加载器本质上是AppClassLoader。
因此,JDBC打破了双亲委派原则是因为使用BootstrapClassLoader加载了DriverManager时引用需要引用到Driver驱动,而在源码中可以看到应该加载Driver驱动的BootstrapClassLoader变成了线程上下文加载器进行加载,该线程默认是AppClassLoader。所以打破了双亲委派原则。
四、自定义类加载器
按照双亲委派策略来自定义一个类的加载器。具体步骤如下:
(1)需要继承ClassLoader类。
(2)重新findClass方法。
(3)利用反射来执行自定义类中的方法。
代码语言:javascript复制public class TestClassLoader extends ClassLoader {
private String path;
public TestClassLoader(String path) {
this.path = path;
}
/**
* 重新findClass方法
*
* @param name 类名
* @return 类
* @throws ClassNotFoundException
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name == null || "".equals(name)) {
return null;
}
//class文件名
String fileName = name ".class";
File file = new File(path, fileName);
//将class文件 转化成二进制字节流
if (file.exists()) {
FileInputStream fileInputStream;
ByteArrayOutputStream outputStream;
try {
fileInputStream = new FileInputStream(file);
outputStream = new ByteArrayOutputStream();
int size;
try {
while ((size = fileInputStream.read()) != -1) {
outputStream.write(size);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes = outputStream.toByteArray();
fileInputStream.close();
outputStream.close();
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
自定义类加载器如上图,继承了ClassLoader方法,根据类加载的目的作用来重新findClass方法,name为传入的类名,将类名转换成编译后的类名.class文件,根据文件路径名读取该文件并转化为二进制流,最后通过defineClass方法转换成Class对象。
加载测试类代码如下:
代码语言:javascript复制public class LoaderTest {
public void test() {
System.out.println("加载测试类 - 方法执行");
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//创建自定义的测试类加载器
TestClassLoader testClassLoader = new TestClassLoader("/Users/yuwenlei/webuy/leetcode/src/main/java/com/ywl/leetcode");
//加载测试类
Class<?> aClass = testClassLoader.loadClass("com.ywl.leetcode.LoaderTest");
if (aClass != null) {
Object object = aClass.newInstance();
Method test = aClass.getDeclaredMethod("test");
test.invoke(object);
System.out.println(aClass.toString());
System.out.println(aClass.getClassLoader().getParent());
System.out.println(aClass.getClassLoader().getParent().getParent());
}
}
}
测试类中建立一个test()方法,通过类加载器指定类的路径,并传入需要加载的类,加载该测试类。通过反射获取LoaderTest被加载后的Class对象,执行test方法。
执行结果:
test()被成功执行,并且可以发现当前的类加载器不为AppClassLoader,为自定义的LoaderTest类,自定义加载器的父类加载器为ExtClassLoader,ExtClassLoader的父类为null(BootstrapClassLoader)。