Javac 常量传播对类初始化的影响
一、Java Compiler 虚拟机运行java类时,需要将类进行初始化,一般来说,java代码在虚拟机中执行,至少需要2次编译(至少的原因是,”解释器+JIT“可能反复动态优化编译)。Javac是java compiler在编译时优化代码编译器。 javac优化手动实际上是相对保守的:基本上是简单的常量传播、无用代码擦除等有限手段。 二、类的加载与卸载过程 类的加载过程: 加载->验证->准备->解析->初始化->使用 触发条件: (不考虑JIT分支预测):new对象(实际是invokespecial),getstatic/putstatic(读写类中的非常量成员 + 读final修饰的引用类型,注意:引用类型不包括字面量字符串),invokestatic 反射调用 类的卸载过程: 卸载(GC 回收:常量符号引用解除、内存回收) 触发条件(同时满足以下三条): 不存在类的静态引用类型成员字段和方法没有被其他类引用 不存在类的实例 类的classloader已被销毁 三、类的加载案例 package com.apptest; public class Base { static { System.out.println("Base init"); } public static final int A = 123; public static int B = 123; } package com.apptest; public class Child extends Base { static { System.out.println("Child init"); } } 测试代码 package com.apptest; public class ClassLoadTest { public static void main(String[] args) { System.out.println("Child.A="+ Child.A +" :::不会触发基类加载,不会触发子类加载"); System.out.println("Child.B="+ Child.B +" :::会触发基类加载,结果在本段之前执行,不会触发子类加载"); Child[] ch = null; System.out.println("Child[] ch=null;" + " :::不会触发子类加载"); ch = new Child[1]; System.out.println("ch = new Child[1]: "+" :::不会触发子类加载"); System.out.println("Child[].class基类: "+Child[].class.getSuperclass()); System.out.println("Child.class="+Child.class +" :::不会触发子类加载"); Child c = new Child(); //子类正式加载 } } 运行结果如下 Child.A=123 :::不会触发基类加载,不会触发子类加载 Base init Child.B=123 :::会触发基类加载,结果在本段之前执行,不会触发子类加载 Child[] ch=null; :::不会触发子类加载 ch = new Child[1]: :::不会触发子类加载 Child[].class基类: class java.lang.Object Child.class=class com.apptest.Child :::不会触发子类加载 Child init 【1】 Child.A 不仅没有触发子类加载,连父类也没加载,原因是因为A虽然是成员,但是Javac会根据final修饰符和A变量指向常量两个原因,对代码段扫描替换为常量符号引用,进行常量传播优化。 【"Child.A="+ Child.A +" :::不会触发基类加载,不会触发子类加载" 】 编译后会是 【"Child.A=123 :::不会触发基类加载,不会触发子类加载"】 如果A指向的不是常量,是会触发父类加载的,比如将A做如下修改 public static final String A = new String("AA"); 【2】Child.B 触发了父类初始化,但没有触发子类初始化,主要原因是,虽然B成员指向常量,但因为程序运行不确定性,可能被其他线程修改,因此无法进行常量传播。 Child.B字符串代码段会被StringBuilder通过append进行连接,而读取Child.B 使用的getstatic,会触发对父类的加载 GETSTATIC com/apptest/Child.B : I 为什么只触发的是父类的初始化,主要原因是父类和子类的静态成员包括方法,在子类中如果没有进行覆盖,直接指向父类,而GETSTATIC促使Child类只是被加载了,但并没有初始化。 【3】 Child[] ch = null; 因为是无用代码,会被javac擦除,因此可以直接不用讨论 【4】new Child[]; 虽然是Child数组,但Java中,数组的夫类是Object,而Child[].class本身和Chil.class是两个类,不需要讨论 【5】Child.class 这个相比有些特殊,Child.class是比较特殊的静态常量,同样也是只读(final),但是Child.class实际上是Child类的实例化,类本身是抽象概念 ,而Child.class只是Child类中的一个常量,实际使用是Child child = new Child(); 而不是Child.class child = new Child.class[]; 此外,在java class header中,它的classpoint也不是指向Child的,因此和类只是描述关系。 我们可以查看类文件信息 Last modified 2021-4-17; size 473 bytes MD5 checksum f74b5478553ce99742e1bdf9ef84a625 Compiled from "Child.java" public class com.apptest.Child extends com.apptest.Base minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#17 // com/apptest/Base."<init>":()V #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #20 // Child init #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #23 // com/apptest/Child #6 = Class #24 // com/apptest/Base #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/apptest/Child; #14 = Utf8 <clinit> #15 = Utf8 SourceFile #16 = Utf8 Child.java #17 = NameAndType #7:#8 // "<init>":()V #18 = Class #25 // java/lang/System #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #20 = Utf8 Child init #21 = Class #28 // java/io/PrintStream #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V #23 = Utf8 com/apptest/Child #24 = Utf8 com/apptest/Base #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Utf8 java/io/PrintStream #29 = Utf8 println #30 = Utf8 (Ljava/lang/String;)V { public com.apptest.Child(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method com/apptest/Base."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/apptest/Child; static {}; descriptor: ()V flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Child init 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 } SourceFile: "Child.java" 从#5我们知道了Child.class是Child类的常量。 此外,我们还有一种方式来证明,Child.class和Child指向的父类并不是同一个 System.out.println(ClassLayout.parseInstance(Child.class).toPrintable()); System.out.println(ClassLayout.parseInstance(new Child()).toPrintable()); System.out.println(ClassLayout.parseInstance(new Child()).toPrintable()); ava.lang.Class object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) bf 03 00 f8 (10111111 00000011 00000000 11111000) (-134216769) 12 4 java.lang.reflect.Constructor Class.cachedConstructor null 16 4 java.lang.Class Class.newInstanceCallerCache null 20 4 java.lang.String Class.name (object) 24 4 (alignment/padding gap) 28 4 java.lang.ref.SoftReference Class.reflectionData null 32 4 sun.reflect.generics.repository.ClassRepository Class.genericInfo null 36 4 java.lang.Object[] Class.enumConstants null 40 4 java.util.Map Class.enumConstantDirectory null 44 4 java.lang.Class.AnnotationData Class.annotationData null 48 4 sun.reflect.annotation.AnnotationType Class.annotationType null 52 4 java.lang.ClassValue.ClassValueMap Class.classValueMap null 56 32 (alignment/padding gap) 88 4 int Class.classRedefinedCount 0 92 404 (loss due to the next object alignment) Instance size: 496 bytes Space losses: 36 bytes internal + 404 bytes external = 440 bytes total Child init com.apptest.Child object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total com.apptest.Child object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 由结果可知,后面2个实例的class point(-134168059)和Child.class class point是不同的 【6】正常逻辑,使用new触发了invokespecial,而invokespecial执行的前提是父类必须初始化。 四、类的加载过程前置 我们从第二个分段“类的加载与卸载过程”中知道,触发类的加载给人的错觉是字节码必须执行到指定的位置,实际上“解释器+JIT”可能会进行分支预测、逃逸分析等,对类提前加载,注意:这个加载会停留在初始化阶段之前。 为了证明这个行为,我们利用ClassLoader进行Hook public class DelegateClassLoader extends URLClassLoader { private ClassLoader mPathClassLoader; private DelegateClassLoader(String dexPath,ClassLoader parentClassLoader) { super(new URL[0],parentClassLoader); mPathClassLoader = getClass().getClassLoader(); } //PathClassloader.parent <- PathClassloader ====> PathClassloader.parent <- Hook ClassLoader <- PathClassloader public static synchronized void hook(ClassLoader pathClassLoader) throws NoSuchFieldException, IllegalAccessException { ClassLoader classLoader = new DelegateClassLoader("",pathClassLoader.getParent()); Field parentField = ClassLoader.class.getDeclaredField("parent"); parentField.setAccessible(true); parentField.set(pathClassLoader,classLoader); Thread.currentThread().setContextClassLoader(classLoader); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { return super.loadClass(name, resolve); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { System.out.println("HOOK " + name ); return super.findClass(name); } } 测试 public class ClassLoadTest { public static void main(String[] args) { try { DelegateClassLoader.hook(ClassLoadTest.class.getClassLoader()); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } System.out.println("-------------------"); System.out.println("" + Child.B); System.out.println("-------------------"); System.out.println("" + new Child()); } } 执行结果如下 ------------------- HOOK com.apptest.Child HOOK com.apptest.Base Base init 123 ------------------- Child init com.apptest.Child@1540e19d