深入理解JVM(九)——类加载的过程

通过之前的介绍可知,类加载过程共有5个步骤,分别是:加载、验证、准备、解析、初始化。其中,验证、准备、解析称为连接。下面详细介绍这5个过程JVM所做的工作。

加载

注意:“加载”是“类加载”过程的第一步,千万不要混淆。

1. 加载的过程

在加载过程中,JVM主要做3件事情:

  1. 通过一个类的全限定名来获取这个类的二进制字节流,即class文件:

    在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化时机的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程。

  2. 将二进制字节流的存储结构转化为特定的数据结构,存储在方法区中;
  3. 在内存中创建一个java.lang.Class类型的对象:

    接下来程序在运行过程中所有对该类的访问都通过这个类对象,也就是这个Class类型的类对象是提供给外界访问该类的接口。

2. 从哪里加载?

JVM规范对于加载过程给予了较大的宽松度。一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从以下地方读取:

  • 从压缩包中读取

    如:Jar、War、Ear等。

  • 从其它文件中动态生成

    如:从JSP文件中生成Class类。

  • 从数据库中读取

    将二进制字节流存储至数据库中,然后在加载时从数据库中读取。有些中间件会这么做,用来实现代码在集群间分发。

  • 从网络中获取

    从网络中获取二进制字节流。典型就是Applet。

3. 类 和 数组加载过程的区别?

数组也有类型,称为“数组类型”。如:

String[] str = new String[10];

这个数组的数组类型是Ljava.lang.String,而String只是这个数组中元素的类型。

当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类,再由类加载器创建数组中的元素类。

而普通类的加载由类加载器完成。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器。

4. 加载过程的注意点

  1. JVM规范并未给出类在方法区中存放的数据结构

    类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,JVM规范并没有指定。

  2. JVM规范并没有指定Class对象存放的位置

    在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类型的对象,作为本类的外部接口。既然是对象就应该存放在堆内存中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象。HotSpot将Class对象存放在方法区。

  3. 加载阶段和连接阶段是交叉的

    通过之前的介绍可知,类加载过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始:

    加载、连接、初始化,但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。

验证

验证阶段比较耗时,它非常重要但不一定必要,如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间。

1. 验证的目的是什么?

验证是为了保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。

2. 为什么需要验证?

虽然Java语言是一门安全的语言,它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行,如果出现这些情况,编译无法通过。也就是说,Java语言的安全性是通过编译器来保证的。

但是我们知道,编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的,当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的。通过上文可知,虚拟机规范中没有限制二进制字节流的来源,那么任意来源的二进制字节流虚拟机都能接受,为了防止字节流中有安全问题,因此需要验证!

3. 验证的过程

  1. 文件格式验证

    这个阶段主要验证输入的二进制字节流是否符合class文件结构的规范。二进制字节流只有通过了本阶段的验证,才会被允许存入到方法区中。

    本验证阶段是基于二进制字节流的,而后面的三个验证阶段都是在方法区中进行,并基于类特定的数据结构的。

    通过上文可知,加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区。而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区。也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作。这个过程印证了:加载和验证是交叉进行的。

  2. 元数据验证

    本阶段对方法区中的字节码描述信息进行语义分析,确保其符合Java语法规范。

  3. 字节码验证

    本阶段是验证过程的最复杂的一个阶段。

    本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。

  4. 符号引用验证

    本阶段验证发生在解析阶段,确保解析能正常执行。

准备

准备阶段完成两件事情:

1. 为已经在方法区中的类中的静态成员变量分配内存

类的静态成员变量也存储在方法区中。

2. 为静态成员变量设置初始值

初始值为0、false、null等。

示例1:

public static String name = "柴毛毛";

在准备阶段,JVM会在方法区中为name分配内存空间,并赋上初始值null。

给name赋上”柴毛毛”是在初始化阶段完成的。

示例2:

public static final String name = "柴毛毛";

被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段。

解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

初始化

初始化阶段就是执行类构造器clinit()的过程。

clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。

初始化过程的注意点

  1. clinit()方法中静态成员变量的赋值顺序是根据Java代码中成员变量的出现的顺序决定的。
  2. 静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量。
  3. 静态代码块能给出现在静态代码块之后的静态成员变量赋值。
  4. 构造函数init()需要显示调用父类构造函数,而类的构造函数clinit()不需要调用父类的类构造函数,因为虚拟机会确保子类的clinit()方法执行前已经执行了父类的clinit()方法。
  5. 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成clinit()方法。
  6. 接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化。
  7. 接口中不能使用静态代码块。
  8. 接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法。
  9. 虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方法会被执行,其它的方法都被阻塞。并且,只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次。
时间: 2024-10-06 18:08:30

深入理解JVM(九)——类加载的过程的相关文章

深入理解JVM之类加载

---title: [学习]深入理解JVM之类加载.mddate: 2019-10-20 22:20:06tags: JVM 类加载--- Java类的加载,连接,初始化都是在程序运行期间执行的 ## Java 虚拟机与程序的生命周期 1. 执行 System.exit()方法2. 程序正常结束3. 遇到异常或错误终止4. 由于操作系统或程序虚拟机进程错误 以上的情况都可以结束生命周期 ## Java 类加载的方式 1. 本地系统直接加载2. 通过网络下载.class 文件3. 通过zip,ja

【深入理解JVM】类加载器与双亲委派模型

原文链接:http://blog.csdn.net/u011080472/article/details/51332866,http://www.cnblogs.com/lanxuezaipiao/p/4138511.html 加载类的开放性 类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因.在类加载的第一阶段"加载"过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器.这一动作是放在Java虚拟机外部

【深入理解JVM】:类加载机制

概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 与那些在编译时需要进行链接工作的语言不同,在Java语言里,类型的加载.连接和初始化过程都是在程序运行期间完成的,例如import java.util.*下面包含很多类,但是,在程序运行的时候,虚拟机只会加载哪些我们程序需要的类.这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性. 类加载的时机 类从

深入理解JVM读书笔记三: 虚拟机类加载机制

Java虚拟机类加载机制是把Class类文件加载到内存,并对Class文件中的数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程. 7.1概述 与那些在编译时需要进行链接工作的语言不同,在Java语言里面,类型的加载和链接过程都是在程序运行期间完成的(其实C++也是分为静态链接库和动态链接库的),这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的. 7.

JVM虚拟机之类加载的过程

我们都知道JVM虚拟机的可执行文件为.class文件,那么什么时候JVM虚拟机会加载自己所需要的类呢?之前自己一直有这样的问题,上网找过好多网友的解释,感觉好像理解但是自己却无法说清楚,今天看了<深入理解JVM虚拟机>一书的讲解感觉自己有些透了,在此记录下来自己的理解~! 类的生命周期 类加载过程(主动|被动) 类的主动引用(一定会发生类的初始化) - new一个类的对象 - 调用类的静态成员(除了final常量)和静态方法 - 使用java.lang.reflect包的方法对类进行反射调用

深入理解JVM虚拟机6:深入理解JVM类加载机制

深入理解JVM类加载机制 简述:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 下面我们具体来看类加载的过程: 类的生命周期 类从被加载到内存中开始,到卸载出内存,经历了加载.连接.初始化.使用四个阶段,其中连接又包含了验证.准备.解析三个步骤.这些步骤总体上是按照图中顺序进行的,但是Java语言本身支持运行时绑定,所以解析阶段也可以是在初始化之后进行的.以上顺序都只是说开始的顺序,实际过

深入理解JVM之CAS原子操作(九)

在准备进入concurrentHashMap的源码世界中的时候,发现很多方法是基于CAS原子操作的,之前在看JVM的时候确实看过CAS,但是并没有仔细的去研究,所以决定先一探究竟,经过网上查阅一些资料,对CAS还是有了一些理解. 如果说在只有一个线程的时候,资源不会出现竞争,也不会存在所谓的共享资源的说法,很多问题可以避免,也无需讨论原子性操作的问题.但是当出现多个线程协调运行的时候,很多问题就会出现了,怎么提高多个线程访问共享资源的效率,如何避免多个线程并发读写共享资源时不出现数据混乱,这些问

《深入理解Java虚拟机》:类加载的过程

<深入理解Java虚拟机>:类加载的过程 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段. 下面详细讲述类加载过程中每个阶段所做的工作. 加载 加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情: 1.通过一个类的全限定名来获取其定义的二进制字节流. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构. 3.在Java堆中生成一

深入理解JVM(七)JVM类加载机制

7.1JVM类加载机制 虚拟机把数据从Class文件加载到内存,并且校验.转换解析和初始化最终形成可以被虚拟机使用的Java类型,这就是虚拟机的类加载机制. 7.2类加载的时机 1.类加载的步骤开始的顺序: 加载(Loading) -> 验证(Verification) -> 准备(Preparation) -> 解析(Resolution) -> 初始化(Initialization) -> 使用(Using) -> 卸载(Unloading) ,验证.准备.解析的过