虚拟机类加载机制

Posted 鱼翔空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟机类加载机制相关的知识,希望对你有一定的参考价值。

java内存区域与内存溢出

垃圾收集器与内存分配策略

7.1 概述

java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转化解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被称作虚拟机的类加载机制。

java天生可以动态扩展的语言特性依赖于运行期动态加载和动态链接的特点。

7.2 类加载的时机

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期将会经历:

加载(Loading)->验证(Verification)->准备(Preparation)->解析(Resolution)-> 初始化(Initialization)->使用(Using)->卸载(Unloading)

图片

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的

解析某些时候会在类初始化以后开始,如java运行时的动态绑定

java有且只有六种情况必须立即对类进行初始化

一、遇到new、getstatic、putstatic或invokestatic 这四条指令时,生成这四条指令的场景:

  • 使用new 关键字实例化对象的时候

  • 读取或设置一个类型的静态字段(非final修饰,非编译期把结果放入常量池的静态字段除外)

  • 调用一个类型的静态方法的时候

二、使用java.lang.reflect包的方法进行反射调用的时候;

三、初始化时发现父类还没有初始化,先触发父类的初始化;

四、当虚拟机启动时,需要指定一个执行的主类;

五、当使用jdk7新加入的动态语言支持时;

六、当一个接口定了jdk8新加入的默认方法时;

7.3 类加载的过程

7.3.1 加载

需要完成三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;

  2. 将获取到的字节流所代表的静态存储结构转化为方法区的运行时数据结构;

  3. 在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

其中二进制字节流的获取方式多种多样,包含

  • 从java、war等zip包读取;

  • 从网络中获取

  • 动态代理(反射)

  • 从其他文件生产

  • 从数据库,加密文件等等

开发人员可以在此阶段通过定义自己的类加载器去控制字节流的获取方式;

加载阶段与链接阶段的部分动作是交叉进行的。比如加载的时候会去验证一些基本信息,java文件是否符合当前虚拟机等;

7.3.2 验证

验证的目的是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部要求

验证阶段会完成四个阶段的校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证

  1. 文件格式验证,主要验证字节流是否符合Class文件格式的规范(如文件头,版本号,常量类型等),在加载过程中完成验证,如果验证失败,无法加载,也无法进入方法区;

  2. 元数据验证,主要对字节码描述的信息进行语义分析(如,是否有父类,父类是否允许继承,是否需要实现父类的方法等),在方法区存储的结构上进行

  3. 字节码验证;主要是通过数据流分析和控制流分析,确定程序的语义是合法、符合逻辑的。

  4. 符号引用验证,这个阶段在解析阶段执行,用来验证类对自身以外的各类信息进行匹配性校验,模块化的权限等

7.3.3 准备

正式为类中定义的变量(静态变量)分配内存并设置初始值(分配了空间,并赋值了默认值)。

仅包含类变量,不包含实例变量,实例变量会随对象实例化时一起分配到堆中;

静态变量会在此阶段初始为零值(被final修饰的静态变量在此会赋真正的值)

图片

7.3.4解析

java虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:以一组符号来描述引用的目标

直接引用:是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄;

解析包括

1,类或接口的解析

2,字段解析

3,方法解析

4,接口方法解析

7.3.5 初始化

是类加载过程中的最后一个步骤,直到初始化阶段,虚拟机才真正开始执行类中编写的java代码,将主导权交给应用程序;

初始阶段就是执行类构造器() 方法的过程;

()是由编译器自动收集类中的所有类变量的赋值动作静态语句块中的语句合并产生的

子类的()方法执行前,父类的()方法已经执行完毕

以下操作不会执行类初始化

  • 通过子类引用父类的静态字段(只会触发父类的初始化);

  • 定义对象数组

  • 常量在编译期间会存入调用类的常量池中,没有触发;

  • 通过类名获取class对象,不会触发类的初始化;

  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会

  • 通过classloader默认的loadclass方法,也不会触发初始化;

7.4 类加载器

7.4.1 类与类加载器

对任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

7.4.2双亲委派模型

对虚拟机来说,只存在两种类加载器

一类是:启动类加载器,是虚拟机自身的一部分

一类是:其他所有的类加载器,都是由java实现

java程序会用到三个类加载器

  • 启动类加载器(Bootstrap Class Loader):负责加载 JAVA_HOME\\lib目录,或者被-Xbootclasspath 参数指定的路径;

  • 扩展类加载器(Extension Class Loader): 负责加载JAVA_HOME\\lib\\ext 目录,或者被java.ext.dirs 系统变量指定的路径;在类sun.misc.Launcher$ExtClassLoader

  • 应用程序类加载器(Application Class Loader):负责加载用户类路径(ClassPath)上所有的类库,由sun.misc.Launcher$AppClassLoader 来实现

双亲委派模型:

图片

双亲委派工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有父类都无法加载时,才会尝试自己去完成。

7.4.3 破坏双亲委派模型

到java模块化出现为止,一共经历了3次较大规模破坏双亲委派模型

  • 双亲委派出现在jdk1.2,之前已实现的类加载,在双亲委派出现时进行了兼容处理

  • 由模型自身缺陷导致,jdni由启动类加载器加载,却要调用厂商的JNDI服务来实现,为了解决这个问题,出现了线程上下文类加载器;

  • 用户对程序动态性的追求导致,代码热替换、模块热部署等,如OSGI

OSGI实现模块化热部署的关键,每一个程序模块都有一个自己的类加载器,在OSGI环境下,类加载器是网状结构;

7.5 java模块化系统

JDK9中引入java模块化系统

模块化的目标:可配置的封装隔离机制;

如果启用了模块化进行封装,模块就可以声明对其他模块的显式依赖,这样虚拟机就能够在启动时验证应用程序开发阶段设定好的依赖关系是否完备;

7.5.1 模块的兼容性

7.5.2 模块化下的类加载器

JDK9以后为了模块化系统的顺利进行,模块化下类加载器发生了一些变动:

  • 扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代

  • 平台类加载器和应用程序类加载器都不再派生自java.net.URLClassLoader,如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能在JDK9中崩溃

在平台类及应用程序类加载器收到类加载器请求,在委派给父加载器前,要先判断该类是否能够归属到某一个系统模块中,如果可以,优先由对应的系统模块加载。

图片

如果觉得对你有帮助,请关注公众号:5ycode,后续会不断更新哦

公众号图片

以上是关于虚拟机类加载机制的主要内容,如果未能解决你的问题,请参考以下文章

虚拟机类加载机制

Java虚拟机类加载机制——案例分析

虚拟机类加载机制

Java虚拟机类加载机制

Java虚拟机--虚拟机类加载机制

虚拟机类加载机制概述