JVM类加载

文章目录

  • 1 类文件结构
  • 2 字节码指令
    • 2.1 编译执行流程分析
    • 2.2 多态原理
    • 2.3 异常处理
    • 2.4 synchronized
  • 3 编译器处理
  • 4 类加载阶段
  • 5 类加载器
  • 6 运行期优化

1 类文件结构

执行 javac -parameters -d . HellowWorld.java编译为 HelloWorld.class文件,根据 JVM 规范,类文件结构如下

ClassFile {
u4 magic;	//魔数
u2 minor_version;	//版本
u2 major_version;
u2 constant_pool_count;	//常量池
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;	//访问标识与继承信息
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;	//Field 信息
field_info fields[fields_count];
u2 methods_count;	//Method 信息
method_info methods[methods_count];
u2 attributes_count;	//附加属性
attribute_info attributes[attributes_count];
}

2 字节码指令

2.1 编译执行流程分析

原始代码如下:

package cn.itcast.jvm.t3.bytecode;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}

字节码文件自己分析嫌慢,可以执行指令javap -v filepath反编译命令,直接获取字节码指令更直观

[root@localhost ~]# javap -v Demo3_1.class
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."":()V
#2 = Class #27 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #28.#29 //
java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 Demo3_1.java
#26 = NameAndType #8:#9 // "":()V
#27 = Utf8 java/lang/Short
#28 = Class #34 // java/lang/System
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 cn/itcast/jvm/t3/bytecode/Demo3_1
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (I)V
{
public cn.itcast.jvm.t3.bytecode.Demo3_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field
java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method
java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
3)常量池载入运行时常量池
4)方法字节码载入方法区
5)main 线程开始运行,分配栈帧内存
(stack=2,locals=4)
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
MethodParameters:
Name Flags
args
}

问题:方法如何被执行的呢?

ANS:原始代码编译成字节码文件->常量池载入运行时常量池->方法字节码载入方法区->main线程开始运行,分配栈帧内存->执行引擎开始执行字节码->最终在内存结构上表现如下图

JVM类加载_第1张图片

常用字节码指令参照:

指令码 操作码 描述(栈指操作数栈)
0x03 iconst_0 0(int)值入栈
0x10 bipush valuebyte值带符号扩展成int值入栈
0x11 sipush 将一个 short 值入栈
0x12 ldc 常量池中的常量值入栈
0x2a aload_0 加载 slot 0 的局部变量
0x4b astroe_0 将栈顶值保存到 slot 0 的局部变量中
0x57 pop 从栈顶弹出一个字长的数据。
0x59 dup 复制栈顶一个字长的数据,将复制后的数据压栈。
0x60 iadd 将栈顶两int类型数相加,结果入栈。
0x84 iinc 直接在局部变量 slot 上进行运算
0x9c ifge 若栈顶int类型值大于等于0则跳转。
0xa7 goto 无条件跳转到指定位置。
0xbb new 创建新的对象实例。
0xb4 getfield 获取对象字段的值。
0xb2 getstatic 获取静态字段的值。
0xb7 invokespecial 预备调用构造方法
0xb6 invokevirtual 预备调用成员方法
0xb8 invokestatic 预备调用静态方法
0xb9 invokeinterface 预备调用方法
0xb0 areturn 返回引用类型值。
0xb1 return void函数返回。
0xc2 monitorenter 进入并获得对象监视器。(线程同步)
0xc3 monitorexit 释放并退出对象监视器。(线程同步)

2.2 多态原理

借助工具分析

①jps 获取进程 id

②运行 HSDB 工具,进入 JDK 安装目录,执行java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB,进入图形界面 attach 进程 id

③查找对象,打开 Tools -> Find Object By Query,输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行

④点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord,后 8 字节就是对象的 Class 指针

⑤通过 Windows -> Console 进入命令行模式,执行mem ③中的对象头地址 2

⑥查看类的 vtable,Alt+R 进入 Inspector 工具,输入刚才的⑤得到的 Class 内存地址,得到vtable长度为n

⑦ ⑤得到的 Class 内存地址偏移 0x1b8 就是 vtable 的起始地址,通过 Windows -> Console 进入命令行模式,执行 mem vtable起始地址 6,就得到了 6 个虚方法的入口地址

⑧通过 Tools -> Class Browser 查看每个类的方法定义,比较可知,方法属于那个类,以判断是否多态调用

结论

当执行 invokevirtual 指令时,

  1. 先通过栈帧中的对象引用找到对象

  2. 分析对象头,找到对象的实际 Class

  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了

  4. 查表得到方法的具体地址

  5. 执行方法的字节码

2.3 异常处理

原始代码:

public class Demo3_11_4 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}

字节码指令:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try --------------------------------------
4: istore_1 // 10 -> i |
5: bipush 30 // finally |
7: istore_1 // 30 -> i |
8: goto 27 // return -----------------------------------
11: astore_2 // catch Exceptin -> e ----------------------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -----------------------------------
21: astore_3 // catch any -> slot 3 ----------------------
22: bipush 30 // finally |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // throw ------------------------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
StackMapTable: ...
MethodParameters: ...

总结:

Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号

11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

2.4 synchronized

原始代码:

public class Demo3_13 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}

字节码:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial :()V
7: astore_1 // lock引用 -> lock
8: aload_1 // <- lock (synchronized开始)
9: dup
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock引用)
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // invokevirtual println:
(Ljava/lang/String;)V
20: aload_2 // <- slot 2(lock引用)
21: monitorexit // monitorexit(lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2(lock引用)
27: monitorexit // monitorexit(lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: ...
MethodParameters: ...

总结:

①加载对象然后上锁或者解锁

②异常表的作用是保证上锁的代码块出现异常时,对象锁也能正常释放掉

③方法级别的 synchronized 不会在字节码指令中有所体现

3 编译器处理

语法糖,即.java文件编译为.class字节码文件过程中的代码转换,例如

语法糖 转换
默认构造器 无参构造,方法内调用父类无参构造
自动拆装箱 Integer.valueOf / 整型值x.intValue
泛型集合取值 ((Integer)list.get(0)).intValue()
可变参数 其实是一个数组
foreach 循环 数组转为下标循环,集合则准换为迭代器
switch 字符串 两层switch,第一层先匹配hashcode提高效率, 再匹配内容
switch 枚举 和上面类似,先在静态代码块中将类元素做映射,再两层switch
try-with-resources 接口实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码
方法重写 子类返回值可以是父类返回值的子类,子类中定义了桥接方法
匿名内部类 额外生成类,且如果引用了局部变量,会在新类的有参构造中对该变量赋值

4 类加载阶段

分为三个阶段:加载、链接、初始化

阶段 主要内容
加载 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类
重要 field 有_java_mirror 即 java 的类镜像(存储在堆中),例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
有父类先加载父类
加载和链接可能交替运行
链接 ①验证:验证类是否符合 JVM规范,安全性检查
②准备:为 static 变量分配空间,设置默认值,赋值有两个可能,如果变量为final修饰的基本类型以及字符串常量,准备阶段就能赋值,否则只能初始化再赋值
③解析:将常量池中的符号引用解析为直接引用
初始化 调用 < cinit >()V ,虚拟机会保证这个类的『构造方法』的线程安全

类初始化发生的时机:

会初始化(懒惰的) 不会初始化
main 方法所在的类,总会被首先初始化 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
首次访问这个类的静态变量或静态方法时 类对象.class 不会触发初始化
子类初始化,如果父类还没初始化,会引发 创建该类的数组不会触发初始化
子类访问父类的静态变量,只会触发父类的初始化 类加载器的 loadClass 方法
Class.forName Class.forName 的参数 2 为 false 时
new 会导致初始化

5 类加载器

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

一、如何指定类加载器加载指定类?

①启动类加载器:

java -Xbootclasspath/a:. 类路径

其中,可以用下面参数来替换核心类

java -Xbootclasspath: 新路径

java -Xbootclasspath/a: 追加路径(后追加)

java -Xbootclasspath/p: 追加路径(前追加,用于替换核心类)

②扩展类加载器:

jar -cvf my.jar 类路径 打个jar包,拷贝到 JAVA_HOME/jre/lib/ext

二、双亲委派模式怎么理解?

类加载器的 loadClass 方法说明了查找类的规则,如下:

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;
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

  2. 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()

  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找

  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  6. 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 处

  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了

三、以Driver驱动类为例来分析说明线程上下文类加载器?

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,看源码:

public class DriverManager {
	// 注册驱动的集合
	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
		= new CopyOnWriteArrayList<>();
	// 初始化驱动
	static {
		loadInitialDrivers();
		println("JDBC DriverManager initialized");
	}

DriverManager类加载器是 Bootstrap ClassLoader,即该类存在于核心类库, 但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>
                () {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // 1)使用 ServiceLoader 机制加载驱动,即 SPI
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers =
                    ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });
    println("DriverManager.initialize: jdbc.drivers = " + drivers);
    // 2)使用 jdbc.drivers 定义的驱动名加载驱动
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

JVM类加载_第2张图片

再看 1)中ServiceLoader.load 方法可看到底层使用线程上下文类加载器器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载

四、自定义类加载器?

步骤:

  1. 继承 ClassLoader 父类

  2. 要遵从双亲委派机制,重写 findClass 方法注意不是重写 loadClass 方法,否则不会走双亲委派机制

  3. 读取类文件的字节码

  4. 调用父类的 defineClass 方法来加载类

  5. 使用者调用该类加载器的 loadClass 方法

示例代码:

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);
        }
    }
}

6 运行期优化

由JVM内存结构可知 (回顾:JVM内存结构) ,字节码需由解释器逐行解释为机器码再执行,而即时编译器(JIT)不仅能实现这一功能,还能进一步优化,对比如下:

引擎 作用/特点 优势
解释器 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释 将字节码解释为针对所有平台都通用的机器码
即时编译器 根据平台类型,生成平台特定的机器码 将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
缺点显然是耗费时间和资源,因此针对的是热点代码

根据JIT不同参与程度又将JVM执行状态分为5个层次:

0 层,解释执行(Interpreter)

1 层,使用 C1 即时编译器编译执行(不带 profiling)

2 层,使用 C1 即时编译器编译执行(带基本的 profiling)

3 层,使用 C1 即时编译器编译执行(带完全的 profiling)

4 层,使用 C2 即时编译器编译执行

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

JIT相关优化的经典应用场景为:

应用 说明
逃逸分析 观察新建的对象是否逃逸
方法内联 把热点方法内代码拷贝、粘贴到调用者的位置
常量折叠 9 * 9 替换为81
字段优化 方法外的字段首次读取会缓存起来,以减少访问次数
反射优化 invoke的调用,使用的是MethodAccessor 的 NativeMethodAccessorImpl 实现(本地实现), 当调用次数达到膨胀阈值时,使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右

你可能感兴趣的:(jvm,jvm,java)