jvm基础: <clinit> 与 <init> 方法
类的初始化.
先理解 类初始化阶段 的含义: 该阶段负责为类变量赋予正确的初始值, 是一个类或接口被首次使用前的最后一项工作。
- (首次)创建某个类的新实例时--new, 反射, 克隆 或 反序列化;
- (首次)调用某个类的静态方法时;
- (首次)使用某个类或接口的静态字段或对该字段(final 字段除外)赋值时;
- (首次)调用java的某些反射方法时;
- (首次)初始化某个类的子类时;
- (首次)在虚拟机启动时某个含有 main() 方法的那个启动类
在instanceKlass类的initialize_impl()方法中定义了类的初始化过程。
https://github.com/openjdk/jdk/blob/f014854ac71a82b307667ba017f01b13eed54330/src/hotspot/share/oops/instanceKlass.cpp#L1057
// Step 8 // 执行类或接口的初始化方法<clinit> { DTRACE_CLASSINIT_PROBE_WAIT(clinit, -1, wait); if (class_initializer() != NULL) { // Timer includes any side effects of class initialization (resolution, // etc), but not recursive entry into call_class_initializer(). PerfClassTraceTime timer(ClassLoader::perf_class_init_time(), ClassLoader::perf_class_init_selftime(), ClassLoader::perf_classes_inited(), jt->get_thread_stat()->perf_recursion_counts_addr(), jt->get_thread_stat()->perf_timers_addr(), PerfClassTraceTime::CLASS_CLINIT); call_class_initializer(THREAD);// 调用类或接口的<clinit>方法 } else { // The elapsed time is so small it's not worth counting. if (UsePerfData) { ClassLoader::perf_classes_inited()->inc(); } call_class_initializer(THREAD); } }
在类初始化过程中,最重要的就是调用类的<clinit>方法了,该方法是由Javac编译器自动生成和命名的,<clinit>是一个不含参数的静态方法,该方法不能通过程序直接编码的方式实现,只能由编译器根据类变量的赋值语句或静态语句块自动插入到Class文件中。此外,<clinit>方法没有任何虚拟机字节码指令可以调用,它只能在类型初始化阶段被虚拟机隐式调用。最终在最后一行通过JavaCalls模块执行该类的<clinit>方法。
- <clinit>方法 的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作)
- <clinit>方法 的内容: 所有的类变量初始化语句和类型的静态初始化器
注意: 并非所有的类都会拥有一个<clinit>方法, 满足下列条件之一的类不会拥有<clinit>方法:
-
该类既没有声明任何类变量,也没有静态初始化语句;
-
该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;
-
该类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式;
如果不明白<clinit>方法的作用及产生的过程,可以参考《深入解析Java编译器:源码剖析与实例详解》,这本书将会详细介绍这个方法的来龙去脉。
调用InstanceKlass::call_class_initializer()函数来执行<clinit>方法,函数的实现如下:
https://github.com/openjdk/jdk/blob/f014854ac71a82b307667ba017f01b13eed54330/src/hotspot/share/oops/instanceKlass.cpp#L1480
void InstanceKlass::call_class_initializer(TRAPS) { if (ReplayCompiles && (ReplaySuppressInitializers == 1 || (ReplaySuppressInitializers >= 2 && class_loader() != NULL))) { // Hide the existence of the initializer for the purpose of replaying the compile return; } methodHandle h_method(THREAD, class_initializer()); //获取该类的类初始化方法<clinit> assert(!is_initialized(), "we cannot initialize twice"); LogTarget(Info, class, init) lt; if (lt.is_enabled()) { ResourceMark rm(THREAD); LogStream ls(lt); ls.print("%d Initializing ", call_class_initializer_counter++); name()->print_value_on(&ls); ls.print_cr("%s (" INTPTR_FORMAT ")", h_method() == NULL ? "(no method)" : "", p2i(this)); } if (h_method() != NULL) { JavaCallArguments args; // No arguments JavaValue result(T_VOID); JavaCalls::call(&result, h_method, &args, CHECK); // Static call (no args) } }
最终通过调用JavaCalls::call()函数来完成Java方法的调用,这个函数的实现非常重要,在前面也多次接触过这个函数,不过目前还没有介绍相关的执行过程,在介绍方法执行引擎时会详细介绍。
<clinit>方法如下:
https://github.com/openjdk/jdk/blob/f014854ac71a82b307667ba017f01b13eed54330/src/hotspot/share/oops/instanceKlass.cpp#L1471
static int call_class_initializer_counter = 0; // for debugging Method* InstanceKlass::class_initializer() const { Method* clinit = find_method( vmSymbols::class_initializer_name(), vmSymbols::void_method_signature()); if (clinit != NULL && clinit->has_valid_initializer_flags()) { return clinit; } return NULL; }
案例解析
1、关于编译错误illegal forward reference(违法向前引用):
package com.jvm.exercises; /** * @author dimdark */ public class ClinitAndInitTest { static ClinitAndInitTest test = new ClinitAndInitTest(); // 静态语句块 static { System.out.println("static statements block"); // 注意 test 与 s 的声明位置 System.out.println(test); // 调用类变量test, 未出现编译错误 System.out.println(s); // 调用类变量s, 出现编译错误illegal forward reference } static String s = "string"; }
结论:
在static语句块中使用到静态变量时一定要将该静态变量的声明语句放在static语句块的前面, 否则会发生illegal forward references的编译错误。
2、关于静态常量(static final类型)的赋值时机所引起的问题:
// 对比下面两段代码的输出结果 package com.jvm.exercises; /** * @author dimdark */ public class ClinitTestFive { private static ClinitTestFive test; static { test = new ClinitTestFive(); } private static final String name = "string_name"; private String testName; private ClinitTestFive() { testName = name; } public static void main(String[] args) { System.out.println(test.testName); // 输出结果为: string_name } } package com.jvm.exercises; /** * @author dimdark */ public class ClinitTestFive { private static ClinitTestFive test; static { test = new ClinitTestFive(); } private static final String name = new String("string_name"); private String testName; private ClinitTestFive() { testName = name; } public static void main(String[] args) { System.out.println(test.testName); // 输出结果为: null } }
结论: 要保证静态常量在使用前被赋予值, 否则会出现意想不到的情况.
<init>方法:
-
<init>方法 的执行时期: 对象的初始化阶段
-
实例化一个类的四种途径:
1. 调用new
操作符
2. 调用Class
或java.lang.reflect.Constructor
对象的newInstance()
方法
3. 调用任何现有对象的clone()
方法
4. 通过java.io.ObjectInputStream
类的getObject()
方法反序列化 -
小案例:
package com.jvm.exercises; /** * @author dimdark */ public class InitTest { private int code = 0; InitTest() { code = 1; name = "init_name"; } private String name = "name"; @Override public String toString() { return "InitTest{" + "code=" + code + ", name='" + name + '\'' + '}'; } public static void main(String[] args) { System.out.println(new InitTest()); // InitTest{code=1, name='init_name'} } }
作者:dimdark
链接:https://www.jianshu.com/p/8a14ed0ed1e9
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
init和clinit区别
①init和clinit方法执行时机不同
init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
②init和clinit方法执行目的不同
init is the (or one of the) constructor(s) for the instance, and non-static field initialization.
clinit are the static initialization blocks for the class, and static field initialization.
上面这两句是Stack Overflow上的解析,很清楚init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。看看下面的这段程序就很清楚了。
class X { static Log log = LogFactory.getLog(); // <clinit> private int x = 1; // <init> X(){ // <init> } static { // <clinit> } }
clinit详解
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
①<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问如下代码
public class Test{ static{ i=0;//给变量赋值可以正常编译通过 System.out.print(i);//这句编译器会提示"非法向前引用" } static int i=1; }
②虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。 因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下代码中,字段B的值将会是2而不是1。
static class Parent{ public static int A=1; static{ A=2;} static class Sub extends Parent{ public static int B=A; } public static void main(String[]args){ System.out.println(Sub.B); } }
③接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。 但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。 只有当父接口中定义的变量使用时,父接口才会初始化。 另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
注意:接口中的属性都是static final类型的常量,因此在准备阶段就已经初始化话。
参考链接:
jvm基础第三节: <clinit> 与 <init> 方法 - 简书 https://www.jianshu.com/p/8a14ed0ed1e9
深入理解jvm--Java中init和clinit区别完全解析_有图有真相-CSDN博客 https://blog.csdn.net/u013309870/article/details/72975536
深入理解Java对象的创建过程:类的初始化与实例化_Rico's Blogs-CSDN博客 https://blog.csdn.net/justloveyou_/article/details/72466416
JVM init和clinit的理解_u012588160的博客-CSDN博客 https://blog.csdn.net/u012588160/article/details/100108895
JVM源码分析之Java对象的创建过程 - 知乎 https://zhuanlan.zhihu.com/p/48165015
类的初始化 - HotSpot-Researcher - 博客园 https://www.cnblogs.com/mazhimazhi/p/13495639.html
其它链接:
InstanceKlass::class_initializer()
GitHub - JetBrains/jdk8u_hotspot https://github.com/JetBrains/jdk8u_hotspot