文章目录
前言
在Java的世界里,一个很重要的概念就是双亲委派,这也是面试题高曝光的一个知识点,要了解什么是双亲委派,需要从JVM的类加载机制说起
加载顺序
类从被加载到虚拟机内存中开始,到卸载出内存为止,声明周期包括以上过程,其中验证、准备以及解析3个部分可以统称为连接,下面咱们分阶段看看每个步骤到底做了什么
加载
虚拟机在此阶段需要完成以下三件事情:
1)通过一个类的全限定名
来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各种数据访问入口
其中,第一点通过类的全限定名来获取,并没有指明要从哪里获取、怎样获取,所以实现细节是非常灵活的
同时也促进了很多技术的发展,比如
- 从Zip包中读取,日后发展为JAR包、WAR格式等
- 从网络中获取,典型应用就是Applet
- 运行时计算生成,比如动态代理技术,在java.lang.reflect.Proxy中
- 其他文件,如JSP
验证
验证是连接阶段的第一步,主要的目的就是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
相对C/C++来说,Java算是相对安全的语言,纯粹的Java代码对自由操作内存是不现实的,间接提高了安全性。
验证阶段非常重要,这个阶段是否严谨,决定了Java虚拟机的健壮性,大致会完成以下4个阶段的校验:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
这个阶段是正式为类变量
分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间
因为是类变量,所以并不包括实例变量
不过类变量在此阶段的初始值也是数据类型的零值
public static int val = 100;
实际上变量val的初始值是0,不是100,将val赋值为100的put static指令是程序被编译后,存放于类构造器的方法之中。
如果是以下代码:
public static final int val = 100;
编译阶段会为val生成ConstantValue属性,则在准备阶段虚拟机会根据这个属性将val赋值为100
解析
解析阶段就是指虚拟机将常量池中的符号引用替换为直接引用的过程
符号引用
:以一组符号来描述所引用的目标,可以是任何形式的字面量,各虚拟机实 现的内存布局不同,但是他们能接受的符号引用必须是一致的直接引用
:可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄
初始化
这一步也是类加载过程的最后一步了,前边的过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制;这一阶段,才真正开始执行类中定义的Java程序代码
类与类加载器
简单了解了Java虚拟机加载的全过程,我们再来重点看一下第一个阶段:加载
虚拟机设计团队把加载这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所有需要的类,实现这个动作的代码模块就成为:类加载器
主角登场——双亲委派模型
从Java虚拟机的角度来讲,只有两种不同的类加载器
- 启动类加载器(Bootstrap ClassLoader),不同的虚拟机不同,C++/Java
- 所有其他的类加载器,由Java语言实现,独立于虚拟机外部,且全部继承自抽象类java.lang.ClassLoader
JVM提供了以下三类加载器:
- 启动类加载器:负责加载
JAVA_HOME\lib
目录,或者通过-Xbootclasspath参数指定,且被虚拟机认可的类 - 扩展类加载器:负责加载
JAVA_HOME\lib\ext
目录,通过java.ext.dirs系统变量指定路径中的类库 - 应用程序类加载器:负责加载用户路径(classpath)上的类库,也可以通过继承java.lang.ClassLoader实现自定义的类加载器
经典图:
如图描述类加载之间的这种层次关系,就成为类加载器的双亲委派模型(Parents Delegation Model)
所谓的双亲,我认为也就是对Parents的直译。众所周知,软件的世界貌似只有父亲(姑且这么说,便于理解)
双亲委派模型要求除了顶层的启动类加载器外,其余的都应当有自己的父类加载器
不过这里的加载器相互之间不是通过继承的关系来实现,而是组合关系来复用上层加载器的代码
所以,这种模型最大的作用就是最大限度的保证对于同一个类的加载,是高度统一且公认的
比如当你需要加载java.lang.Object,它存放在rt.jar中,加载的时候自然会一步步的委托给最顶层的启动类加载器,则java.lang.Object在各个加载器环境中都是同一个类;反之则会混乱不堪,你实现一个,他也实现一个,就胡闹了
题外话
这里插一句题外话,我们知道双亲委派模型并不是一个强制性的约束模型,它只是一种建议
Java里有几种方式是例外的,比如:JDK 1.0时代重新loadClass()函数、JDNI、OSGi等
很赞同这句话,但是这里我想着重说一下这个JNDI
大家也知道去年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞;
今年3月,外网爆出目前主流的 Java EE轻量级开源框架Spring含JNDI注入漏洞,该CVE编号为0day,漏洞评级为高危。
根源在于这个JNDI,我想说的是技术的创新肯定是必要的,只不过在实现过程中容易变成双刃剑
各位朋友无论是自研,抑或是在引入第三方框架的情况下,对自己的实现逻辑有基本的了解是非常必要的,否则容易引发你意想不到的结果
参考资料
《深入理解Java虚拟机》 —— 周志明 机械工业出版社