类装载器
Posted Lee_Sung
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类装载器相关的知识,希望对你有一定的参考价值。
类装载器
1、class装载验证流程
- 加载
- 链接
- 验证
- 准备
- 解析
- 初始化
1-1 加载
- 装载类的第一个阶段
- 缺的类的二进制流
- 转为方法区数据结构
- 在Java堆中生成对应的java.lang.Class对象
1-2 链接
1-2-1 链接 -> 验证
目的:保证class流的格式是正确的
- 1、class文件格式的验证
- 是否以0xCAFEBABE开头
- 版本号是否合理
- 元数据验证
- 是否有父类
- 继承了final类?
- 非抽象类实现了所有的抽象方法
- 字节码验证 (很复杂)
- 运行检查
- 栈数据类型和操作码数据参数吻合
- 跳转指令指定到合理的位置
- 符号引用验证
- 常量池中描述类是否存在
- 访问的方法或字段是否存在且有足够的权限
同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 |
---|---|---|---|
Private | √ | ||
Default | √ | √ | |
Protected | √ | √ | √ |
Public | √ | √ | √ |
1-2-2 链接 ->准备
- 分配内存,并为类设置初始值 (方法区中)
Eg.
public static int v=1;
/*
- 在准备阶段中,v会被设置为0
- 在初始化的<clinit>中才会被设置为1
*/
public static final int v=1; //对于static final类型,在准备阶段就会被赋上正确的值
1-2-3 链接->解析
- 符号引用(字符串引用对象不一定被加载)替换为直接引用(指针或者地址偏移量,引用对象一定在内存)
1、符号引用:符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。即:
- 对于其他类的符号引用必须给出类的全名。
- 对于其他类的字段,必须给出类名、字段名以及字段描述符。
- 对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。
比如说在没有继承的情况下,类默认的父类是java.lang.Object类,符号引用即在class的常量值里面有个字符串“java.lang.Object”。符号引用并不能直接地被用到,只是一种表示方式。
2、直接引用:
- 对于指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的本地指针。
- 指向实例变量、实例方法的直接引用都是偏移量。实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量。实例方法的直接引用可能是方法表的偏移量。
3、在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类 的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
1-3初始化
执行类构造器< clinit >
- static变量 赋值语句
- static语句
子类的< clinit >调用前保证父类的< clinit >被调用
类的初始化过程:
首先,初始化父类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化。
然后,初始化子类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化。
其次,初始化父类的普通成员变量和代码块,再执行父类的构造方法。
最后,初始化子类的普通成员变量和代码块,再执行子类的构造方法。
< clinit >是线程安全的
2、装载器ClassLoader
2-1 什么是类装载器ClassLoader
- ClassLoader是一个抽象类
- ClassLoader的实例将读入Java字节码将类装载到JVM中
- ClassLoader可以定制,满足不同的字节码流获取方式
- ClassLoader负责类装载过程中的加载阶段
ClassLoader包括bootstrap classLoader(启动类加载器),ClassLoader在JVM运行时加载Java核心API,其中包括用户自定义的ClassLoader:
- ExtClassLoader:用来加载Java的扩展API,也就是/lib/ext中的类;
- APPClassLoader:用来加载用户机器上CLASSPATH设置目录中的Class,通常在没有指定ClassLoader的情况下,程序员自定义的类就由改ClassLoader进行加载
2-2 ClassLoader加载流程
(1)当运行一个程序的时候,JVM启动,运行bootstrap classloader,改ClassLoader加载Java核心API(ExtClassLoader和AppClassLoader也在此时被加载);
(2)调用ExtClassLoader加载扩展API;
(3)APPClassLoader加载CLASSPATH目录下定义的Class。
3、 JDK中ClassLoader默认设计模式
3-1 ClassLoader的重要方法
- public Class
3-2 分类
- BootStrap ClassLoader (启动ClassLoader)
- Extension ClassLoader (扩展ClassLoader)
- App ClassLoader (应用ClassLoader/系统ClassLoader)
- Custom ClassLoader(自定义ClassLoader)
每个ClassLoader都有一个Parent作为父亲(BootStrap ClassLoader是最早的,所以没有)
3-3 协同工作
Eg.
public class HelloLoader
public void print()
System.out.println("I am in apploader");
public class FindClassOrder
public static void main(String args[])
HelloLoader loader=new HelloLoader();
loader.print();
//将下面这个类放在与上面两个类的路径下,如D:/test
public class HelloLoader
public void print()
System.out.println("I am in bootloader");
/*
1、直接运行以上代码:
I am in apploader
2、加上参数 -Xbootclasspath/a:D:/test
I am in bootloader //从底向上找寻,发现没有被加载,那么从顶向下加载,即bootstrap classloader先加载,bootstrap classloader的启动路径被指定为D:/test,且找到了要加载的类,所以运行结果如此。
此时AppLoader中不会加载HelloLoader,I am in apploader 在classpath中却没有加载,说明类加载是从上往下的
*/
4、双亲模式的问题及解决方法
4-1 双亲模式问题
- 顶层的ClassLoader,无法加载底层ClassLoader的类
- Java框架(rt.jar)如何加载应用的类?
javax.xml.parsers包中定义了xml解析的类接口Service Provider Interface SPI 位于rt.jar,即接口在启动ClassLoader中。而SPI的实现类,在AppLoader。
- Thread. setContextClassLoader()——解决双亲模式问题
- 上下文加载器
- 是一个角色
- 用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题
- 基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例
Eg.
//代码来自于javax.xml.parsers.FactoryFinder,展示如何在启动类加载器加载AppLoader的类
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
try
if (cl == null)
if (useBSClsLoader)
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
else
//上下文ClassLoader
cl = ss.getContextClassLoader();
if (cl == null)
throw new ClassNotFoundException();
else
//使用上下文ClassLoader,可以突破双亲模式的局限性
return cl.loadClass(className);
else
return cl.loadClass(className);
catch (ClassNotFoundException e1)
if (doFallback)
// Use current class loader - should always be bootstrap CL
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
…..
4-2 打破双亲模式
- 双亲模式是默认的模式,但不是必须这么做
- Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent
- OSGi的ClassLoader形成网状结构,根据需要自由加载Class
Eg. 破坏双亲模式例子- 先从底层ClassLoader加载
/*
OrderClassLoader的部分实现
先尝试自己加载一个类,如果找不到,则让父类做加载
如果OrderClassLoader不重载loadClass(),只重载findClass,那么类由APPClassLoader加载
*/
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
// First, check if the class has already been loaded
Class re=findClass(name);
if(re==null)
System.out.println("无法载入类:"+name+"需要请求父加载器");
return super.loadClass(name,resolve);
return re;
/*
findClass会去查找类,定义类,加载类,达到了从底层开始加载的目的
*/
protected Class<?> findClass(String className) throws ClassNotFoundException
Class clazz = this.findLoadedClass(className); //首先看类有没有被加载
if (null == clazz)
try
//如果没有,则去读文件,把类的二进制byte数组读进来
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//省略部分代码
fis.close();
byte[] bytes = baos.toByteArray();
//把类真正定义起来
clazz = defineClass(className, bytes, 0, bytes.length);
catch (FileNotFoundException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
return clazz;
5、热替换
- 含义:当一个class被替换后,系统无需重启,替换的类立即生效
Eg.
public class CVersionA
public void sayHello()
System.out.println("hello world! (version A)");
/*
DoopRun不停调用CVersionA.sayHello()方法,因此有输出:hello world! (version A)。
在DroopRunDoopRun 的运行过程中,替换CVersionA为下例(替换的是 .class 二进制文件):
*/
public class CVersionA
public void sayHello()
System.out.println("hello world! (version B)");
/*
替换后,DroopRun的输出变为:hello world! (version B)
*/
以上是关于类装载器的主要内容,如果未能解决你的问题,请参考以下文章