Javac 常量传播对类初始化的影响

一、Java Compiler

虚拟机运行java类时,需要将类进行初始化,一般来说,java代码在虚拟机中执行,至少需要2次编译(原因是Javac编译后,还要经过”解释器+JIT“可能反复动态优化编译)。Javac是java compiler在编译时优化代码编译器。

javac优化手动实际上是相对保守的:基本上是 简单的常量 传播、无用代码擦除等有限手段。

二、类的加载与卸载过程

类的加载过程:

加载->验证->准备->解析->初始化->使用

触发条件:

  • (不考虑JIT分支预测):new对象(实际是invoke-special,在android中是invoke-direct)、getstatic/putstatic 读写类中的非常量成员 (类方法或static变量)或读final static修饰的引用类型,注意:引用类型不包括字面量字符串),invoke-static
  • 反射调用

类的卸载过程:

卸载(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");
    }
}

测试代码【1】

package com.apptest;

public class ClassLoadTest {

    public static void main(String[] args) {


        Child a;
        System.out.println("Child a;" + " :::不会触发子类和父类的初始化");
        System.out.println("Child.A="+ Child.A +"  :::不会触发基类和子类初始化");
        System.out.println("Child.B="+ Child.B +" :::会触发基类初始化,不会触发子类不初始化");

    }
}

运行结果如下

Child a; :::不会触发子类和父类的初始化
Child.A=A  :::不会触发基类和子类初始化
Base init
Child.B=123 :::会触发基类初始化,不会触发子类初始化

测试代码二:

ublic class ClassLoadTest {

    public static void main(String[] args) {

 

        Child[] ch = null;
        System.out.println("Child[] ch=null;" + " :::不会触发基类、子类初始化");
        Class<? super Child[]> cls = Child[].class;
        System.out.println("Child[].class: 不会触发基类、子类的初始化");
        ch = new Child[0];
        System.out.println("ch = new Child[0]: "+" :::不会触发子类、基类的初始化");
        System.out.println("Child.class="+Child.class +" :::不会触发子类、基类的初始化");
        Child c = new Child(); //子类正式初始化


    }


}

运行结果

Child[] ch=null; :::不会触发基类、子类初始化
Child[].class: 不会触发基类、子类的初始化
ch = new Child[0]:  :::不会触发子类、基类的初始化
Child.class=class com.apptest.Child :::不会触发子类、基类的初始化
Base init
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成员指向常量,但是B属于静态成员,改静态成员属于Base类,但他并不属于子类。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值