一、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