解密Java虚拟机如何加载一个Class文件
Posted 云时代架构
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解密Java虚拟机如何加载一个Class文件相关的知识,希望对你有一定的参考价值。
首先,我们来简单的回顾下类加载机制中的内容。
类加载机制
虚拟机把类的数据从.class文件加载到内存,并对class文件中的数据进行校验、转换、解析、初始化等操作后,最终形成可以被虚拟机识别并使用的Class对象的过程就叫做“虚拟机的类加载”,主要包括为3大阶段。
阶段一:加载
加载,类加载器通过类的全限定名来获取类的二进制字节流,获取的方式可以通过jar包、war包、网络、JSP文件中获取,绝大部分情况下是通过jar包、war包中获取。
获取到字节流后,会将字节流中的信息转化为方法区中的运行时数据结构。在内存中,生成代表该类的Class对象,作为访问该类的数据入口。
阶段二:连接
连接比较复杂,分为3个小阶段:
验证:确保被加载类的正确性,即确保被加载的类符合javac编译的规范,可编译通过的代码。
准备:为类的静态变量分配内存,并初始化为默认值(零值)。
解析:将类中的符号引用转化为直接引用。
阶段三:初始化
为类的静态变量赋值,与连接阶段中的的准备不同。此阶段,代码可debug查看。
如int类型的静态变量static int x = 3,连接阶段赋零值即为0,而初始化阶段赋值即为3。
以上就是类加载机制的三大阶段,而我们今天要将的类加载器存在于阶段一中--加载。可以说,没有类加载器也就没有了后续的流程,类加载器在Java虚拟机中起到了至关重要的作用。
类加载器
类加载器(class loader)将Java类从本地磁盘加载到Java虚拟机中,并同时创建了该类的Class对象,实现了“通过一个类的全限定类名来获取此类的二进制字节流”功能。
类加载器是Java语言的一项创新,也是Java语言流程的重要原因之一,在类层次划分、OSGI、热部署、代码加密等领域有着重要的作用,成为Java不可或缺的一部分。
首先,我们来写一个测试类,来看下类加载器,ClassLoaderTest测试类:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while (true) {
System.out.println(loader);
if(loader==null){
break;
}
loader = loader.getParent();
}
}
}
运行结果:
sun.misc.Launcher$AppClassLoader@41dee0d7
sun.misc.Launcher$ExtClassLoader@f7b650a
null
首先获取到的是AppClassLoader类加载器,紧接着又获取的是ExtClassLoader类加载器,最后获取的对象为null
为什么为null呢,后续来解答!
接下来,我们来看看在Java体系中到底有哪些类加载器。
类加载的分类
在Java中,类加载器可以分为两大类,一类是由Java系统提供的,另外一类是自定义的,由开发人员编写提供的。
系统类加载器:
引导类加载器(bootstrap class loader):用来加载Java的核心库,由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作,不继承自java.lang.ClassLoader(这就是上面例子中为什么最后取到的对象为null的原因)。负责加载
扩展类加载器(extensions class loader):用来加载Java的扩展库,由sun.misc.Launcher$ExtClassLoader来实现。负责加载
系统类加载器(system class loader):用来加载Java应用的类路径(CLASSPATH)的Java类,由sun.misc.Launcher$AppClassLoader来实现。一般来说,Java应用中的类都是由它来完成加载的,可以通过ClassLoader.getSystemClassLoader()来获取。
自定义类加载器:
自定义类加载器(User Custom ClassLoader):开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。在程序运行期间, 通过自定义的java.lang.ClassLoader子类动态加载class文件。
java.lang.ClassLoader类介绍
方法 | 说明 |
---|---|
getParent() | 返回该类加载器的父类加载器。 |
loadClass(String name) | 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findClass(String name) | 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findLoadedClass(String name) | 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
以上为ClassLoader对于类加载功能的主要方法介绍。
在我们的应用程序中,都是由这4种类加载器互相配合进行加载,这4种类加载器在虚拟机中维护了一种父子关系,这种关系叫做“双亲委派模型”。下面,我们就来看看什么是双亲委派模型。
双亲委派模型
下面的图片中,展示的就是“双亲委派模型”,模型中呈现出Java体系架构中的四大类加载器的关系,除了顶层的引导类加载器之外,其余类加载都需要有父加载器存在,但是此子父类关系并不是通过java代码中继承的方式实现。具体如何实现,后面讲解。
知道了类加载器的结构模型,那么该模型在代码整个Java体系中如何工作呢?
工作流程:一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个类加载请求委派给其父类加载器去完成,每一个层的类加载器都是如此,依次向父类加载器传递,最终所有的类加载请求都会传送到顶层的启动类加载器(bootstrap)中,只有当父加载器反馈无法完成这个类加载请求时,子类加载器才会尝试自己去进行类加载操作,如果子类加载器也依旧无法完成,则代码层面就会抛出异常。
此时,你会不会感到疑惑?为啥儿子自己的活不去干,而首先交给他爹去完成呢?这么做的目的何在?
在Java体系中,双亲委派模型保证了类的唯一性,将Java类与它的类加载器绑定到了一起,当父类加载器加载完成后,子类加载器不会再次加载。此外,双亲委派模型还保证了Java框架的安全性。例如:java.lang.Object类,无论是上述哪个类加载器要加载这个类,最终都会委派给模型中的启动类加载器去加载,因此java.lang.Object类在程序中保证了唯一性。
相反,如果没有使用该模型,而是由各个类加载器自行去加载的话,那么系统中就会出现不同的java.lang.Object类,类的唯一性被打破,Java体系中的基本行为就得不到保证。例如:,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。涉及到“类相等”的方法有:Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法以及instanceof对象所属关系判定。
试想一下,如果我们自定义一个java.lang.Object类会怎么样?(其实我们自定义的java.lang.Object类无法在程序中被导入,只能模拟定义java.lang.Object类--java.lang.ObjectTest)
当JVM请求类加载进行自定义的类加载时,双亲委派模型会将请求传递到启动类加载器中,但是启动类加载器默认只加载
为什么,why?
这是因为以java.开头的是核心API包,需要访问权限,强制加载会抛出异常,任何以java.开头的包都会报错:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
此异常是代码层面抛出的,并不是native方法虚拟机底层抛出,源码可见(ClassLoader类):
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
此时,你会不会又突发奇想,我自己定义一个类,放在
编写代码,并打成jar包,jar包的名称就叫做 jiaboyan.jar:
jar cvf jiaboyan.jar com\jiaboyan\test\ObjectTest.class
接下来,把jiaboyan.jar包放入到
执行main()方法,结果如下:
sun.misc.Launcher$AppClassLoader@8fd9b4d
从输出可以看出,放置到
why?不是说了委派给最顶层的类加载进行加载吗?其实,这是由于虚拟机出于安全角度考虑,不会加载
下一篇,将会对类加载器源码进行分析!!!
END
✎推荐阅读
【推荐书籍】
扫码购买
以上是关于解密Java虚拟机如何加载一个Class文件的主要内容,如果未能解决你的问题,请参考以下文章
《深入理解Java虚拟机》-----第7章 虚拟机类加载机制——Java高级开发必须懂的