欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【JAVA核心知识】5: JVM的类加载

程序员文章站 2022-07-10 17:45:21
类加载过程想要使用一个类,首先需要将其加载到JVM中,类加载到JVM需要经过三个步骤:加载->链接->初始化。其中链接又分为验证,准备,解析三步。加载类加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各个数据的入口。注意相关信息不是一定要从Class文件中获取,它即可与从ZIP中读取(如从Jar包,War包中读取),也可以在运算时生成(动态代理),也可以由其它文件生成(如将JSP文件转换成对应的Class类)。链接链接过程分为验证,准备,解...

1 类加载过程

想要使用一个类,首先需要将其加载到JVM中,类加载到JVM需要经过三个步骤:加载->链接->初始化。其中链接又分为验证,准备,解析三步。
【JAVA核心知识】5: JVM的类加载

1.1 加载

类加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各个数据的入口。注意相关信息不是一定要从Class文件中获取,它即可与从ZIP中读取(如从Jar包,War包中读取),也可以在运算时生成(动态代理),也可以由其它文件生成(如将JSP文件转换成对应的Class类)。

1.2 链接

链接过程分为验证,准备,解析三步。

1.2.1 验证

JAVA是一种相对安全的语言,验证的意义是为了确保Class文件的字节流中包含的信息符合当前虚拟机要求。不会危害到虚拟机的安全。验证主要包含文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 文件格式验证:验证字节流是否符合Class文件格式规范,并且能被当前JVM加载处理。如常量类型是否支持。
  • 元数据验证:字节码信息进行语言分析,分析是否符合java语言规范。
  • 字节码验证:最重要的验证环节,元数据验证后对方法体验证,保证类方法在运行时不会又危害发生。
  • 符号引用验证:验证符号引用,保证能够访问到,不会出现无法访问的情况。

1.2.2 准备

准备阶段正式对类变量分配内存,并设置初始默认值。如定义 private static int s = 2020;经过此阶段s=0,而不是2020.但是如果定义为private final static int s = 2020,在准备阶段会直接将s设置成2020而不是初始值0;

1.2.3 解析

解析阶段JVM将常量池中的符号引用替换为直接引用。准备阶段只是分配了内存,但是类变量并没有指向那一块内存,这一步就是完成实际指向的工作。

1.3 初始化

初始化阶段为类变量设置正确的初始值。如上文 private static int s = 2020中将s赋值2020的动作遍在这一步完成。同时初始化阶段也会执行静态代码块。如果有超类,则先对超类执行初始化。
初始化阶段是执行类构造器<client>方法的过程,<client>方法是编译器自动收集类中的类变量赋值操作和静态语句块中的语句合并而成的。JVM会保证父类的<client>方法会在子类的<client>方法执行之前执行完毕。如果这个类没有静态变量赋值和静态代码块,那么编译器可以不为这个类生成<client>方法。
以下几种情况不会触发初始化:

  • 子类引用父类的静态变量,只会触发父类的初始化,不会触发子类的初始化
  • 定义对象数组,不会触发初始化
  • 常量在编译过程中直接存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类的初始化。如A引用B类的常量final static x = 3。此时不会对B进行初始化。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果initalize为false,不会触发初始化。
  • 通过ClassLoader默认的loadClss方法,也不会触发初始化。

2 类加载器

类加载的动作由类加载器完成。对于JVM来说,类的唯一性是通过类的全限定名+类加载器来区分的。不同的类加载器加载的同一个类并不被认为是同一类。如有一个类C,分别用类加载器CL1,CL2加载。一个参数需要CL1的C实例,传入的确实CL2的C实例,就会报错java.lang.ClassCastException:C cannot be cast to C。
JVM提供了三种类加载器:启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader).

  • 启动类加载器:用来加载Java的核心库,主要加载的是JVM自身所需要的类,使用C++实现,并非继承于java.lang.ClassLoader,是JVM的一部分。负责加载JAVA_HOME\lib目录中的,或者-Xbootclasspath参数指定的路径中的,且被虚拟机认可[注1]的类。开发者无法直接获取到其引用。
    注1:JVM是按文件名识别的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包放在lib目录下也没有作用,同时启动加载器只加载包名为java,javax,sun等开头的类。且java是特殊包名,开发者的包名不能以java开头,如果自定义了一个java.***包来让类加载器加载,那么就会抛出异常java.lang.SecurityException: Prohibited package name: java.***
  • 扩展类加载器: 用来加载Java的扩展库。负责加载JAVA_HOME\lib\ext目录中的,或通过系统变量java.ext.dirs指定路径中的类库。由java语言实现。开发者可以直接使用。
  • 应用程序类加载器:负责加载用户路径(classpath)上的类库。开发者可以直接使用。可以通过ClassLoader.getSystemClassLoader()获得。一般情况下程序的默认类加载器就是该加载器。

除了提供的加载器外,开发者可以通过继承ClassLoader类的方式实现自己的类加载器。
【JAVA核心知识】5: JVM的类加载

3 双亲委派机制

JVM的类加载机制为双亲委派机制,除了顶层的启动类加载器,双亲委派机制要求每一个类加载器都要有自己的父加载器。这个父加载器并不是指继承,而是一种委派关系。双亲委派机制下一个类加载器收到类加载请求不会直接自己去加载,而是先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,则继续委托,一直委托到最顶层的启动类加载器。父加载器可以加载目标类的话就由父加载器完成加载任务,如果父加载器无法完成则子加载器自己尝试加载。这就是双亲委派机制。
双亲委派机制的优势:双亲委派机制使得java类随着他的类加载器具备了一种带有带有优先级的层级关系。通过这种层级关系可以避免类的的重复加载,当父加载器已经加载过目标类时,子加载器无需重复加载一次。
其次是安全方面,通过双亲委派机制可以避免核心类不会被随意替换,例如网络传递一个名为java.lang.Integer的类,在双亲委派机制下,加载请求会被传递到顶层的启动类加载器,启动类加载器发现这个名字的类已经加载过了,就会直接返回已经加载过的Integer.class。这样就可以防止核心API被修改。

4 OSGI

OSGI(Open Service Gateway Initiative)是面向java的动态模型系统。OSGI能够提供无需重启的动态改造功能,基于OSGI的程序很可能可以实现模块级的热插拔功能,当程序进行升级时,只需停用,重新安装然后启动程序的一部分。但并非所有的程序都适合OSGI架构,其在提供强大功能的同时也提高了复杂度,因为OSGI不支持双亲委派机制。
eclipse就是基于OSGI技术来构建的。
OSGI每个模块都自己的类,每个模块可以声明其需要模块的类(导入),也可以声明自己的类以供其它模块使用(导出),每个模块都有自己的类加载器,他负责加载本模块的类,对于非本模块的类:核心库的类会代理给父类加载器(通常是启动类加载器),其他模块导入的类则代理给对应模块由其加载。对于java开头的类,默认都是由父类加载器完成的,可以通过org.osgi.framework.bootdelegation设置某些包或者类必须由父类加载器加载。如设置org.osgi.framework.bootdelegation = com.my.* 此时com.my下的所有类都由父类加载器进行加载
例如由两个模块:M-A和M-B,分别有类C-A和C-B,C-A继承自C-B,M-A启动时,对C-A进行加载,因为继承关系继而需要加载C-B,此时由于M-A声明了C-B是由M-B导入的,那么就会将C-B的加载交由M-B执行,M-B对C-B进行加载,所得的类实例可以被所有声明导入此类的模块使用。

参考资料:
jvm之java类加载机制和类加载器(ClassLoader)的详解
深入探讨 Java 类加载器

本文地址:https://blog.csdn.net/yue_hu/article/details/109622483