Java类加载过程

2021.02.11 15:02 465
阅读约 3 分钟

类加载阶段

加载

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
    • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中 可以通过前面介绍的 HSDB 工具

链接

验证

验证类是否符合 JVM规范,安全性检查 

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/HelloWorld
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

准备

  • 为 static 变量分配空间,设置默认值
  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成 如果
  • static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶 段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成将常量池中的符号引用解析为直接引用
package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException,
IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}

解析

将常量池中的符号引用解析为直接引用 

package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {
	public static void main(String[] args) throws ClassNotFoundException,
		IOException {
		ClassLoader classloader = Load2.class.getClassLoader();
		// loadClass 方法不会导致类的解析和初始化
		Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
		// new C();
		System.in.read();
	}
}
class C {
	D d = new D();
}
class D {
}

初始化

<cinit>()V 方法

初始化即调用<cinit> ()V ,虚拟机会保证这个类的『构造方法』的线程安全 

发生的时机 

概括得说,类初始化是【懒惰的】 

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况 

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadclass方法
  • Class.forName的参数2为false时

类加载器

名称 加载哪的类说明
Bootstrap ClassLoader   JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
 Application ClassLoader classpath上级为 Extension
自定义类加载器自定义上级为 Applicatio

使用下面的命令指定类加载器为Bootstrap 

java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5_1
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以用这个办法替换核心类
    • java -Xbootclasspath: java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>

运行期优化

即时编译

分层编译(TieredCompilation)

尝试下面的例子

public class JIT1 {
public static void main(String[] args) {
	for (int i = 0; i < 200; i++) {
		long start = System.nanoTime();
		for (int j = 0; j < 1000; j++) {
		new Object();
	}
		long end = System.nanoTime();
		System.out.printf("%d\t%d\n",i,(end - start));
	  }
	}
}

当执行多次后,会发现那创建对象的时间开销明显降低,原因是什么呢?
JVM 将执行状态分成了 5 个层次:

 0 层,解释执行(Interpreter) 

  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
  • 4 层,使用 C2 即时编译器编译执

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等 

即时编译器(JIT)与解释器的区别 

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需 再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运 行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速 度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),优化之

方法内联

private static int square(final int i) { 
	return i * i; 
}
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:

System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化

反射优化

package cn.itcast.jvm.t3.reflect;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Reflect1 {
	public static void foo() {
		System.out.println("foo...");
	}
	public static void main(String[] args) throws Exception {
			Method foo = Reflect1.class.getMethod("foo");
		for (int i = 0; i <= 16; i++) {
			System.out.printf("%d\t", i);
			foo.invoke(null);
		}
			System.in.read();
	}
}

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 sun.reflect.NativeMethodAccessorImpl实现 

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1 

可以使用阿里的 arthas 工具: 

java -jar arthas-boot.jar

选择pid,输入序号回车即可

反编译字节码

jad sun.reflect.GeneratedMethodAccessor1

可以看到类中反射调用方法已经被优化为静态类调用

package sun.reflect;

import cn.itcast.jvm.t3.reflect.Reflect1;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor1
extends MethodAccessorImpl {
    /*
     * Loose catch block
     * Enabled aggressive block sorting
     * Enabled unnecessary exception pruning
     * Enabled aggressive exception aggregation
     * Lifted jumps to return sites
     */
    public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
        block4: {
            if (arrobject == null || arrobject.length == 0) break block4;
            throw new IllegalArgumentException();
        }
        try {
            Reflect1.foo();
            return null;
        }
        catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        }
        catch (ClassCastException | NullPointerException runtimeException) {
            throw new IllegalArgumentException(super.toString());
        }
    }
}

注意 通过查看 ReflectionFactory 源码可知 

sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首 次生成比较耗时,如果仅反射调用一次,不划算) sun.reflect.inflationThreshold 可以修改膨胀阈值

 

字数:579 发布于 7 个月前
Copyright 2018-2021 Siques