[Java 执行那些事] —— 类加载机制( 上)

这里写图片描述

代码编译的结果从本地机器码转换为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。——周志明《深入理解Java虚拟机》

Introduction to Class Loading

类加载(Class Loading)是一种机制,他描述的是将字节码以文件形式加载到内存再经过连接、初始化后,最终形成可以被虚拟机直接使用的Java类型地过程。

JVM采用这种在运行期才去加载、连接、初始化的策略会稍微增加一些的性能开销,导致例如程序启动慢的这样的缺点。但是它却可以为程序提供高度的灵活性,这是因为JVM的字节码执行引擎不需要提前了解关于文件和文件系统的任何信息,我们完全可以等到运行期才指定实际的实现方法,让一个本地程序通过网络加载任何地方的字节码文件。Java的动态拓展性正是赖于JVM类加载机制实现的。

What is it

Class Loading 包含了加载(Loading)、连接(Linking)、初始化(Initialization)三大部分,其中Linking又包含了三个部分:校验(Verification)、准备(Preparation)、解析(Resolution)。而一个类的生命周期只是在Class Loader的基础上多了:使用(Using),卸载(Unloading)两部分。

Class Loaders的组成:

这里写图片描述

类的生命周期
这里写图片描述

加载(Loading)

Loading是Class Loading的第一步,他的工作是负责将字节码(bytecode)加载到JVM内存中,这个内存空间就是我们常说的方法区。在JVM规范中,Loading需要完成以下三点:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

JVM中规定了这三条动作,并且不算具体,也就是说只要在满足这三条,我们可以任意拓展。例如JVM只规定了“通过一个类的全限定名来获取定义此类的二进制字节流”,但是并没有说从哪里加载,我们可以通过.class文件中加载,也可以通过网络加载任何地方的字节码。而这一阶段也是开发人员能够控制最强的地方。

这里要特殊说明的是,JVM在加载数组的时候加载的仅仅是数组的类型类(例如String[] 加载器只会加载String这个类型类),而数组的创建则由JVM直接完成。

这里我们多问几个为什么:

1. JVM为什么只加载数组的类型类
我认为JVM这样做的目的主要是为了节省时间,我们知道数组里面装的都是同一种类型的元素,JVM没必要将一个重复的内容加载多次浪费时间。
2. N维数组怎么加载
如果是N维数组,类加载器会从最外层开始一层一层的递归加载,直到加载到非数组类型为止。
3. 引用类型与基本类型加载起来会不会有区别
其实基本类型早已经在javac阶段装箱成封装对象了,例如int会被装箱成Integer,long装箱成Long等等,所以是没有区别的。

1.2 类加载器(Class Loaders)

为了完成加载过程中的第一条:"通过一个类的全限定名来获取定义此类的二进制字节流"的功能,JVM团队开发了一个模块——类加载器。但是为了给用户提供更好的拓展性JVM团队将这个过程的代码放到了JVM的外部,以便让开发人员可以自定义类加载器。

虽然类加器只是用于实现类加载的动作,但是他的作用远远不限于类加载。对于任意一个类,他的唯一决定方式是:类本身+加载此类的类加载器,这个可以类比为C++中的命名空间,每一个类加载器都有自己独立的命名空间,通俗的讲:一个类java.lang.Object 如果被两个类加载器加载,那么这个两个类就是不相同的。

JVM为什么要如此设计呢?

答案是为了拓展性。这种情况出现在类本身的限制名(包名+类名)无法唯一区分类的时候。例如在不更改包名的情况,如何让不同版本的kafka在同一个JVM下运行呢?为了解决类似的问题类加载器引入的命名空间的概念,提高了拓展性。

加载器的类型

为此JVM提供了多种类加载器,当然用户也可以自行拓展。

  • 从Java虚拟机的角度看,只有两种不同的类加载器:
  1. 启动类加载器(Bootstrap ClassLoader):用C++实现,是虚拟机自身的一部分;
  2. 所有其他的类加载器:用Java语言实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader;
  • 从Java开发人员看,类加载器可分为3种
  1. 启动类加载器(Bootstrap ClassLoader):负责加载<\JAVA——HOME>\lib目录中的并且可以被虚拟机识别的;
  2. 扩展类加载器(Extension ClassLoader):负责加载<\JAVA_HOME>\lib\ext目录中的所有类库,开发者可以直接使用扩展类加载器;
  3. 应用程序类加载器(Application ClassLoader):它是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称它为系统类加载器。他负责加载用户类路径(ClassPath)上所指定的类库

这里写图片描述

Object类重复多次怎么办?

但是这又引入了一个问题:如果每个类加载拥有自己的命名空间,而且是随机的加载类,那么如果用户自己编写了一个java.lang.Object类,并把它放到了ClassPath中,岂不是会出现很多个Object类!这样Java类型体系中最最基础的行为都无法保证,应用程序也将一片混乱。为此JVM团队提出了双亲委派模型(Parent Delegation Model)

双亲委派模型

双亲委派模型的英文名字叫:Parent Delegation Model,当我第一次听到“双亲”这个词的时候很困惑,有歧义感,所以还是建议大家去多看看英文原版的内容会有一种豁然开朗的感觉。

如图所示,所谓的双亲委派模型指除了启动类加载器以外,其余的加载器都有自己的父类加载器,而在工作的时候,如果一个类加载器收到加载请求,他不会马上加载类,而是将这个请求向上传递给他的父加载器,看父加载器能不能加载这个类,加载的原则就是优先父加载器加载,若果父加载器加载不了,自己才能加载。

综上就是双亲委派模型的原理,是不是很简单!

因为有了双亲委派模型的存在,类似Object类重复多次的问题就不会存在了,因为经过层层传递,加载请求最终都会被Bootstrap ClassLoader所响应。加载的Object对象也会只有一个。

并且面对同一JVM进程多版本共存的问题,只要自定义一个不向上传递加载请求的加载器就好啦。

这里写图片描述

Summary

今天我主要讲述Java类加载机制的第一步:加载(Loading),通过这一章节的学习我们知道了类加载其实是一个包含:加载、连接(校验、准备、解释)、初始化的机制。正是因为Java在运行时才进行类加载,从而为Java提供了更高的动态拓展性。

而在Loading过程中JVM规范并没有明确表示要从什么地方加载字节码,所以用户可以通过自定义类加器的方式加载任何地方的字节码。

为了支持类多版本共存,JVM提供了加载器带有命名空间的功能,可以在不修包名的情况下实现多版本共存。

而加载器带有命名空间后又带来了Object类可能重复的问题,为此引入双亲委派模型:子加载器收到加载请求后需要向上传递,优先父加载器加载。

参考:《深入了解Java虚拟机 周志明》

文章的最后向您推荐两个关于Java的专栏。专栏的内容有音频有文稿, 无论是在路上还是业余时间的学习都很有裨益。

这里写图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章