JVM学习03-类加载机制

来源:互联网 发布:网络剧 有毒 第一季 编辑:程序博客网 时间:2024/05/17 23:31

1. 类加载器简介

之前有讲述过 JVM 的启动流程,在JVM找到配置文件对JVM进行初始化的时候,将会生成最基础的类加载器(c/c++语言编写)BootStrap。
在说classLoader前先看一段程序:

ClassLoader classLoader = this.getClass().getClassLoader();System.out.println("当前的ClassLoader : " + classLoader);System.out.println("父ClassLoader : " + classLoader.getParent());System.out.println("曾ClassLoader : " + classLoader.getParent().getParent());// 结果:// 当前的ClassLoader : sun.misc.Launcher$AppClassLoader@2a788b76// 父ClassLoader : sun.misc.Launcher$ExtClassLoader@500c05c2// 曾ClassLoader : null

可以看出来jdk默认当前应用程序的classLoader为AppClassLoader,然后往上的父ClassLoader是ExtClassLoader,然后是null。
那么为什么最顶层的是null,这也就是BootStrap并不是java语言编写的,而是一个顶级的ClassLoader。当JVM启动Bootrap之后,Bootrap就会随之启动ExtClassLoader和AppClassLoader,并完成父子继承关系,最后由AppClassLoader加载主类的字节码,寻找到main函数进行启动。大致如图:
这里写图片描述
1. BootStrapClassLoader:jre路径/lib/rt.jar中的字节码,或者通过 -Xbootclasspath 参数指定路径的下的字节码。
2. ExtClassLoader : jre路径/lib/ext目录下 *.jar 的字节码。
3. AppClassLoader: 加载用户classpath下的字节码。
除了 BootStrapClassLoader 不能直接被程序员使用外,其他的类加载器都能被程序使用和修改。

2. class加载验证流程

要让主类中的main函数运行,当然少不了的就是字节码(class)的加载。这个也是class装载验证流程中的第一步–类加载。然后第二步是连接,最后是进行初始化。

1. 类加载

首先肯定是读取class文件的二进制流。由于Class等类的信息的存储在方法区中的,所以进而转化为方法区的数据结构,为了能够被使用,最后在java堆中生成代表这个字节码的java.lang.Class对象。

2. 链接

在链接中又分为验证,准备和解析等几个步骤。
(1)验证:验证字节码肯定是为了确保字节码的正确性,比如文件是否以0xCAFEBABE开头,版本是否正确等等。这个过程需要对字节码中的内容做大量的验证,诸如文件的格式,元数据,字节码验证,符合引用验证等等。
(2)准备:之前在讲述java成员初始化一节中提到,在初始化之后首先需要对类进行加载。所以在这一步就是对类中的成员进行内存的分配。例如对于

public static int i = 1;

先对i进行内存的分配,然后再赋上初始值为0。注意是准备阶段是0,然后准备阶段完成之后的类的初始化中,才会执行语句赋值为1。
这里在补充一个知识点:编译时常量和运行时常量。

public final static int i = 2;// 对于final static类型, 在准备阶段就会被赋值为2.// 因为i的值不能修改, 所以在编译的时候, i的值就已经确认了// 所以对于final修饰变量称为编译时常量.// ------- 举个面试题byte b1=1,b2=2,b3,b6; final byte b4=4,b5=6; b6=b4+b5; b3=(b1+b2); System.out.println(b3+b6);// 该题目在第四行会报错,原因是(b1 + b2) 会自动将类型提升为int型// 那为毛第三行不报错呢? 原因是b4和b5带有final类型, 编译器会立即知道他们的值,然后对b4+b5进行优化, 代码其实就变成了// b6 = 10; 所以不会报错. 

(3)解析:在解析阶段主要是将符号引用转换为直接引用。符号引用简单来说就是一个字符串,而这个字符串所引用内容的信息必须能够唯一标识一个类、字段和方法等。比如对于一个类的符号引用,必须给出类的全名,如java.lang.Object。而直接引用就是引用某个对象的一个指针或者地址偏移量,是一定存在的引用对象。

3. 初始化

类的初始化,包括static的初始化和非static的初始化,详细参考:http://blog.csdn.net/u013082133/article/details/50427669

3. 类的加载器

3.1 父类委托机制

由上面的类加载器的继承结构可以看出,每个加载器都负责加载自己负责的区域。从java1.2开始,类加载的过程引入了父类委托的机制,这种机制更好的保证了java平台的安全性。
原则:类的寻找是由下往上的,类的加载是由上往下的。
假设平台只有Bootstrap,ExtClassLoader和AppClassLoader三个加载器,当用户类路径下一个类如MyTest类被加载的时候,AppClassLoader首先查看该类是否已经被加载了,如果有就返回该类的字节码,否则询问父亲ExtClassLoader是否已经加载了。同样ExtClassLoader先查看是否自己已经加载了MyTest,如果有就返回,否则继续问Bootstrap。当然由于是在类路径下,Bootstrap也没有加载,也没有父亲可问。这个时候,他就会查看自己是否能够加载MyTest,当然由于在用户路径下(所以不能加载),然后告诉ExtClassLoader说我不能加载。然后同样ExtClassLoader也不能加载并告诉AppClassLoader也不能加载,最后AppClassLoader在查看该类的路径,对该类进行加载。这个就是类的寻找是由下往上的,加载是由上往下的。
实现父类委托的关键代码:
这里写图片描述

3.2 ClassLoader中的一些方法

// 载入并返回一个Classpublic Class<?> loadClass(String name) throws ClassNotFoundException// 定义一个类,不公开调用, jdk提供加载字节码的唯一入口,不能覆盖// 可从磁盘读取字节码protected final Class<?> defineClass(byte[] b, int off, int len)// loadClass回调该方法,自定义ClassLoader的推荐做法protected Class<?> findClass(String name) throws ClassNotFoundException// 寻找已经加载的类protected final Class<?> findLoadedClass(String name) 

3.3 父类委托的缺陷和解决

由于类加载器父类委托的存在,就会导致父类加载器无法加载子类加载器所加载的类。换句话说,比如Java的**SPI机制(Service Provider Interface
)**中,类的接口是定义在rt.jar包中的,而具体的实现是由指定厂商提供,是定义的AppClassLoader中的,如果rt.jar中有一个类需要返回该接口的一个实现,那么就需要加载获取AppClassLoader中这个接口实现类的字节码。
按照父类委托机制,rt.jar的类加载器是无法访问子加载器加载的字节码的,那么该如何解决。
这个就引入了一个线程上下文类加载器的概念,基本思想是给顶层类加载传入底层类加载器的实例。

// 设置线程上下文类加载器Thread.setContextClassLoader(ClassLoader cl)// 获取线程上下文类加载器Thread.getContextClassLoader()

这样,顶层的加载器通过线程的上下文就能访问到底层类加载,从而可以获取底层加载器所加载的字节码。
打破这种默认的父类委托机制模式的应用如Tomcat的WebappClassLoader,OSGi的ClassLoader根据需要自由加载Class等等。
由于父类委托的核心代码是在loadClass方法中实现的,所以当然也可以通过覆盖loadClass方法来打破父类委托的机制,比如:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {    // First, check if the class has already been loaded    // 在findClass中实现类的定义defineClass装载    Class re=findClass(name);    if(re==null){        System.out.println(“无法载入类:”+name+“ 需要请求父加载器");        return super.loadClass(name,resolve);    }    return re;}

4. 自定义类加载器

通过自定义类加载器我们可以对手动生成的字节码进行加载,可以实现热部署等等一些功能。
相信大家经常使用tomcat的热部署,那么关于自定义类的热部署可参照博文:
《Java Class热替换》

其他博文参考:
《Java Classloader机制解析》
《翻译:走出类加载器迷宫》

0 0
原创粉丝点击