JVM第十四篇(类加载与字节码技术五)

JVM第十四篇(类加载与字节码技术五),第1张

JVM第十四篇(类加载与字节码技术五)

类加载
  • 类加载阶段
    • 加载
    • 链接
    • 初始化
  • 类加载器
    • Bootstrap 启动类加载器
    • 扩展类加载器
    • 双亲委派模式
    • 线程上下文类加载器
    • 自定义类加载器
  • 运行期优化
    • 分层编译
    • 方法内联
    • 字段优化
    • 反射优化

类加载阶段

在类加载阶段分为三个阶段,分别是加载,链接,初始化

加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类。
它的重要 field 有:
_java_mirror ,即 java 的类镜像,例如对 String 来说,就是 String.class。
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表

如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的

instanceKlass 这样的【元数据】是存储在方法区(JDK1.8 后的元空间内),但 _java_mirror
是存储在堆中。

在元空间中存放 instanceKlass,_java_mirror是存储在堆中,在 instanceKlass中 存放着 _java_mirror 的引用,在堆中_java_mirror 存储着 instanceKlass的引用,在堆中的对象通过有 两个Person对象,Person对象通过 Person.class找到 instanceKlass,获取类信息。

链接

验证类是否符合 JVM规范,安全性检查
准备阶段
为 static 变量分配空间,设置默认值。
static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾。
如图,static变量存放在 _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 {
}

类的加载是懒惰式的,没有使用到的类不会进行加载,解析和初始化。
在程序中,加载了类 C ,但是没有使用类 C,在类C的加载中,不会进行类C的解析和初始化,在类C使用了类D,类D也不会进行加载,解析和初始化。此时,在类C的常量池中,类D只是一个符号引用

如果将 程序中的 new C(); 加上,就会将类C进行解析和初始化,因为 类C中new了类D,同时将类D进行解析和初始化,因此在类C中对D的符号引用变为直接引用了。

初始化

cinit()V 方法
初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全。
类初始化是懒惰的
发生初始化的情况:

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

不会导致类初始化的情况:

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

类加载器的层级关系(以JDK8为例)

Bootstrap 启动类加载器,Extension 扩展类加载器,Application 应用类加载器。Bootstrap 类加载器是不能直接访问,因为Bootstrap是C++代码编写的,不支持java代码直接访问。

Bootstrap 启动类加载器

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load;
public class F {
	static {
		System.out.println("bootstrap F init");
	}
}
package cn.itcast.jvm.t3.load;
public class Load5_1 {
	public static void main(String[] args) throws ClassNotFoundException {
		Class aClass = Class.forName("cn.itcast.jvm.t3.load.F");
		System.out.println(aClass.getClassLoader());
	}
}

如果在编译器中直接执行程序,可以看出F类的默认加载器是ApplicationClassLoader。
在 命令行中加入 -Xbootclasspath 参数,将 当前类的路径 加入到启动路径,F类将会由Bootstrap 加载。

java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
bootstrap F init
null

在程序中,输出的类加载器为null,因为 Bootstrap 类加载器 是不能由 java代码直接访问的。

扩展类加载器

Extension ClassLoader扩展类加载器加载 JAVA_HOME/jre/lib/ext 路径下的类。

package cn.itcast.jvm.t3.load;
public class G {
	static {
		System.out.println("ext G init");
	}
}
public class Load5_2 {
	public static void main(String[] args) throws ClassNotFoundException {
		Class aClass = Class.forName("cn.itcast.jvm.t3.load.G");
		System.out.println(aClass.getClassLoader());
	}
}

上述程序中,如果直接执行,则得到 加载G类的类加载器为sun.misc.Launcher A p p C l a s s L o a d e r @ 18 b 4 a a c 2 , 为 应 用 类 加 载 器 。 将 当 前 的 类 打 为 j a r 包 , 然 后 放 在 J A V A H O M E / j r e / l i b / e x t 路 径 下 , 再 次 执 行 程 序 , 则 输 出 结 果 为 : s u n . m i s c . L a u n c h e r AppClassLoader@18b4aac2,为应用类加载器。 将当前的类打为jar包,然后放在JAVA_HOME/jre/lib/ext 路径下,再次执行程序,则输出结果为: sun.misc.Launcher AppClassLoader@18b4aac2,为应用类加载器。将当前的类打为jar包,然后放在JAVAH​OME/jre/lib/ext路径下,再次执行程序,则输出结果为:sun.misc.LauncherExtClassLoader@29453f44,此时为扩展类加载器。

双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
在进行加载类的时候,首先查看层次高的加载器,有没有加载过该类,若没有加载过,本级的类加载器才进行加载,若本级的类加载器没有找到类,则抛出 ClassNotFound异常。
源码分析:

protected Class loadClass(String name, boolean resolve)throws  ClassNotFoundException{
	synchronized (getClassLoadingLock(name)) {
	// 1. 检查该类是否已经加载,在已经加载过的类中寻找。
	Class c = findLoadedClass(name);
	if (c == null) {	
		long t0 = System.nanoTime();
		try {
			if (parent != null) {
				// 2. 有上级的话,委派上级 loadClass
				c = parent.loadClass(name, false);
			} else {
				// 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
				c = findBootstrapClassOrNull(name);
			}
		} catch (ClassNotFoundException e) {
		} 
		if (c == null) {
			long t1 = System.nanoTime();
			// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
			c = findClass(name);
			// 5. 记录耗时
			sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
			sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
			sun.misc.PerfCounter.getFindClasses().increment();
		}
	} 
	if (resolve) {
		resolveClass(c);
	} 
	return c;
	}
}

源码总结,类加载过程:自定义类加载器委派—>应用类加载器委派—>扩展类加载器—>启动类加载器。启动类加载器找不到类 —>扩展类加载器找不到类—>应用类加载器找不到类—>自定义类加载器找不到—> ClassNotFound 异常。

线程上下文类加载器

线程上下问类加载器出现的原因:
越基础的类由越上层的加载器进行加载,如果基础类又要调用回用户的代码,那该怎么办?
解决方案:使用“线程上下文类加载器”

这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作(即,父类加载器加载的类,使用线程上下文加载器去加载其无法加载的类),这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。

SPI (Service Provider Interface ———— 服务提供者接口)
Java中所有涉及SPI的加载动作基本上都采用这种方式

JDBC 使用伪代码:

Class.forName("com.mysql.driver.Driver");
Connection conn = Driver.getConnection();
Statement st = conn.getStatement();

JDBC 是一个标准。不同的数据库厂商会根据这个标准,有它们自己的实现。JDBC 的接口,存在于 JDK 中了。因此,JDBC 相关的这些接口,在启动的时候,是由启动类加载器加载的。
通常,数据库厂商提供的 jar 包放置在 classPath 下,由此可知,数据库厂商所提供的实现类不会由启动类加载器来去加载,它们通常是由应用类加载器来去加载的。
接口是有启动类加载器加载的,而具体的实现是由应用类加载器加载的。根据类的双亲委托原则,父加载器所加载的类/接口是看不到子加载器所加载的类/接口的,而然,子加载器所加载的类/接口是能够看到父加载器的类/接口的。这样的话,会导致这样一个局面:JDBC 相关的代码可能还需要去调用具体实现类中的代码,但是它是无法看到具体的实现类的。

Thread 中的 getContextClassLoader() 与 setContextClassLoader(ClassLoader cl) 分别用来获取和设置上下文类加载器。Java 应用运行时的初始线程的上下文类加载器是应用类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。
父ClassLoader 可以使用当前线程 Thread.currentThread().getContextClassLoader() 所指定的 classloader 加载的类。这就改变了父ClassLoader 不能使用子ClassLoader 或是其他没有直接父子关系的 ClassLoader 加载的类的情况,即,改变了双亲委托模型。

在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于 SPI 来说,有些接口是 Java 核心库所提供的,而 Java 核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的 jar 包,Java 的启动类加载器是不会加载其他来源的 jar 包,这样传统的双亲委托模型就无法满足 SPI 的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

ContextClassLoader 的作用就是为了破坏 Java 的类加载委托机制。
当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的 ClassLoader 找到并加载该类。

参考作者:tomas家的小拨浪鼓

通过JDBC来观察ContextClassLoader :
下面追踪代码,观察 ContextClassLoader
在 class DriverManager 中的静态代码块中的loadInitialDrivers(); 方法中进行实现类的加载。

static {
 	loadInitialDrivers();
 	println("JDBC DriverManager initialized");
}

在loadInitialDrivers 方法中,使用 ServiceLoader 机制加载驱动,即 SPI

// 在 load方法中进行实现类的加载
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
	driversIterator.next();
}

load方法

public static  ServiceLoader load(Class service) {
	// 获取线程上下文类加载器,默认为应用类加载器
	ClassLoader cl = Thread.currentThread().getContextClassLoader();
	return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由
Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类
LazyIterator 中的 nextService 方法:

// loader就是 cl 
c = Class.forName(cn, false, loader);

在loadInitialDrivers 方法中,还使用 jdbc.drivers 定义的驱动名加载驱动

println("DriverManager.Initialize: loading " + aDriver);
// getSystemClassLoader 获得的类加载器就是应用类加载器
Class.forName(aDriver,true,ClassLoader.getSystemClassLoader());
自定义类加载器

什么时候需要自定义类加载器?

  1. 想加载非 classpath 随意路径中的类文件
  2. 都是通过接口来使用实现,希望解耦时,常用在框架设计
  3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法,
    注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

示例程序

自定义类加载器,加载指定目录下的类文件 .class 文件

class MyClassLoader extends ClassLoader {
    @Override // name 就是类名称
    protected Class findClass(String name) throws ClassNotFoundException {
        String path = "e:\myclasspath\" + name + ".class";
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);
            // 得到字节数组
            byte[] bytes = os.toByteArray();
            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}
// 测试自定义类加载器
public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class c1 = classLoader.loadClass("MapImpl1");
        Class c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2);
        c1.newInstance();
    }
}

运行期优化

在程序运行时,JVM会根据情况对程序进行优化。

分层编译

代码示例

// -XX:+PrintCompilation -XX:-DoEscapeAnalysis
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("%dt%dn",i,(end - start));
    }
}

上面代码外层循环200次,内层循环 1000 次,创建1000个对象,统计每次内层循环的时间。
运行结果如下:

0	399200
1	38400
2	34300
3	34900
4	37800
...
61	32600
62	52000
63	14000
64	13400
65	13000
...
109	14100
110	14260
111	16300
112	500
113	600
114	500

可以看出,运行速度越来越快,这是因为JVM在程序执行时,对代码进行优化。
JVM 将执行状态分成了 5 个层次:
0 层,解释执行(Interpreter)
1 层,使用 C1 即时编译器编译执行(不带 profiling)
2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
4 层,使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,如【方法的调用次数】,【循环的回边次数】等,如果发现代码被频繁执行,则可以由C2即使编译器执行,对热点代码进行彻底优化。

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

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

在示例代码中,使用了一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。在代码中新建的对象并没有使用,创建的对象没有逃逸。就进行优化,不进行对象的创建。

方法内联

示例代码

private static int square(final int i) {
	return i * i;
}
public static void main(String[] args) {
	int x = 0;
	for (int i = 0; i < 500; i++) {
		long start = System.nanoTime();
		for (int j = 0; j < 1000; j++) {
			x = square(9);
		} 
		long end = System.nanoTime();
		System.out.printf("%dt%dt%dn",i,x,(end - start));
	}
}

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、
粘贴到调用者的位置:
x = square(9); 替换为 x = (9 * 9),再进行优化为 x = (81)
代码运行结果

0	81	143600
1	81	30400
2	81	26400
3	81	23100
4	81	25500
...
61	81	21000
62	81	24700
63	81	27200
64	81	9800
65	81	4900
...
218	81	25800
219	81	4600
220	81	0
221	81	0
222	81	100

可以看出对代码进行了优化,将方法进行了内联优化。

字段优化

所谓的字段优化就是对静态变量,成员变量的读写 *** 作的优化。

示例代码:

@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {
    int[] elements = randomInts(1_000);
    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }

    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }
    
    static int sum = 0;
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    static void doSum(int x) {
        sum += x;
    }
    
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

代码分析:类中存在一个数组,随机生成了数据,分别测试三个方法,对数组元素进行累加求和的时间。
启动方法内联优化。
输出结果:

Benchmark 			Mode 	Samples 	Score 		  Score error     Units
t.Benchmark1.test1  thrpt         5 	2420286.539   390747.467      ops/s
t.Benchmark1.test2  thrpt         5     2544313.594   91304.136       ops/s
t.Benchmark1.test3  thrpt         5     2469176.697   450570.647      ops/s

可以看出 测试的三个方法的score值差别不大,那是因为在方法内联的时候进行了优化。
例如在 test1中:

public void test1() {
	// elements.length 首次读取会缓存起来 -> int[] local
	for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
		// 1000 次取下标 i 的元素 <- local
		sum += elements[i];
	}
}

在方法内联优化,在首次读取 elements 数组时,将数组缓存到本地,下一次读取时,直接从本地进行读取。可以节省 1999 次 Field 读取 *** 作。

反射优化

代码示例:

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("%dt", i);
			foo.invoke(null);
		} 
		System.in.read();
	}
}

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

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    	// inflationThreshold 膨胀阈值,默认 15
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
        	// 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            this.parent.setDelegate(var3);
        }
        // 调用本地方法 invoke0
        return invoke0(this.method, var1, var2);
    }
    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }
    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现。通过 debug 得到生成的类名为 sun.reflect.GeneratedMethodAccessor1。
通过使用阿里的 arthas 工具:反编译得到该类

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
	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(Object.super.toString());
		}
	}
}

在生成的类中直接调用了 Reflect1.foo(); 方法。要比 使用本地方法 invoke0快20倍。但是生成类的时候要耗费时间,如果不经常调用,就不会生成类,直接调用。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5564392.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)