Java虚拟机--类加载器如何加载一个Class文件

如何加载一个Class文件

在之前的文章中,笔者介绍了Java虚拟机--类加载机制,阐述了一个类加载到底做了哪些事情!

但是,关于类加载器的只是,并没有做任何介绍,只是说了下会在后面的文章中进行单独阐述。那么,本篇的意义就是来告诉大家类加载器的实现。

首先,我们来简单的回顾下类加载机制中的内容。

类加载机制

虚拟机把类的数据从.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的原因)。负责加载<Java_Runtime_Home>/lib下面的类库到内存中,或-Xbootclasspath选项指定的jar包装入内存;

扩展类加载器(extensions class loader):用来加载Java的扩展库,由sun.misc.Launcher$ExtClassLoader来实现。负责加载 <Java_Runtime_Home>/lib/ext,或-Djava.ext.dirs选项指定目录下的jar包装入到内存中;

系统类加载器(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代码中继承的方式实现。具体如何实现,后面讲解。

1526024942(1).png

知道了类加载器的结构模型,那么该模型在代码整个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请求类加载进行自定义的类加载时,双亲委派模型会将请求传递到启动类加载器中,但是启动类加载器默认只加载<Java_Runtime_Home>/lib路径下的类,在该路径下并没有ObjectTest类,所以启动类加载器无法加载,只能向下传递给子类加载器,最终会将请求传递到系统类加载器中,但是系统类加载器也无法进行加载,会抛出异常。

为什么,why?

这是因为以java.开头的是核心API包,需要访问权限,强制加载会抛出异常,任何以java.开头的包都会报错:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

image

此异常是代码层面抛出的,并不是native方法虚拟机底层抛出,源码可见(ClassLoader类):

if ((name != null) && name.startsWith("java.")) {
        throw new SecurityException
            ("Prohibited package name: " +
             name.substring(0, name.lastIndexOf('.')));
}

此时,你会不会又突发奇想,我自己定义一个类,放在<Java_Runtime_Home>/lib路径下会如何?

image

编写代码,并打成jar包,jar包的名称就叫做 jiaboyan.jar:

jar cvf jiaboyan.jar com\jiaboyan\test\ObjectTest.class

接下来,把jiaboyan.jar包放入到<Java_Runtime_Home>/lib下,也同时放入到<Java_Runtime_Home>/lib/ext,并同时保留截图中的代码,如图所示:

image

执行main()方法,结果如下:

sun.misc.Launcher$AppClassLoader@8fd9b4d

从输出可以看出,放置到<Java_Runtime_Home>/lib目录下的ObjectTest.class类并没有被启动类加载器加载,而是由扩展类加载器加载了。

why?不是说了委派给最顶层的类加载进行加载吗?其实,这是由于虚拟机出于安全角度考虑,不会加载<Java_Runtime_Home>/lib中的陌生类,开发者把自定义的类放置到此目录下启动类加载器是不会进行加载的。

下一篇,将会对类加载器源码进行分析!!!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java工程师日常干货

【随笔】JVM核心:JVM运行和类加载

本篇博客将写一点关于JVM的东西,涉及JVM运行时数据区、类加载的过程、类加载器、ClassLoader、双亲委派机制、自定义类加载器等,这些都是博主自己的一点...

613
来自专栏数据结构笔记

python爬虫系列之数据的存储(一):json库的使用

在上一篇文章里我们讲了 xpath写法的问题还以爬取我的文章信息写了示例,但是在上一篇中我们只是爬取并打印了信息,并没有对信息进行保存。

952
来自专栏大数据文摘

Python程序员最常犯的10个错误,你中招了吗?

1451
来自专栏张俊红

零基础学习爬虫并实战

总第63篇 本篇主要从爬虫是什么、爬虫的一般流程、爬虫各个流程的实现方法、爬虫实例四个方面分享零基础了解爬虫,并进行简单的实战。 在阅读下面之前,我们...

2.4K10
来自专栏www.96php.cn

PHP关键字、PHP 语言结构(Language constructs)和函数的区别

1、 什么是语言结构和函数 语言结构: 就是PHP语言的关键词,语言语法的一部分; 它不可以被用户定义或者添加到语言扩展或者库中; ...

3489
来自专栏PhpZendo

带你入门 JavaScript ES6 (二)

上一篇学习下一代 JavaScript 语法: ES6 (一),我们学习了关于块作用域变量或常量声明 let 和 const 语法、新的字符串拼接语法模版字面量...

431
来自专栏深度学习自然语言处理

【干货】python正则表达式应用笔记

正则表达式 (Regular Expression) 又称 RegEx, 是用来匹配字符的一种工具. 在一大串字符中寻找你需要的内容. 它常被用在很多方...

3058
来自专栏赵俊的Java专栏

爬楼梯

1952
来自专栏开源优测

JMeter函数和变量11

前言 在jmeter中提供了功能强大的内置函数来帮助我们处理字符串、文件读写、计算、运行外部脚本等等能力。 要想在项目中切实运用来jmeter完成复杂的压测场景...

3506
来自专栏GreenLeaves

Flyweight享元模式(结构型模式)

虽然OOP能很好的解决系统抽象的问题,并且在大多数的情况下,也不会损失系统的性能。但是在某些特殊的业务下,由于对象的数量太多,采用面向对象会给系统带来难以承受的...

712

扫码关注云+社区