服务器租用网站模版,推广计划有几种状态,Wordpress4.0参考手册.CHM,网络营销方式有哪些?举例说明Java 何时会触发一个类的初始化#xff1f;
使用new关键字创建对象访问类的静态成员变量 或 对类的静态成员变量进行赋值调用类的静态方法反射调用类时#xff0c;如 Class.forName()初始化子类时#xff0c;会先初始化其父类#xff08;如果父类还没有进行过初始化的话
使用new关键字创建对象访问类的静态成员变量 或 对类的静态成员变量进行赋值调用类的静态方法反射调用类时如 Class.forName()初始化子类时会先初始化其父类如果父类还没有进行过初始化的话遇到启动类时如果一个类被标记为启动类(即包含main方法虚拟机会先初始化这个主类。实现带有默认方法的接口的类被初始化时拥有被default关键字修饰的接口方法的类使用 JDK7 新加入的动态语言支持时 MethodHandle
虚拟机在何时加载类
关于在什么情况下需要开始类加载的第一个阶段《Java虚拟机规范》中并没有进行强制约束留给虚拟机自由发挥。但对于初始化阶段虚拟机规范则严格规定当且仅当出现以下六种情况时必须立即对类进行初始化而加载、验证、准备自然需要在此之前进行。虚拟机规范中对这六种场景中的行为称为对一个类型进行主动引用。除此之外所有引用类型的方式都不会触发初始化称为被动引用。
1. 遇到指定指令时
在程序执行过程中遇到 new、getstatic、putstatic、invokestatic 这4条字节码执行时如果类型没有初始化则需要先触发其初始化阶段。
new
这没什么好说的使用new关键字创建对象肯定会触发该类的初始化。
getstatic 与 putstatic
当访问某个类或接口的静态变量或对该静态变量进行赋值时会触发类的初始化。首先来看第一个例子
// 示例1
public class Demo {public static void main(String[] args) {System.out.println(Bird.a);}
}class Bird {static int a 2;// 在类初始化过程中不仅会执行构造方法还会执行类的静态代码块// 如果静态代码块里的语句被执行说明类已开始初始化static {System.out.println(bird init);}
}执行后会输出
bird init
2同样地如果直接给Bird.a进行赋值也会触发Bird类的初始化
public class Demo {public static void main(String[] args) {Bird.a 2;}
}class Bird {static int a;static {System.out.println(bird init);}
}执行后会输出
bird init接着再看下面的例子
public class Demo {public static void main(String[] args) {Bird.a 2;}
}class Bird {// 与前面的例子不同的是这里使用 final 修饰static final int a 2;static {System.out.println(bird init);}
}执行后不会有输出。
本例中a不再是一个静态变量而变成了一个常量运行代码后发现并没有触发Bird类的初始化流程。常量在编译阶段会存入到调用这个常量的方法所在类的常量池中。本质上调用类并没有直接引用定义常量的类因此并不会触发定义常量的类的初始化。即这里已经将常量a2存入到Demo类的常量池中这之后Demo类与Bird类已经没有任何关系甚至可以直接把Bird类生成的class文件删除Demo仍然可以正常运行。使用javap命令反编译一下字节码
// 前面已省略无关部分public static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: iconst_24: invokevirtual #4 // Method java/io/PrintStream.println:(I)V7: return
}从反编译后的代码中可以看到Bird.a已经变成了助记符iconst_2(将int类型2推送至栈顶)和Bird类已经没有任何联系这也从侧面证明只有访问类的静态变量才会触发该类的初始化流程而不是其他类型的变量。
关于Java助记符如果将上面一个示例中的常量修改为不同的值会生成不同的助记符比如
// bipush 20
static int a 20;
// 3: sipush 130
static int a 130
// 3: ldc #4 // int 327670
static int a 327670;其中 iconst_n将int类型数字n推送至栈顶n取值0~5 lconst_n将long类型数字n推送至栈顶n取值0,1类似的还有fconst_n、dconst_n bipush将单字节的常量值(-128~127) 推送至栈顶 sipush将一个短整类型常量值(-32768~32767) 推送至栈顶 ldc将int、float或String类型常量值从常量池中推送至栈顶 再看下一个实例
public class Demo {public static void main(String[] args) {System.out.println(Bird.a);}
}class Bird {static final String a UUID.randomUUID().toString();static {System.out.println(bird init);}
}执行后会输出
bird init
d01308ed-8b35-484c-b440-04ce3ecb7c0e在本例中常量a的值在编译时不能确定需要进行方法调用这种情况下编译后会产生getstatic指令同样会触发类的初始化所以才会输出bird init。看下反编译字节码后的代码
// 已省略部分无关代码
public static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: getstatic #3 // Field com/hicsc/classloader/Bird.a:Ljava/lang/String;6: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V9: returninvokestatic
调用类的静态方法时也会触发该类的初始化。比如
public class Demo {public static void main(String[] args) {Bird.fly();}
}class Bird {static {System.out.println(bird init);}static void fly() {System.out.println(bird fly);}
}执行后会输出
bird init
bird fly通过本例可以证明调用类的静态方法确实会触发类的初始化。
2. 反射调用时
使用java.lang.reflect包的方法对类型进行反射调用的时候如果类型没有进行过初始化则需要先触发其初始化。来看下面的例子
ublic class Demo {public static void main(String[] args) throws Exception {ClassLoader loader ClassLoader.getSystemClassLoader();Class clazz loader.loadClass(com.hicsc.classloader.Bird);System.out.println(clazz);System.out.println(——————);clazz Class.forName(com.hicsc.classloader.Bird);System.out.println(clazz);}
}class Bird {static {System.out.println(bird init);}
}执行后输出结果
class com.hicsc.classloader.Bird
------------
bird init
class com.hicsc.classloader.Bird本例中调用ClassLoader方法load一个类并不会触发该类的初始化而使用反射包中的forName方法则触发了类的初始化。
3. 初始化子类时
当初始化类的时候如果发现其父类还没有进行过初始化则需要先触发其父类的初始化。比如
public class Demo {public static void main(String[] args) throws Exception {Pigeon.fly();}
}class Bird {static {System.out.println(bird init);}
}class Pigeon extends Bird {static {System.out.println(pigeon init);}static void fly() {System.out.println(pigeon fly);}
}执行后输出
bird init
pigeon init
pigeon fly本例中在main方法调用Pigeon类的静态方法最先初始化的是父类Bird然后才是子类Pigeon。因此在类初始化时如果发现其父类并未初始化则会先触发父类的初始化。
对子类调用父类中存在的静态方法只会触发父类初始化而不会触发子类的初始化。
看下面的例子可以先猜猜运行结果
public class Demo {public static void main(String[] args) {Pigeon.fly();}
}class Bird {static {System.out.println(bird init);}static void fly() {System.out.println(bird fly);}
}class Pigeon extends Bird {static {System.out.println(pigeon init);}
}输出
bird init
bird fly本例中由于fly方法是定义在父类中那么方法的拥有者就是父类因而使用Pigeno.fly()并不是表示对子类的主动引用而是表示对父类的主动引用所以只会触发父类的初始化。
4. 遇到启动类时
当虚拟机启动时如果一个类被标记为启动类(即包含main方法虚拟机会先初始化这个主类。比如
public class Demo {static {System.out.println(main init);}public static void main(String[] args) throws Exception {Bird.fly();}
}class Bird {static {System.out.println(bird init);}static void fly() {System.out.println(bird fly);}
}执行后输出
main init
bird init
bird fly5. 实现带有默认方法的接口的类被初始化时
当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法) 时如果有这个接口的实现类发生了初始化那该接口要在其之前被初始化。
由于接口中没有static{}代码块怎么判断一个接口是否初始化来看下面这个例子
public class Demo {public static void main(String[] args) throws Exception {Pigeon pigeon new Pigeon();}
}interface Bird {// 如果接口被初始化,那么这句代码一定会执行// 那么Intf类的静态代码块一定会被执行public static Intf intf new Intf();default void fly() {System.out.println(bird fly);}
}class Pigeon implements Bird {static {System.out.println(pigeon init);}
}class Intf {{System.out.println(interface init);}
}执行后输出
interface init
pigeon init可知接口确实已被初始化如果把接口中的default方法去掉那么不会输出interface init即接口未被初始化。
6. 使用JDK7新加入的动态语言支持时
当使用JDK7新加入的动态类型语言支持时如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄并且这个方法句柄对应的类没有进行过初始化则需要先触发其初始化。
简单点来说当初次调用MethodHandle实例时如果其指向的方法所在类没有进行过初始化则需要先触发其初始化。
什么是动态类型语言
动态类型语言的关键特性是它的类型检查的主体过程是在运行期进行的常见的语言比如JavaScript、PHP、Python等相对地在编译期进行类型检查过程的语言就是静态类型语言比如 Java 和 C# 等。简单来说对于动态类型语言变量是没有类型的变量的值才具有类型在编译时编译器最多只能确定方法的名称、参数、返回值这些而不会去确认方法返回的具体类型以及参数类型。而Java等静态类型语言则不同你定义了一个整型的变量x那么x的值也只能是整型而不能是其他的编译器在编译过程中就会坚持定义变量的类型与值的类型是否一致不一致编译就不能通过。因此「变量无类型而变量值才有类型」是动态类型语言的一个核心特征。
关于MethodHandle与反射的区别可以参考周志明著「深入理解Java虚拟机」第8.4.3小节这里引用部分内容方便理解。 Reflection 和 MethodHandle 机制本质上都是在模拟方法调用但是 Reflection 是在模拟 Java 代码层次的方法调用而 MethodHandle 是在模拟字节码层次的方法调用。反射中的 Method 对象包含了方法签名、描述符以及方法属性列表、执行权限等各种信息而 MethodHandle 仅包含执行该方法的相关信息通俗来讲Reflection 是重量级而 MethodHandle 是轻量级。 总的来说反射是为 Java 语言服务的而 MethodHandle 则可为所有 Java 虚拟机上的语言提供服务。
来看一个简单的示例
public class Demo {public static void main(String[] args) throws Exception {new Pigeon().fly();}
}class Bird {static {System.out.println(bird init);}static void fly() {System.out.println(bird fly);}
}class Pigeon {void fly() {try {MethodHandles.Lookup lookup MethodHandles.lookup();// MethodType.methodType 方法的第一个参数是返回值// 然后按照目标方法接收的参数的顺序填写参数类型// Bird.fly() 方法返回值是空, 没有参数MethodType type MethodType.methodType(void.class);MethodHandle handle lookup.findStatic(Bird.class, fly, type);handle.invoke();} catch (Throwable a) {a.printStackTrace();}}
}在Pigeon类中使用MethodHandle来调用Bird类中的静态方法fly按照前面所述初次调用MethodHandle实例时如果其指向的方法所在类没有进行过初始化则需要先触发其初始化。所以这里一定会执行Bird类中的静态代码块。而最终的运行结果也与我们预计的一致
bird init
bird fly虚拟机如何加载类 - 类的加载过程
类的加载全过程包括加载、验证、准备、解析和初始化 5 个阶段是一个非常复杂的过程。 加载 Loading
Loading 阶段主要是找到类的class文件并把文件中的二进制字节流读取到内存然后在内存中创建一个java.lang.Class对象。
加载完成后就进入连接阶段但需要注意的是加载阶段与连接阶段的部分动作如一部分字节码文件格式验证动作是交叉进行的加载阶段尚未完成连接阶段可能已经开始但这些夹在加载阶段之中进行的动作仍然属于连接阶段的一部分这两个阶段的开始时间仍然保持着固定的先后顺序也就是只有加载阶段开始后才有可能进入连接阶段。
验证 Verification
验证是连接阶段的首个步骤其目的是确保被加载的类的正确性即要确保加载的字节流信息要符合《Java虚拟机规范》的全部约束要求确保这些信息被当做代码运行后不会危害虚拟机自身的安全。
其实Java 代码在编译过程中已经做了很多安全检查工作比如不能将一个对象转型为它未实现的类型、不能使用未初始化的变量(赋值除外)、不能跳转到不存在的代码行等等。但 JVM 仍要对这些操作作验证这是因为 Class 文件并不一定是由 Java 源码编译而来甚至你都可以通过键盘自己敲出来。如果 JVM 不作校验的话很可能就会因为加载了错误或有恶意的字节流而导致整个系统受到攻击或崩溃。所以验证字节码也是 JVM 保护自身的一项必要措施。
整个验证阶段包含对文件格式、元数据、字节码、符号引用等信息的验证。
准备 Preparation
这一阶段主要是为类的静态变量分配内存并将其初始化为默认值。这里有两点需要注意
仅为类的静态变量分配内存并初始化并不包含实例变量初始化为默认值比如int为0引用类型初始化为null
需要注意的是准备阶段的主要目的并不是为了初始化而是为了为静态变量分配内存然后再填充一个初始值而已。就比如
// 在准备阶段是把静态类型初始化为 0即默认值
// 在初始化阶段才会把 a 的值赋为 1
public static int a 1;来看一个实例加深印象可以先考虑一下运行结果。
public class StaticVariableLoadOrder {public static void main(String[] args) {Singleton singleton Singleton.getInstance();System.out.println(counter1: Singleton.counter1);System.out.println(counter2: Singleton.counter2);}
}class Singleton {public static Singleton instance new Singleton();private Singleton() {counter1;counter2;System.out.println(构造方法里counter1: counter1 , counter2: counter2);}public static int counter1;public static int counter2 0;public static Singleton getInstance() {return instance;}
}其运行结果是
构造方法里counter1:1, counter2:1
counter1:1
counter2:0在准备阶段counter1和counter2都被初始化为默认值0因此在构造方法中自增后它们的值都变为1然后继续执行初始化仅为counter2赋值为0counter1的值不变。
如果你理解了这段代码再看下面这个例子想想会输出什么
// main 方法所在类的代码不变
// 修改了 counter1 的位置并为其初始化为 1
class Singleton {public static int counter1 1;public static Singleton instance new Singleton();private Singleton() {counter1;counter2;System.out.println(构造方法里counter1: counter1 , counter2: counter2);}public static int counter2 0;public static Singleton getInstance() {return instance;}
}运行后输出
构造方法里counter1:2, counter2:1
counter1:2
counter2:0counter2并没有任何变化为什么counter1的值会变成2其实是因为类在初始化的时候是按照代码的顺序来的就比如上面的示例中为counter1赋值以及执行构造方法都是在初始化阶段执行的但谁先谁后呢按照顺序来因此在执行构造方法时counter1已经被赋值为1执行自增后自然就变为2了。
解析 Resolution
解析阶段是将常量池类的符号引用替换为直接引用的过程。在编译时Java 类并不知道所引用的类的实际地址只能使用符号引用来代替。符号引用存储在class文件的常量池中比如类和接口的全限定名、类引用、方法引用以及成员变量引用等如果要使用这些类和方法就需要把它们转化为 JVM 可以直接获取的内存地址或指针即直接引用。
因此解析的动作主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这 7 类符号引用进行的。
初始化 Initialization
在准备阶段我们只是给静态变量设置了类似0的初值在这一阶段则会根据我们的代码逻辑去初始化类变量和其他资源。
更直观的说初始化过程就是执行类构造器clinit方法的过程。
类的初始化是类加载过程的最后一个步骤直到这一个步骤JVM 才真正开始执行类中编写的 Java 代码。初始化完也就差不多是类加载的全过程了什么时候需要初始化也就是我们最前面讲到的几种情况。
类初始化是懒惰的不会导致类初始化的情况也就是前面讲到的被动引用类型再讲全一点
访问类的 static final 静态常量基本类型和字符串不会触发初始化访问类对象.class不会触发初始化创建该类的数组不会触发初始化执行类加载器的 loadClass 方法不会触发初始化Class.forName(反射)的参数2为false时(为true才会初始化)
在编译生成class文件时编译器会产生两个方法加于class文件中一个是类的初始化方法clinit, 另一个是实例的初始化方法init。
1. 类初始化方法clinit()
Java 编译器在编译过程中会自动收集类中所有静态变量赋值语句、静态代码块中的语句将其合并到类构造器clinit()方法收集的顺序由源代码文件中出现的顺序决定。类初始化方法一般在类初始化阶段执行。如果两个类存在父子关系那么在执行子类的clinit()方法之前会确保父类的方法已执行完毕因此父类的静态代码块会优先于子类的静态代码块。
例子
public class ClassDemo {static {i 20;}static int i 10;static {i 30;}// init 方法收集后里面的代码就是这个当然你是看不到该方法的init() {i 20;i 10;i 30;}
}clinit()方法不需要显示调用类解析完了会立即调用且父类的clinit()永远比子类的先执行因此在jvm中第一个执行的肯定是Object中的clinit()方法。clinit()方法不是必须的如果没有静态代码块和变量赋值就没有接口也有变量复制操作因此也会生成clinit()但是只有当父接口中定义的变量被使用时才会初始化。
这里有一点需要特别强调JVM 会保证一个类的clinit()方法在多线程环境中被正确的加锁同步如果多个线程同时去初始化一个类那么只会有其中一个线程去执行这个类的clinit()方法其它线程都需要等待直到clinit()方法执行完毕。如果在一个类的clinit()方法中有耗时很长的操作那么可能会造成多个线程阻塞在实际应用中这种阻塞往往是很隐蔽的。因此在实际开发过程中我们都会强调不要在类的构造方法中加入过多的业务逻辑甚至是一些非常耗时的操作。
另外静态代码块中只能访问定义它之前的变量定义在它之后的变量可以赋值但不能访问
class Class{static {c 2; // 赋值操作可以正常编译通过System.out.println(c);//编译器提示 Illegal forward reference非法向前引用}static int c 1;
}2. 对象初始化方法init()
init()是实例对象自动生成的方法。编译器会按照从上至下的顺序收集 「类成员变量」 的赋值语句、普通代码块最后收集构造函数的代码最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。
例子
public class ClassDemo {int a 1;{a 2;System.out.println(2);}{b b2;System.out.println(b2);}String b b1;public ClassDemo(int a, String b) {System.out.println(构造器赋值前this.a this.b);this.a a;this.b b;}public static void main(String[] args) {ClassDemo demo new ClassDemo(3, b3);System.out.println(构造结束后demo.a demo.b);
// 2
// b2
// 构造器赋值前2 b1
// 构造结束后3 b3}
}上面的代码的init()方法实际为
public init(int a, String b){super(); // 不要忘记在底层还会加上父类的构造方法this.a 1;this.a 2;System.out.println(2);this.b b2;System.out.println(b2);this.b b1;System.out.println(构造器赋值前 this.a this.b); // 构造方法在最后this.a a;this.b b;
}类执行过程小结
确定类变量的初始值。在类加载的准备阶段JVM 会为「类变量」初始化默认值这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量则直接会被初始成用户想要的值。初始化入口方法。当进入类加载的初始化阶段后JVM 会寻找整个 main 方法入口从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时会首先初始化类构造器之后初始化对象构造器。初始化类构造器。JVM 会按顺序收集「类变量」的赋值语句、静态代码块将它们组成类构造器最终由 JVM 执行。初始化对象构造器。JVM 会按顺序收集「类成员变量」的赋值语句、普通代码块最后收集构造方法将它们组成对象构造器最终由 JVM 执行。
如果在初始化 「类变量」时类变量是一个其他类的对象引用那么就先加载对应的类然后实例化该类对象再继续初始化其他类变量。 参考
深入理解JVM类加载机制jvm深入理解类加载机制