摘要:当你在程序中对象时,有没有考虑过是如何把静态的字节码转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。
当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。
Loading, Linking, and InitializationJVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization。
下面分别介绍这三个过程:
LoadingLoading 过程主要工作是由ClassLoader完成。该过程具体包括三件事:
根据类的全名,生成一份二进制字节码来表示该类
将二进制的字节码解析成方法区对应的数据结构
最后生成一 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的数据结构,它包含了指向所有类方法(也包括也从父类继承的方法)的指针,这样再调用父类方法时就不用再去搜索了。
ResolutionResolution 阶段主要工作是确认类、接口、属性和方法在类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 规定了以下六种情况为主动调用,其余的皆为被动调用:
一个类的实例被创建(new操作、反射、cloning,反序列化)
调用类的static方法
使用或对类/接口的static属性进行赋值时(这不包括final的与在编译期确定的常量表达式)
当调用 API 中的某些反射方法时
子类被初始化
被设定为 JVM 启动时的启动类(具有main方法的类)
本文后面会给出一个示例用于说明主动调用的被动调用区别。
在这个阶段,执行代码的顺序遵循以下两个原则:
有static先初始化static,然后是非static的
显式初始化,构造块初始化,最后调用构造函数进行初始化
示例 属性在不同时期的赋值class Singleton { private static Singleton mInstance = new Singleton();// 位置1 public static int counter1; public static int counter2 = 0; // private static Singleton mInstance = new Singleton();// 位置2 private 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 ActiveUsageDemo public static void main(String[] args) { // Using hoursOfSleep is an active use of NewParent, // but a passive use of NewbornBaby System.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
上面是程序的输出结果,下面我来一行行分析之。
由于Alibaba是 JVM 的启动类,属于主动调用,所以会依此进行 loading、linking、initialization 三个过程。
经过 loading与 linking 阶段后,所有的属性都有了默认值,然后进入最后的 initialization 阶段。
在 initialization 阶段,先对 static 属性赋值,然后在非 static 的。k 第一个显式赋值为 0 。
接下来是t1属性,由于这时Alibaba这个类已经处于 initialization 阶段,static 变量无需再次初始化了,所以忽略 static 属性的赋值,只对非 static 的属性进行赋值,所有有了开始的:
1:j i=0 n=0 2:构造块 i=1 n=1 3:t1 i=2 n=2
接着对t2进行赋值,过程与t1相同
4:j i=3 n=3 5:构造块 i=4 n=4 6:t2 i=5 n=5
之后到了 static 的 i 与 n:
7:i i=6 n=6
到现在为止,所有的static的成员变量已经赋值完成,接下来就到了 static 代码块
8:静态块 i=7 n=99
至此,所有的 static 部分赋值完毕,接下来是非 static 的 j
9:j i=8 n=100
所有属性都赋值完毕,最后是构造块与构造函数
10:构造块 i=9 n=101 11:init i=10 n=102
经过上面这9步,Alibaba这个类的初始化过程就算完成了。这里面比较容易出错的是第3步,认为会再次初始化 static 变量或代码块。而实际上是没必要,否则会出现多次初始化的情况。
希望大家能多思考思考这个例子的结果,加深这三个过程的理解。
总结经过最后这三个例子,相信大家对 JVM 对类加载机制都有了更深的理解,如果大家还是有疑问,欢迎留意讨论。
参考Java Virtual Machine Specification Chapter 5
Chapter 7 of Inside the Java Virtual Machine
JVM Internals
What kind of method is Constructor, static or non static?
Understanding the Java ClassLoader
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/66501.html
摘要:如果需要支持类的动态加载或需要对编译后的字节码文件进行解密操作等,就需要与类加载器打交道了。双亲委派模型,双亲委派模型,约定类加载器的加载机制。任何之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由调用。 jvm系列 垃圾回收基础 JVM的编译策略 GC的三大基础算法 GC的三大高级算法 GC策略的评价指标 JVM信息查看 GC通用日志解读 jvm的card table数据...
摘要:学习能更深入的理解这门语言,能理解语言底层的执行过程,深入到字节码层次。 目录 前言 程序的运行 1.JVM类加载机制 ①一般在什么情况下会去加载一个类?也就是说,什么时候.class字节码文件中加载这个类到JVM内存里来? ②验证、准备、初始化 ③初始化 2.类加载器和双亲委派机制 ...
摘要:前面提到,对于数组类来说,它并没有对应的字节流,而是由虚拟机直接生成的。对于其他的类来说,虚拟机则需要借助类加载器来完成查找字节流的过程。验证阶段的目的,在于确保被加载类能够满足虚拟机的约束条件。 Java 虚拟机将字节流转化为 Java 类的过程。这个过程可分为加载、链接以及初始化 三大步骤。 加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类...
摘要:作用负责将加载到中审查每个类由谁加载父优先的等级加载机制将字节码重新解析成统一要求的对象格式类结构分析为了更好的理解类的加载机制,我们来深入研究一下和他的方法。就算两个是同一份字节码,如果被两个不同的实例所加载,也会认为它们是两个不同。 申明:本文首发于 详细深入分析 ClassLoader 工作机制 ,如有转载,注明原出处即可,谢谢配合。 什么是 ClassLoader ? 大家...
阅读 4643·2021-11-18 13:23
阅读 877·2021-09-22 15:24
阅读 1903·2021-09-06 15:00
阅读 2600·2021-09-03 10:30
阅读 1258·2021-09-02 15:15
阅读 2034·2019-08-30 15:54
阅读 3019·2019-08-30 15:44
阅读 1429·2019-08-29 15:12