当前位置:   article > 正文

Java虚拟机类加载机制--类加载的过程详解_类加载是java虚拟机将类的字节码数据从磁盘或网络中读入内存,并转换成在jvm中可以

类加载是java虚拟机将类的字节码数据从磁盘或网络中读入内存,并转换成在jvm中可以

一个Java文件从编码完成到最终执行,一般主要包括两个过程

  • 编译:即通过javac命令将java文件编译成字节码,也就是我们常说的.class文件。
  • 运行:则是把编译声称的.class文件交给Java虚拟机(JVM)执行。

我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

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

在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销,但是却为Java应用提供了极高的扩展性和灵活性,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

例如,编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过Java预置的或自定义类加载器,让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。这种动态组装应用的方式目前已广泛应用于Java程序之中,从最基础的Applet、JSP到相对复杂的OSGi技术,都依赖着Java语言运行期类加载类才得以诞生。


1.类加载的时机

一个Java类从被加载到虚拟机内存中开始,到卸载出内存为止。

它的整个生命周期将会经历一下七个阶段:

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

其中验证、准备、解析三个部分统称为连接(Linking)

这七个阶段也就是类的生命周期,发生顺序如下图所示。

类的生命周期

2.类加载的过程

什么是类的加载过程?

一个java类文件被Java虚拟机从编译到执行,包括两个过程:编译、运行。

编译,即我们编译好的java类文件,通过javac命令编译成.class的字节码文件

运行,即将编译好的字节码文件交由Java虚拟机JVM来执行。

由此,类加载过程指Java虚拟机把.class的字节码文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

 即,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。

由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三步来实现对这个类进行初始化。


2.1 加载阶段

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆内存空间创建一个该类的java.lang.Class对象,用来封装类在方法区类的对象。

在加载阶段,Java虚拟机要完成以下3件事情:

【1】通过一个类的全限定名来获取其定义的二进制字节流。

         二进制文件字节流必须从某个class文件中获取。
【2】将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

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

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

类的加载的最终产品是位于堆区中的Class对象。

Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

补充,加载.class字节码文件的方式有以下几种:

【1】从本地系统直接加载

【2】通过网络下载.class文件,典型场景:Web Applet

【3】从zip,jar等归档文件中加载.class文件

【4】运行时计算生成,使用较多:动态代理技术

【5】由其他文件生成,典型场景:JSP应用

【5】从专有数据库中提取.class文件

【6】将Java源文件动态编译为.class文件(服务器)

【7】从加密文件中获取,典型的防class文件被反编译的保护措施

加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性。

这里涉及到类加载器、双亲委派模型是非常重要的知识点,这部分内容会在后面的文章中单独介绍到。

加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。


===============================>  连接阶段  <================================

在连接阶段,可分为三个小的阶段:验证、准备、解析。

  • 验证 是否有正确的内部结构,并和其他类协调一致
  • 准备 负责为类的静态成员分配内存,并设置默认初始化值
  • 解析 将类的二进制数据中的符号引用替换为直接引用

2.2 验证阶段

目的:

确保.class文件的字节流中包含信息符合当前虚拟机的全部约束要求,保证被加载类的正确性,不会危害虚拟机自身安全。

Java虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟机保护自身的一项必要措施。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

包括四个阶段的检验动作:

  • 文件格式验证

验证字节流是否符合.class文件格式规范,并且能被当前版本的虚拟机处理。

该验证阶段的主要目的是保证输入的字节流能够正确地解析并存储于方法区之内,格式上符合一个描述Java类型信息的要求。

该阶段的验证是基于二进制字节流进行的,只有通过这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储。

后边三个阶段的验证全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流。

  • 元数据验证

对字节码描述的信息进行语义分析校验,主要是对比javac编译阶段的语义分析。

以保证其描述的信息服务Java语法规范的要求。

  • 字节码验证

通过数据流和控制流分析,确定程序语义是否是合法的、符合逻辑的。

在元数据校验阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

  • 符号引用验证

确保解析动作能正确执行,比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。

符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。


 2.3 准备阶段

为类中定义的变量(静态变量,被static修饰的变量)分配内存,并且设置该类变量初始值的阶段,而不是我们在程序中设定的初值。也可以认为是给类变量赋初值。

  • 这里不包含用final修饰的static,因为final在编译的时候就会分配,准备阶段会显式初始化;
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java的堆内存中。

jvm默认为静态变量的初值是这样的

  • 静态变量是基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0
  • 静态变量时引用类型的默认值为null
  • 被final和static共同修改的静态变量,我们通常称为常量,然后常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。

为什么被final和static变量修饰的成员变量在准备阶段的赋值会比较特别呢,这是因为,被final和static修改的变量,我们叫做ConstantValue属性,ConstantValue属性就是这样特殊的属性。

关于准备阶段,还有两个容易产生混淆的概念:

  • 首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  • 其次是这里所说的初始值“通常情况”下是数据类型的零值。

假设一个类变量的定义为:public static int value = 123;
那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把
value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。


2.4 解析阶段

将常量池中的符号引用转换为直接引用的过程。

解析操作往往会伴随着JVM在执行完初始化之后再执行。


什么是符号引用,什么是直接引用?

  • 符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  • 直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;
  • 在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。


2.5 初始化阶段

类的初始化阶段是类加载过程的最后一个步骤。

之前的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。

直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程
序。

初始化阶段,为静态变量赋值 ,执行静态构造块。即执行类构造器方法<clinit>()的过程。

  • 此方法不需要定义,它是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • <clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<clinit>())
  • 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
  • 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。

2.6 卸载阶段

卸载类即该类的 Class 对象被 GC回收。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

只要想通一点就好了,jdk 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。


3. 总结

类加载过程:

 变量的赋值过程:

 相关扩展知识点:

  • Java虚拟机的基本结构?

  • 类加载器?

  • 类加载的双亲委托机制?

  • 普通Java类的类加载过程和Tomcat的类加载过程是否一样?区别在哪?

  • Java堆的垃圾回收机制?

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/1006913
推荐阅读
相关标签
  

闽ICP备14008679号