JVM - 类加载机制

文章目录

    • 类加载子系统在 JVM 中的位置
    • 类加载的时机
      • 初始化时机:主动引用与被动引用
      • 被动引用举例
    • 类加载过程
      • 装载
      • 验证
      • 准备
      • 解析
      • 初始化

类加载子系统在 JVM 中的位置

首先我们来从宏观的角度看看,类加载机制在整个Java虚拟机中处于一个什么位置,先来一张JVM的组成结构图:
JVM - 类加载机制_第1张图片
     可以看到类加载子系统是属于Java虚拟机的上层建筑,只有将类从class二进制流加载到内存中,并校验准备解析通过才能正式的被Java 虚拟机所使用,之后才会讨论到运行时数据区,执行引擎。
     虚拟机把类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 这里说的类的数据,本质上是符合class文件要求的二进制字节流,并不一定要求必须是存放在磁盘上的.class 文件,任何形式都可以,可以从网络传输获取,甚至可以从数据库获取

类加载的时机

    一个类从被加载到内存开始,到卸载出内存为止,称为类的生命周期,这包含 7 个步骤。
JVM - 类加载机制_第2张图片
     加载、验证、准备、初始化和卸载这5个阶段的开始顺序是确定的,类的加载过程必须按照这个顺序按部就班的开始,但是解析阶段却不一定,它可能在初始化之前开始,也有可能在初始化之后才进行,这是为了支持Java的运行时绑定。
     注意一下,这里说的是按部就班的“开始”,并不是按部就班的“进行”或“结束”,也就是说这几个阶段的开始顺序是确定的,但是执行过程有可能是交叉进行的,经常是一边加载一边验证,加载过程尚未结束的时候,验证过程已经开始。

初始化时机:主动引用与被动引用

那么什么时候会开始一个类加载过程的第一个阶段:加载? 虚拟机规范并没有进行强制性的规定,具体的策略依赖于具体的Java虚拟机实现。但虚拟机规范对类的初始化阶段做了严格的限定:有且只有以下5种情况下,才会对类进行初始化(当然加载、验证、准备过程要在这之前已经开始)

  1. 遇到 new, getstatic, putstatic, invokestatic 指令码时,如果一个类还没被初始化过,必须进行类初始化。在 java 语言中,生成这几条指令最常见的场景是:new 一个对象,读取类的静态变量,设置类的静态变量(被static finall 修饰的类变量除外,这些变量在编译期已经实现放入常量池中)、调用类的静态方法的时候。
  2. 通过java.lang.reflect 包中的api,对类进行反射调用的时候,如果类还没有初始化过,则触发初始化过程
  3. 初始化一个类的时候,会先初始化其父类,如果父类还没有被初始化过,则先触发父类的初始化过程。(初始化接口并不需要先初始化其父接口)
  4. java虚拟机启动的时候,会指定一个类的main方法作为入口,需要触发该类的初始化过程
  5. 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic, REF_putStatic,REF_invokeStatic 的方法句柄,如果这个方法所属的类还没有初始化过,触发该类的初始化。

    java虚拟机规范对上面的5种情况用了一个”有且仅有“ 的限定词,有就是说有且仅有遇到上面的5种情况时,才会触发类的初始化过程,其他所有不属于这5中情况的都不会触发类的初始化。基于此,我们把这5种情况称之为类的主动引用,除此之外,所有的其余的引用类的方式称为类的被动引用。
     乍一听,除了这5种,还有别的方式引用类?咋一时想不想起来呢 ?? 其实是有的。主要有一下几种方式: 通过数组定义引用类(通过集合类引用某个类)、通过子类引用父类的静态变量,常量在编译阶段会存入调用类的常量池中,本质上不会触发定义常量的类的初始化。

被动引用举例

类加载过程

装载

装载过程主要由3个基本动作组成,要装载一个类型,Java虚拟机必须:

  1. 通过全限定名找到代表该类型的二进制数据流
  2. 解析这个二进制数据流为方法区内的内部数据结构
  3. 创建一个表示该类型的 java.lang.Class类的实例
    找到一个类型的 class二进制数据流之后,Java虚拟机必须对这个数据进行足够的处理,最后才能创建一个 java.lang.Class 类的实例来代表这个类型。虚拟机必须把这个数据流解析为与具体实现相关的内部数据结构,装载步骤的最终产品就是这个 java.lang.Class 类的实例, 它成为应用程序与内部数据结构之间的接口。要访问类型的信息,就通过这个实例来访问(类的信息存储在内部数据结构上,而不同的虚拟机实现有不同的表示类信息的内部数据结构),这样对应用程序来说,就屏蔽了不同虚拟机实现对类信息的存储细节。

验证

在装载开始之后,就准备进行连接了。连接的第一步就是验证: 确认类型符合 Java 语言的语义,并且不会危害 Java 虚拟机的安全。

准备

准备阶段为类变量分配内存,并设置默认初始值。这里的初始值是指类型的初始值,并不是初始化时应用程序赋予的真正的初始值。(准备阶段不执行Java代码),类型的初始值是什么呢,
JVM - 类加载机制_第3张图片
引用类型的初始值是 null

解析

解析阶段就是将类信息(准确的说是类型的常量池)中的符号引用替换成直接引用的过程。对于符号引用,各种文章讲了很多,我也有点糊涂。我这里说一下我自己的理解: 例如有这么一段java代码:

UserService service = new UserService();
service.getUserList();

这段代码编译成class文件,那么在class文件中肯定有一串字符来表示UserService 这个类(一一般来就是类的权限定名),
有一串字符来表示getUserList()方法,这些字面上的符号就是符号引用,那么在运行时要真正的用到UserService类,前提是UserService类已经被加载到内存,
那么就需要将这个字符替换为UserService类在内存中的引用,这个引用可以是UserService类在内存中的指针,也可能是内存地址偏移量,也有可能是一个句柄,无论它是什么形式,
它必须能够直接或间接地定位到UserService类在内存中的位置,这样才能去使用它。对方法也是如此,编译时产生的代表getUserList方法的字符也要转换为这个方法在内存中的位置,否则无法执行。

初始化

连接步骤之后,就可以进入到初始化阶段了。初始化阶段给类变量赋予应用程序想要的正确的初始值,以及在类被正式使用前做一些准备工作。通俗点讲,这个阶段执行static静态代码块以及类变量赋初始化值。

public class HelloWorld{
static String name = "helloworld"   // 类变量初始化语句
static {    // 静态初始化语句
	System.out.println("initialization success")	
}
}

     在准备阶段, name的值是null,只有在初始化阶段,才会赋予它正确的“初始值”。
在编译时,Java编译器将类的所有类变量初始化语句和静态初始化语句都放入到一个 方法中,顺序就按照他们在源代码中的顺序一致。这个 方法又称为类的初始化方法,它并不等同于类构造器方法。Java虚拟机保证在执行类的 方法之前,会先确保其父类的 方法已经执行过,不像构造器方法一样,你需要显示地调用其父类构造器,也就是说一个类在初始化之前,必定会先初始化其父类,其父类的初始化过程同样如此,所以Object类肯定是最先被初始化的 。 方法并不是所有类都会存在,只有在必要的情况下它才会出现,什么是必要的情况呢,也就是说它能不出现就不出现。如果一个类没有任何的类静态变量(或者这个类静态变量不需要初始化)也没有任何的静态初始化语句,那么类就不会有 方法。但是即使一个类没有 方法,也并不代表它的父类没有 方法,在初始化时,它的父类也是要先初始化的。而且这个方法 是由java虚拟机调用的,我们的java程序是无法调用它的。
     说这么多,可能有人会有个疑问:既然我们无法显示调用它,要理解那么多干嘛?对我们的写代码能力没有任何帮助嘛! 那就来说点干货,对我们的编码也能够起到一定的指导意义: Java虚拟机需要保证一个类只初始化一次,那么也就是说在多线程环境下,它必须被正确的同步,如果多个线程同时对一个类进行初始化,只能有一个线程能够执行初始化,其他线程会等待!也就是说,我们最好在类初始化中不要做太繁重的工作

你可能感兴趣的:(java进阶)