由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。
一、class装载验证流程
1、加载
2、链接
连接又可以分为三步:
(1)、验证
(2)、准备
(3)、解析
3、初始化
(一)、class装载验证流程 -加载
1、装载类的第一个阶段
2、取得类的二进制流(通过不同方式取得二进制流的内容)
3、转为方法区数据结构
4、在Java堆中生成对应的java.lang.Class对象
(二)、class装载验证流程 -链接 验证
1、链接 -> 验证 : 是连接的第一步
目的:保证Class流的格式是正确的
(1)文件格式的验证
a.是否以0xCAFEBABE开头(class文件都是以这开头的,不是则不是正确的)
b.版本号是否合理
(2)元数据验证(看基本的数据是否合理,就是检查基本的语义是否合理,是否正确的)
a.是否有父类(比如一个父类或方法不存在,等,都是不真孤儿)
b.继承了final类?
c.非抽象类实现了所有的抽象方法
(3)字节码验证 (很复杂)
a.运行检查
b.栈数据类型和操作码数据参数吻合
c.跳转指令指定到合理的位置
(4).符号引用验证
a.常量池中描述类是否存在
c.访问的方法或字段是否存在且有足够的权限
所有的检查都是大部分,不可能是全部都能验证全,但如果不经过验证则一定是错误的
2、链接 -> 准备
分配内存,并为类设置初始值 (方法区中)
public static int v=1;
在准备阶段中,v会被设置为0
在初始化的<clinit>中才会被设置为1(比如在static和final则是例外,在准备阶段则已经初始化置为1)
对于static final类型,在准备阶段就会被赋上正确的值
public static final int v=1;
3、链接 -> 解析
符号引用替换为直接引用(符号不能真正被使用,只是被引用,直接真正的引用则是通过指针与地址)
符号引用:字符串 引用对象不一定被加载
直接引用:指针或者地址偏移量 引用对象一定在内存
(三)、class装载验证流程 – 初始化
1、执行类构造器<clinit>(clinit表示类初始化)下面则是类初始化就会被初始化
static变量 赋值语句
static{}语句
2、子类的<clinit>调用前保证父类的<clinit>被调用
3、<clinit>(类初始化)是线程安全的,一个类初始化时其他类则等待。
(四)、class装载验证流程-总结
Java.lang.NoSuchFieldError错误可能在什么阶段抛出
二、什么是类装载器ClassLoader
1、ClassLoader是一个抽象类
2、ClassLoader的实例将读入Java字节码将类装载到JVM中
3、ClassLoader可以定制,满足不同的字节码流获取方式(可以从网络中加载class文件,从文件中加载等等方式)
4、ClassLoader负责类装载过程中的加载阶段(他只负责把类读进来)
三、JDK中ClassLoader默认设计模式
1、ClassLoader的重要方法
public Class<?> loadClass(String name) throws ClassNotFoundException
载入class并返回一个Class
protected final Class<?> defineClass(byte[] b, int off, int len)--------->byte[]指的是二进制流的class文件
定义一个类,不公开调用
protected Class<?> findClass(String name) throws ClassNotFoundException
findClass时 loadClass回调该方法,自定义ClassLoader的推荐做法,去重载这个方法
protected final Class<?> findLoadedClass(String name)
寻找已经加载的类,已经加载则不会二次进行加载
(一)、JDK中ClassLoader默认设计模式 – 分类
1、BootStrap ClassLoader (启动ClassLoader)
2、Extension ClassLoader (扩展ClassLoader)
3、App ClassLoader (应用ClassLoader/系统ClassLoader)这个classloader去加载跟我们应用相关的class。
4、Custom ClassLoader(自定义ClassLoader)
5、每个ClassLoader都有一个Parent作为父亲
(二)、JDK中ClassLoader默认设计模式 – 协同工作
JDK中classloader括谱图:
工作顺序如下图:
自底向上检查类是否已经加载;跟我们应用相关的,也是最主要的则是App ClassLoader这个加载器,我们自己写的类都是加载在这个地方的。什么是自底向上检查类是否已经加载呢?就是当我们去找一个类的时候,会在当前class类中去找。因此他会在App ClassLoader中去找我们写的类中是否已经加载这个类。如果自己写的class没没有做找到,他不会去做加载。他会把这个请求传给他的父类,也就是Extension ClassLoader.(扩展classLoader),徐问他是否这个class类。他也是一样,会在当前自己的class类中去找,找到则返回,否者继续去他的父类中去找。如果最近没有找到,则会去加载所需要的这个类。
而,加载则是相反的,可以这么理解,他是自底向上的询问,那么到了最顶端,则开始自上而下的去加载:他不会再在某个阶段的classloader找不到时就去加载这个类。
他是会去由启动ClassLoader去加载,加载成功则返回,如果加载失败;,则会去扩展classLoader去加载,如果也失败,则会去让AppclassLoader去加载
r
rt.jar这个是启动classLoader的架包,他也是系统的核心类.所以启动classLoader,则区加载这个架包中的所有类。当然在jvm启动时可以去配置后面-Xbootclasspath这个参数,去启动这个类的路径,那么这个里面的class也会存在启动classLoader里面的。
我们这个路径下的jar包都会被这个扩展ClassLoader所加载。
而我们classpath下载类都是被appClassLoader所加载。
下面则是classLoader代码实现
(1)他先去找,找不到,则去加载。
2、下面案例测试加载顺序:
上面则可以看出他是加载在appclassloader中的,因为他们是在我们创建的findorder包中的,下面演示把他们放在如下图的右上角中位置,这个位置可以是当前系统中任何位置,他跟我们classpth是没关系的。
然后通过下面来测试他们两个的当执行main函数时,他们是如何找到这两个类的。
1、直接运行上面代码:我们可以发现他是加载在appclassloader中的
I am in apploader
2、加上参数 -Xbootclasspath/a:D:/tmp/clz (当我们加上这个参数,指定他的加载位置及上面右上角的位置时,我们可以看到如下:)
I am in bootloader
此时AppLoader中不会加载HelloLoader(因为他找不到这个类了,然后他由当前往上找也没有找到,那他就开始加载,以为在bootloader指定了这个类,所以他加载成功)
也就是 I am in apploader 在classpath中却没有加载
因此由上: 说明类加载是从上往下的
下面强制它在apploader中加载
public static void main(String args[]) throws Exception {
ClassLoader cl=FindClassOrder2.class.getClassLoader();
byte[] bHelloLoader=loadClassBytes("geym.jvm.ch6.findorder.HelloLoader");
Method md_defineClass=ClassLoader.class.getDeclaredMethod("defineClass", byte[].class,int.class,int.class);
md_defineClass.setAccessible(true);
md_defineClass.invoke(cl, bHelloLoader,0,bHelloLoader.length);
md_defineClass.setAccessible(false);
HelloLoader loader = new HelloLoader();
System.out.println(loader.getClass().getClassLoader());
loader.print();
}
通过反射去调用这个方法。
-Xbootclasspath/a:D:/tmp/clz 对于这个程序依然用这个参数。运行如下面结果:
I am in apploader 这是因为之前我们强行的让Appclassloader去加载这个类。
因为开始在查找类的时候,先在底层的Loader查找,是从下往上的。Apploader能找到,就不会去上层加载器加载
5、能否只用反射,仿照上面的写法,将类注入启动ClassLoader呢?
(三)、JDK中ClassLoader默认设计模式 – 问题
上面介绍的加载过程其实是jdk中的双亲模式,因为我们看到每个clasLoader都会有一个parent,一个父亲,但是这个父亲不是他的super的class,不是他的超类,而他的内部会聚集一个classloader类,作为他的parent,当每次加载或者去找类的时候,会去委托这个parentq去做一些事情,但是这个模式存在一个小问题。 顶层ClassLoader,无法加载底层ClassLoader的类,也就是说在启动的这个classLoader里面无法生成加载 appClassLoader中的任何一个类。既然无法加载这个类,也就没有办法去生成这个类的实例对象。
双亲模式的问题: 顶层ClassLoader,无法加载底层ClassLoader的类
Java框架(rt.jar)如何加载应用的类?
javax.xml.parsers包中定义了xml解析的类接口 Service Provider Interface SPI 位于rt.jar 即接口在启动ClassLoader中。 而SPI的实现类,在AppLoader。
(四)、JDK中ClassLoader默认设计模式 – 解决(解决双亲问题)
1、Thread. setContextClassLoader()
-》上下文加载器
-》是一个角色
-》用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题
-》基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例
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 {
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return cl.loadClass(className); //使用上下文ClassLoader
}
}
}
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());
}
…..
代码来自于 javax.xml.parsers.FactoryFinder 展示如何在启动类加载器加载AppLoader的类
代码中可看书cl就是上下文的加载器
上下文ClassLoader可以突破双亲模式的局限性
6、双亲模式的破坏
双亲模式是默认的模式,但不是必须这么做
破坏的方式有很多种,如下面:
1.Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent
2.OSGi的ClassLoader形成网状结构,根据需要自由加载Class
7、破坏双亲模式例子- 先从底层ClassLoader加载
OrderClassLoader的部分实现
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;
}
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(className);
if (null == clazz) {
try {
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;
}
下面是使用测试如下
下面是打印结果:
这个例子可以看出由底层开始加载。
如果OrderClassLoader不重载loadClass(),只重载findClass,那么程序输出为
四、热替换
1、含义:
当一个class被替换后,系统无需重启,替换的类立即生效 比如PHP很容易实现,但java很难实现。
例子: geym.jvm.ch6.hot.CVersionA
public class CVersionA {
public void sayHello() {
System.out.println("hello world! (version A)");
}
}
2、DoopRun 不停调用CVersionA . sayHello()方法,因此有输出:
hello world! (version A)
3、在DoopRun 的运行过程中,替换CVersionA 为:
public class CVersionA {
public void sayHello() {
System.out.println("hello world! (version B)");
}
}
4、把class文件在系统中给替换掉;替换后, DoopRun 的输出变为
hello world! (version B)
最后,思考如果通过classloader如何去实现这个功能。
总结:jdk的classloader没有强制大家去使用这个双新模式。因此我们可以重载这个classloader去做很多的事情。比如tomcat、osgi等他们都是破坏双亲模式的产物。因此有这个机制存在才会让我们的程序存在无线的可能性。必须热替换;没有这个机制是无法实现的。因此这个功能给大家带来无限的想像空间。