JVM 的类初始化机制

2024-09-09 18:18
文章标签 java jvm 初始化 机制

本文主要是介绍JVM 的类初始化机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。

JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization。 

Loading

Loading 过程主要工作是由ClassLoader完成。该过程具体包括三件事:

  1. 根据类的全名,生成一份二进制字节码来表示该类
  2. 将二进制的字节码解析成方法区对应的数据结构
  3. 最后生成一 Class 对象的实例来表示该类

JVM 中除了最顶层的Boostrap ClassLoader是用 C/C++ 实现外,其余类加载器均由 Java 实现,我们可以用getClassLoader方法来获取当前类的类加载器:

public class ClassLoaderDemo {public static void main(String[] args) {System.out.println(ClassLoaderDemo.class.getClassLoader());}
}# sun.misc.Launcher$AppClassLoader@30a4effe
# AppClassLoader 也就是上图中的 System Class Loader

此外,我们在启动java传入-verbose:class来查看加载的类有那些。

java -verbose:class ClassLoaderDemo[Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]....
....[Loaded java.security.BasicPermissionCollection from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded ClassLoaderDemo from file:/Users/liujiacai/codes/IdeaProjects/mysql-test/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
sun.misc.Launcher$AppClassLoader@2a139a55
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]

Linking

Verification

Verification 主要是保证类符合 Java 语法规范,确保不会影响 JVM 的运行。包括但不限于以下事项:

  • bytecode 的完整性(integrity)
  • 检查final类没有被继承,final方法没有被覆盖
  • 确保没有不兼容的方法签名
Preparation

在一个类已经被load并且通过verification后,就进入到preparation阶段。在这个阶段,JVM 会为类的成员变量分配内存空间并且赋予默认初始值,需要注意的是这个阶段不会执行任何代码,而只是根据变量类型决定初始值。如果不进行默认初始化,分配的空间的值是随机的,有点类型c语言中的野指针问题。

Type    Initial Value
int    0
long    0L
short    (short) 0
char    '\u0000'
byte    (byte) 0
boolean    false
reference    null
float    0.0f
double    0.0d

在这个阶段,JVM 也可能会为有助于提高程序性能的数据结构分配内存,常见的一个称为method table的数据结构,它包含了指向所有类方法(也包括也从父类继承的方法)的指针,这样再调用父类方法时就不用再去搜索了。

Resolution

Resolution 阶段主要工作是确认类、接口、属性和方法在类run-time constant pool的位置,并且把这些符号引用(symbolic references)替换为直接引用(direct references)。

locating classes, interfaces, fields, and methods referenced symbolically from a type’s constant pool, and replacing those symbolic references with direct references.

这个过程不是必须的,也可以发生在第一次使用某个符号引用时。

Initialization

经过了上面的load、link后,第一次 主动调用某类的最后一步是Initialization,这个过程会去按照代码书写顺序进行初始化,这个阶段会去真正执行代码,注意包括:代码块(static与static)、构造函数、变量显式赋值。如果一个类有父类,会先去执行父类的initialization阶段,然后在执行自己的。

上面这段话有两个关键词:第一次与主动调用。第一次是说只在第一次时才会有初始化过程,以后就不需要了,可以理解为每个类有且仅有一次初始化的机会。那么什么是主动调用呢? JVM 规定了以下六种情况为主动调用,其余的皆为被动调用:

  1. 一个类的实例被创建(new操作、反射、cloning,反序列化)
  2. 调用类的static方法
  3. 使用或对类/接口的static属性进行赋值时(这不包括final的与在编译期确定的常量表达式)
  4. 当调用 API 中的某些反射方法时
  5. 子类被初始化
  6. 被设定为 JVM 启动时的启动类(具有main方法的类)

本文后面会给出一个示例用于说明主动调用的被动调用区别。

在这个阶段,执行代码的顺序遵循以下两个原则:

  1. 有static先初始化static,然后是非static的
  2. 显式初始化,构造块初始化,最后调用构造函数进行初始化

示例

属性在不同时期的赋值
class Singleton {private static Singleton mInstance = new Singleton();// 位置1public static int counter1;public static int counter2 = 0;//    private static Singleton mInstance = new Singleton();// 位置2private Singleton() {counter1++;counter2++;}public static Singleton getInstantce() {return mInstance;}
}public class InitDemo {public static void main(String[] args) {Singleton singleton = Singleton.getInstantce();System.out.println("counter1: " + singleton.counter1);System.out.println("counter2: " + singleton.counter2);}
}

当mInstance在位置1时,打印出

counter1: 1
counter2: 0

当mInstance在位置2时,打印出

counter1: 1
counter2: 1

Singleton中的三个属性在Preparation阶段会根据类型赋予默认值,在Initialization阶段会根据显示赋值的表达式再次进行赋值(按顺序自上而下执行)。根据这两点,就不难理解上面的结果了。

主动调用 vs. 被动调用
class NewParent {static int hoursOfSleep = (int) (Math.random() * 3.0);static {System.out.println("NewParent was initialized.");}
}class NewbornBaby extends NewParent {static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);static {System.out.println("NewbornBaby was initialized.");}
}public class ActiveUsageDemo {// Invoking main() is an active use of ActiveUsageDemopublic static void main(String[] args) {// Using hoursOfSleep is an active use of NewParent,// but a passive use of NewbornBabySystem.out.println(NewbornBaby.hoursOfSleep);}static {System.out.println("ActiveUsageDemo was initialized.");}
}

上面的程序最终输出:

ActiveUsageDemo was initialized.
NewParent was initialized.
1

之所以没有输出NewbornBaby was initialized.是因为没有主动去调用NewbornBaby,如果把打印的内容改为NewbornBaby.hoursOfCrying 那么这时就是主动调用NewbornBaby了,相应的语句也会打印出来。

首次主动调用才会初始化
public class Alibaba {public static int k = 0;public static Alibaba t1 = new Alibaba("t1");public static Alibaba t2 = new Alibaba("t2");public static int i = print("i");public static int n = 99;private int a = 0;public int j = print("j");{print("构造块");}static {print("静态块");}public Alibaba(String str) {System.out.println((++k) + ":" + str + "   i=" + i + "    n=" + n);++i;++n;}public static int print(String str) {System.out.println((++k) + ":" + str + "   i=" + i + "    n=" + n);++n;return ++i;}public static void main(String args[]) {Alibaba t = new Alibaba("init");}
}

上面这个例子是阿里巴巴在14年的校招附加题,我当时看到这个题,就觉得与阿里无缘了。囧

1:j   i=0    n=0
2:构造块   i=1    n=1
3:t1   i=2    n=2
4:j   i=3    n=3
5:构造块   i=4    n=4
6:t2   i=5    n=5
7:i   i=6    n=6
8:静态块   i=7    n=99
9:j   i=8    n=100
10:构造块   i=9    n=101
11:init   i=10    n=102

上面是程序的输出结果,下面我来一行行分析之。

  1. 由于Alibaba是 JVM 的启动类,属于主动调用,所以会依此进行 loading、linking、initialization 三个过程。
  2. 经过 loading与 linking 阶段后,所有的属性都有了默认值,然后进入最后的 initialization 阶段。
  3. 在 initialization 阶段,先对 static 属性赋值,然后在非 static 的。k 第一个显式赋值为 0 。
  4. 接下来是t1属性,由于这时Alibaba这个类已经处于 initialization 阶段,static 变量无需再次初始化了,所以忽略 static 属性的赋值,只对非 static 的属性进行赋值,所有有了开始的:
    1:j   i=0    n=02:构造块   i=1    n=13:t1   i=2    n=2
    
  5. 接着对t2进行赋值,过程与t1相同
     4:j   i=3    n=35:构造块   i=4    n=46:t2   i=5    n=5
    
  6. 之后到了 static 的 i 与 n:
     7:i   i=6    n=6
    
  7. 到现在为止,所有的static的成员变量已经赋值完成,接下来就到了 static 代码块
    8:静态块   i=7    n=99
    
  8. 至此,所有的 static 部分赋值完毕,接下来是非 static 的 j
     9:j   i=8    n=100
    
  9. 所有属性都赋值完毕,最后是构造块与构造函数
    10:构造块   i=9    n=10111:init   i=10    n=102
    

经过上面这9步,Alibaba这个类的初始化过程就算完成了。这里面比较容易出错的是第3步,认为会再次初始化 static 变量或代码块。而实际上是没必要,否则会出现多次初始化的情况。

这篇关于JVM 的类初始化机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1151914

相关文章

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Java中的工具类命名方法

《Java中的工具类命名方法》:本文主要介绍Java中的工具类究竟如何命名,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java中的工具类究竟如何命名?先来几个例子几种命名方式的比较到底如何命名 ?总结Java中的工具类究竟如何命名?先来几个例子JD

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依