类加载
类加载阶段
加载
-
将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用,镜像起到一个桥梁的作用,Java对象不能直接访问instanceKlass 的信息,它得通过镜像_java_mirror 来访问。例如,对于String,Java只能先找到String.class,String.class实际上就是instanceKlass 的一个对象,它们互相持有指向对方的指针。我们通过java对象,想访问instanceKlass ,需要先访问镜像对象。 -
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
-
如果这个类还有父类没有加载,先加载父类
-
加载和链接可能是交替运行的
-
类被加载到方法区,方法区的实现就是元空间的实现,所以类的字节码都被加载到元空间中,就构成了instanceKlass数据结构,加载的同时就会在堆内存中生成镜像,就是Person.class对象,这个类对象就是在堆内存中,但是它持有了instanceKlass的地址。反之,instanceKlass也持有了Person.class的地址。如果以后用new关键字创建了一系列的person实例对象。那么它们是怎么联系起来的,每个实例对象都有自己的对象头,16字节,其中,8字节对应着对象的class地址,如果你想通过对象获取class信息,它就会访问对象的对象头。然后通过地址找到java_mirror,然后去元空间找到获取类的信息,如fields,methods等。
instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
_java_mirror则是保存在堆内存中
InstanceKlass和.class(JAVA镜像类)互相保存了对方的地址
类的对象在对象头中保存了.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息**
链接
验证
-
第一步 :验证class文件格式是否正确
- 1、文件格式验证
- 2、元数据验证
- 3、字节码验证
- 4、符号引用验证
准备
-
为类的静态变量分配内存,并将其赋默认值
- static 变量在JDK 7 之前存储于instanceKlass 末尾, 从JDK1.7开始,存储于_java_mirror末尾
- static 变量分配空间和赋初始值是两个步骤,分配完空间之后会给变量赋默认值,分配空间在准备阶段完成,赋初始值是在初始化阶段完成
- 如果static变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,那么赋值就是在准备阶段完成的。
- 如果static变量是final的,但属于引用类型,那么赋初始值也会在初始化阶段完成
- 但是无论如何在准备阶段都会赋默认值,(如0、0L、null、false等)。
-
实例变量是在创建对象的时候完成赋值的,没有赋默认值一说
-
对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
-
静态变量跟类对象存在一起,它们都是存储在堆中的,早期在方法区
解析
-
将常量池中的符号引用(仅仅是符号,并不知到这个类或者方法 在内存中的具体位置)解析为直接引用(解析为内存中的地址引用)
-
import java.io.IOException; /** * @Author: sunyang * @Date: 2021/7/26 * @Description: */ public class Test { public static void main(String[] args) throws ClassNotFoundException, IOException { ClassLoader classLoader = Test.class.getClassLoader(); // loadClass 加载类C, 只会加载,不会触发其他过程,不会导致类的解析和初始化,就不造成类D的加载 Class<?> c = classLoader.loadClass("Test"); System.in.read(); } } class C { D d = new D(); } class D { }
-
类的加载都是懒加载,用到了才会加载
-
import java.io.IOException; /** * @Author: sunyang * @Date: 2021/7/26 * @Description: */ public class Test { public static void main(String[] args) throws ClassNotFoundException, IOException { new C(); // 会加载解析类C和D System.in.read(); } } class C { D d = new D(); } class D { }
初始化
《clinit》()v方法
- 初始化即调用《clinit》()v
发生的时机
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类静态变量(如果是常量则不会,因为静态常量是在准备阶段就已经赋值了的),只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会发生的情况
- 访问类的static final 静态常量(基本类型,和字符串常量)不会触发初始化
- 类对象.class 不会触发初始化(因为是在类加载时就创建了java_mirror对象在堆中)
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2为false时
实验
-
import java.io.IOException; /** * @Author: sunyang * @Date: 2021/7/26 * @Description: */ public class Test { static { System.out.println("main init"); } public static void main(String[] args) throws ClassNotFoundException, IOException { // 1. 静态常量不会触发初始化 System.out.println(B.b); // 2.类对象.class 不会触发初始化 System.out.println(B.class); // 3.创建该类的数组不会触发初始化,只是预留空间,并没有new 对象 System.out.println(new B[0]); // 4. 不会初始化类B,但会加载B,A ClassLoader c1 = Thread.currentThread().getContextClassLoader(); c1.loadClass("B"); // 5. 不会初始化类B,但会加载B,A ClassLoader c2 = Thread.currentThread().getContextClassLoader(); Class.forName("B", false, c2); //1. 首次访问这个类的静态变量或静态方法时 System.out.println(A.a); // 2. 子类初始化,如果父类还没初始化,会引发 System.out.println(B.c); // 3.子类访问父类静态变量,只触发父类初始化 System.out.println(B.a); // 4. 会初始化类B,并先初始化类A Class.forName("B"); } } class A { static int a = 0; static { System.out.println("a init"); } } class B extends A { final static double b = 5.0; static boolean c = false; static { System.out.println("b init"); } }
-
import javax.swing.plaf.PanelUI; import java.io.IOException; /** * @Author: sunyang * @Date: 2021/7/26 * @Description: */ public class Test { public static void main(String[] args) { System.out.println(A.a); System.out.println(A.b); System.out.println(A.c); } } class A { public static final int a = 10; public static final String b = "hello"; public static final Integer c = 20; static { System.out.println("init E"); }
-
D:\ideaworkspace\untitled\out\production\untitled>javap -v A.class Classfile /D:/ideaworkspace/untitled/out/production/untitled/A.class Last modified 2021-7-27; size 657 bytes MD5 checksum f3d468c45fc55c43249bc1cdc6771bb0 Compiled from "Test.java" class A minor version: 0 major version: 52 flags: ACC_SUPER Constant pool: #1 = Methodref #8.#28 // java/lang/Object."<init>":()V #2 = Methodref #29.#30 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #3 = Fieldref #7.#31 // A.c:Ljava/lang/Integer; #4 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream; #5 = String #34 // init E #6 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V #7 = Class #37 // A #8 = Class #38 // java/lang/Object #9 = Utf8 a #10 = Utf8 I #11 = Utf8 ConstantValue #12 = Integer 10 #13 = Utf8 b #14 = Utf8 Ljava/lang/String; #15 = String #39 // hello #16 = Utf8 c #17 = Utf8 Ljava/lang/Integer; #18 = Utf8 <init> #19 = Utf8 ()V #20 = Utf8 Code #21 = Utf8 LineNumberTable #22 = Utf8 LocalVariableTable #23 = Utf8 this #24 = Utf8 LA; #25 = Utf8 <clinit> #26 = Utf8 SourceFile #27 = Utf8 Test.java #28 = NameAndType #18:#19 // "<init>":()V #29 = Class #40 // java/lang/Integer #30 = NameAndType #41:#42 // valueOf:(I)Ljava/lang/Integer; #31 = NameAndType #16:#17 // c:Ljava/lang/Integer; #32 = Class #43 // java/lang/System #33 = NameAndType #44:#45 // out:Ljava/io/PrintStream; #34 = Utf8 init E #35 = Class #46 // java/io/PrintStream #36 = NameAndType #47:#48 // println:(Ljava/lang/String;)V #37 = Utf8 A #38 = Utf8 java/lang/Object #39 = Utf8 hello #40 = Utf8 java/lang/Integer #41 = Utf8 valueOf #42 = Utf8 (I)Ljava/lang/Integer; #43 = Utf8 java/lang/System #44 = Utf8 out #45 = Utf8 Ljava/io/PrintStream; #46 = Utf8 java/io/PrintStream #47 = Utf8 println #48 = Utf8 (Ljava/lang/String;)V { public static final int a; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 10 public static final java.lang.String b; descriptor: Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: String hello public static final java.lang.Integer c; descriptor: Ljava/lang/Integer; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL A(); descriptor: ()V flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 47: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LA; static {}; descriptor: ()V flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: bipush 20 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: putstatic #3 // Field c:Ljava/lang/Integer; 8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 11: ldc #5 // String init E 13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: return LineNumberTable: line 50: 0 line 52: 8 line 53: 16 } SourceFile: "Test.java"
-
典型应用 - 完成懒惰初始化单例模式
-
public final class Singleton { private Singleton() { } // 内部类中保存单例 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员 public static Singleton getInstance() { return LazyHolder.INSTANCE; } }
-
以上的实现特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的
-
类加载器
-
启动类加载器,扩展类加载器,应用程序累加载器,自定义类加载器
启动类加载器
-
用 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()); } }
-
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5 bootstrap F init null
-
-
-Xbootclasspath 表示设置 bootclasspath
-
其中 /a:. 表示将当前目录追加至 bootclasspath 之后
-
可以用这个办法替换核心类
- java -Xbootclasspath:
- java -Xbootclasspath/a:<追加路径>
- java -Xbootclasspath/p:<追加路径>
扩展类加载器
-
public class G { static { System.out.println("classpath 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()); } }
-
classpath G init sun.misc.Launcher$AppClassLoader@18b4aac2
-
写一个同名类
-
package cn.itcast.jvm.t3.load; public class G { static { System.out.println("ext G init"); } }
-
// 打个jar包 E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class 已添加清单 正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)
-
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext 重新执行 Load5_2 输出
-
ext G init sun.misc.Launcher$ExtClassLoader@29453f44
-
双亲委派模式
源码分析
-
所谓的双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则
-
这里的双亲,翻译为上级或父级更为合适,因为他们并没有继承关系,只是级别不同而已。
-
/*加载具有指定二进制名称的类。 此方法的默认实现按以下顺序搜索类: 调用findLoadedClass(String)来检查类是否已经加载。 在父类加载器上调用loadClass方法。 如果 parent 为null ,则使用虚拟机内置的类加载器。 调用findClass(String)方法来查找类。 如果使用上述步骤找到了该类,并且解析标志为真,则此方法将在生成的Class对象上调用resolveClass(Class)方法。 鼓励ClassLoader 的子类覆盖findClass(String) ,而不是这个方法。 除非被覆盖,此方法在整个类加载过程中同步getClassLoadingLock方法的结果。 参数: name - 类的二进制名称 解决 - 如果为真,则解决类 返回: 生成的Class对象 抛出: ClassNotFoundException – 如果找不到类 */ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 首先检查该类是否已经被加载 Class<?> c = findLoadedClass(name); // 如果没有被加载 if (c == null) { long t0 = System.nanoTime(); try { // 没有被加载 且父加载器不为空 if (parent != null) { // 调用父类的loadClass方法 递归 c = parent.loadClass(name, false); } else { // 如果为空 说明是启动类加载器,则调用启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } // 如果都没找到 则按顺序调用findClass 去查找类 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
-
执行流程为:
- sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
- sun.misc.Launcher A p p C l a s s L o a d e r / / 2 处 , 委 派 上 级 s u n . m i s c . L a u n c h e r AppClassLoader // 2 处,委派上级 sun.misc.Launcher AppClassLoader//2处,委派上级sun.misc.LauncherExtClassLoader.loadClass()
- sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
- BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
- sun.misc.Launcher E x t C l a s s L o a d e r / / 4 处 , 调 用 自 己 的 f i n d C l a s s 方 法 , 是 在 J A V A H O M E / j r e / l i b / e x t 下 找 H 这 个 类 , 显 然 没 有 , 回 到 s u n . m i s c . L a u n c h e r ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher ExtClassLoader//4处,调用自己的findClass方法,是在JAVAHOME/jre/lib/ext下找H这个类,显然没有,回到sun.misc.LauncherAppClassLoader 的 // 2 处
- 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了
线程上下文类加载器
-
默认就是程序类加载器
-
具体见文档SPI ,
自定义类加载器
- 想加载非classpath 随意路径下的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤
-
继承classLoader父类
-
要遵守双亲委派机制,重写findClass方法
- 注意不是重写loadClass方法,否则不会走双亲委派机制
-
读取类文件的字节码
-
调用父类的defineClass方法来加载类
-
使用者调用该类加载器的loadClass方法
-
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; /** * @program: jvmstudy * @description: Demo * @author: SunYang * @create: 2021-07-27 19:31 **/ public class MyClassLoader extends ClassLoader{ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path = "e:\\" + name + ".class"; ByteArrayOutputStream os = new ByteArrayOutputStream(); try { Files.copy(Paths.get(path), os); byte[] bytes = os.toByteArray(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类文件未找到", e); } } }
命名空间
- 命名空间是由该类加载器以及其父类加载器所构成的,其中父类加载器加载的类对其子类可见,但是反过来子类加载的类对父类不可见,同一个命名空间中一定不会出现同一个类(全限定名一模一样的类)多个Class对象,换句话说就是在同一命名空间中只能存在一个Class对象,所以当你听别人说在内存中同一类的Class对象只有一个时其实指的是同一命名空间中,当然也不排除他压根就不知道这个概念。
原文链接:https://blog.csdn.net/yuge1123/article/details/99945983
运行期优化
即时编译
分层编译
-
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)); } } }
-
0 96426 1 52907 2 44800 3 119040 4 65280 5 47360 6 45226 72 19200 73 15360 74 18347 75 19627 76 17067 146 853 147 854 148 853 149 853 150 854
-
原因是什么呢? 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名称的由来),优化之刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸(是在c2阶段进行的优化)。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果
方法内联
-
private static int square(final int i) { return i * i; }
-
System.out.println(square(9));
-
如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:
-
System.out.println(9 * 9);// 这个是进行了常量折叠优化
-
实验
-
public class JIT2 { // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印 inlining 信息 // -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining // -XX:+PrintCompilation 打印编译信息 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("%d\t%d\t%d\n",i,x,(end - start)); } } private static int square(final int i) { return i * i; } } // 前几次是5位6位数,到最后会变成0;
-
字段优化
-
public void test1() { // elements.length 首次读取会缓存起来 -> int[] local for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local sum += elements[i]; // 1000 次取下标 i 的元素 <- local } }
-
@Benchmark public void test1() { // 运行期优化 for (int i = 0; i < elements.length; i++) { doSum(elements[i]); } } @Benchmark public void test2() { // 这种方式和上一种一样,只不过是一种是我们自己手动优化,一种是JVM优化 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); } }
-
可以节省 1999 次 Field 读取操作 但如果 doSum 方法没有内联,则不会进行上面的优化
反射优化
-
foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
-
当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1
-
注意 通过查看 ReflectionFactory 源码可知 sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首 次生成比较耗时,如果仅反射调用一次,不划算) sun.reflect.inflationThreshold 可以修改膨胀阈值
ark
public void test2() {
// 这种方式和上一种一样,只不过是一种是我们自己手动优化,一种是JVM优化
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);
}
}
- 可以节省 1999 次 Field 读取操作 但如果 doSum 方法没有内联,则不会进行上面的优化
### 反射优化
- foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
- 当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1
- 注意 通过查看 ReflectionFactory 源码可知 sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首 次生成比较耗时,如果仅反射调用一次,不划算) sun.reflect.inflationThreshold 可以修改膨胀阈值