深入理解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()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏马涛涛的专栏

JS题目总结:原型链/new/json/MVC/Promise

解读: 上图中,Object,Function,Array,Boolean都是构造函数

822
来自专栏xx_Cc的学习总结专栏

OC-基础总结(二)

27210
来自专栏mukekeheart的iOS之旅

OC学习10——内存管理

1、对于面向对象的语言,程序需要不断地创建对象。这些对象都是保存在堆内存中,而我们的指针变量中保存的是这些对象在堆内存中的地址,当该对象使用结束之后,指针变量指...

2105
来自专栏老九学堂

【干货】2016高薪Java面试题集锦

很多小伙伴毕业在即,找工作和面试又被提上了日程,为了解决小伙伴们的燃眉之急,老九君特地为大家整理了一份最新的Java面试题集锦与答案,希望能给近期将要找工作的小...

3648
来自专栏老九学堂

必看 | 新人必看的Java基础知识点大梳理

各位正在认真苦学Java的准大神,在这烈日炎炎的夏季里,老九君准备给大家带来一个超级大的“冰镇西瓜,”给大家清凉一下,压压惊。但这个大西瓜可不是一般的大西瓜,是...

3418
来自专栏java工会

Java基础第一阶段知识点,招实习的面试官都在问这些

a) 答:Java源文件被编译成字节码的形式,无论在什么系统环境下,只要有java虚

431
来自专栏JAVA高级架构

Java内存区域与虚拟机类加载机制

一、Java运行时数据区域 ? 1、程序计数器   “线程私有”的内存,是一个较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器。Java虚拟机规范...

3358
来自专栏林德熙的博客

Roslyn NameSyntax 的 ToString 和 ToFullString 的区别

本文告诉大家经常使用的 NameSyntax 拿到值的 ToString 和 ToFullString 方法的区别

632
来自专栏陈树义

从 HelloWorld 看 Java 字节码文件结构

很多时候,我们都是从代码层面去学习如何编程,却很少去看看一个个 Java 代码背后到底是什么。今天就让我们从一个最简单的 Hello World 开始看一看 J...

3827
来自专栏Android 研究

Java虚拟机基础——2JVM运行时数据区

本篇文章主要讲解JVM运行时数据区,所以我们按照线程是否私有的维度将本篇文章一分为二,分为线程私有数据区和所有线程共有的数据区。而在线程私有的数据区又可以分为程...

845

扫码关注云+社区