本文仍然基于JDK8版本,从JDK9模块化器,类加载器有一些变动。

0 javac编译

java代码

public class Math {
public static final int initData = 666; public static User user = new User(); public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
} public static void main(String[] args) {
Math math = new Math();
math.compute();
System.out.println("end");
}
}

javac 编译,javap -v -p 查看class文件

Classfile /F:/workspace/advanced-java/target/classes/com/lzp/java/jvm/classloader/Math.class

// 第1部分,描述信息:大小、修改时间、md5值等
Last modified 2022年1月8日; size 1006 bytes
MD5 checksum 4cece4543963b23a98cd219a59c1887c
Compiled from "Math.java" // 第2部分,描述信息:编译版本
public class com.lzp.java.jvm.classloader.Math
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // com/lzp/java/jvm/classloader/Math
super_class: #11 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, attributes: 1 // 第3部分,常量池信息
Constant pool:
#1 = Methodref #11.#39 // java/lang/Object."<init>":()V
#2 = Class #40 // com/lzp/java/jvm/classloader/Math
#3 = Methodref #2.#39 // com/lzp/java/jvm/classloader/Math."<init>":()V
#4 = Methodref #2.#41 // com/lzp/java/jvm/classloader/Math.compute:()I
#5 = Fieldref #42.#43 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #44 // end
#7 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #47 // com/lzp/java/jvm/classloader/User
#9 = Methodref #8.#39 // com/lzp/java/jvm/classloader/User."<init>":()V
#10 = Fieldref #2.#48 // com/lzp/java/jvm/classloader/Math.user:Lcom/lzp/java/jvm/classloader/User;
#11 = Class #49 // java/lang/Object
#12 = Utf8 initData
#13 = Utf8 I
#14 = Utf8 ConstantValue
#15 = Integer 666
#16 = Utf8 user
#17 = Utf8 Lcom/lzp/java/jvm/classloader/User;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/lzp/java/jvm/classloader/Math;
#25 = Utf8 compute
#26 = Utf8 ()I
#27 = Utf8 a
#28 = Utf8 b
#29 = Utf8 c
#30 = Utf8 main
#31 = Utf8 ([Ljava/lang/String;)V
#32 = Utf8 args
#33 = Utf8 [Ljava/lang/String;
#34 = Utf8 math
#35 = Utf8 MethodParameters
#36 = Utf8 <clinit>
#37 = Utf8 SourceFile
#38 = Utf8 Math.java
#39 = NameAndType #18:#19 // "<init>":()V
#40 = Utf8 com/lzp/java/jvm/classloader/Math
#41 = NameAndType #25:#26 // compute:()I
#42 = Class #50 // java/lang/System
#43 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#44 = Utf8 end
#45 = Class #53 // java/io/PrintStream
#46 = NameAndType #54:#55 // println:(Ljava/lang/String;)V
#47 = Utf8 com/lzp/java/jvm/classloader/User
#48 = NameAndType #16:#17 // user:Lcom/lzp/java/jvm/classloader/User;
#49 = Utf8 java/lang/Object
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 java/io/PrintStream
#54 = Utf8 println
#55 = Utf8 (Ljava/lang/String;)V
{
// 第四部分,变量信息
public static final int initData;
descriptor: I
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 666 public static com.lzp.java.jvm.classloader.User user;
descriptor: Lcom/lzp/java/jvm/classloader/User;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC public com.lzp.java.jvm.classloader.Math();
descriptor: ()V
flags: (0x0001) 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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lzp/java/jvm/classloader/Math;
// 第五部分,方法信息
public int compute();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 9: 0
line 10: 2
line 11: 4
line 12: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/lzp/java/jvm/classloader/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/lzp/java/jvm/classloader/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String end
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 math Lcom/lzp/java/jvm/classloader/Math;
MethodParameters:
Name Flags
args static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #8 // class com/lzp/java/jvm/classloader/User
3: dup
4: invokespecial #9 // Method com/lzp/java/jvm/classloader/User."<init>":()V
7: putstatic #10 // Field user:Lcom/lzp/java/jvm/classloader/User;
10: return
LineNumberTable:
line 6: 0
}

方法中的#1/2,可以到ConstantPool找到对应符号。

参考字节码指令表:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html。

1 类加载过程

j经典的类加载过程如下图,包括加载、链接、初始化三部分。

1.1 加载class文件

字节码文件位于磁盘,当使用到某个类(例如,调用main()方法,new新对象),在磁盘中查找并通过IO读取文件的二进制流,转为方法区数据结构,并存放到方法区,在Java堆中产生 java.lang.Class对象。Class对象是可以方法区的访问入口,用于Java反射机制,获取类的各种信息。

1.2 链接过程

验证:验证class文件是不是符合规范

  1. 文件格式的验证。验证是否以0XCAFEBABE开头,版本号是否合理

  2. 元数据验证。是否有父类,是否继承了final类(final类不能被继承),非抽象类实现了所有抽象方法。

  3. 字节码验证。(略)

  4. 符号引用验证。常量池中描述类是否存在,访问的方法或字段是否存在且有足够的权限。

-Xverify:none  // 取消验证

准备:为类的静态变量分配內存,初始化为系统的初始值

final static修饰的变量:直接赋值为用户定义的值,比如 private final static int value=123,直接赋值123。

private static int value=123,该阶段的值依然是0。

解析:符号引用转换成直接引用(静态链接)

Java代码中每个方法、方法参数都是符号,类加载放入方法区的常量池Constant pool中。

符号引用:应该可以理解成常量池中的这些字面量。【可能没理解对】

直接引用:符号对应代码被加载到JVM内存中的位置(指针、句柄)。

静态链接过程在类加载时完成,主要转换一些静态方法。动态链接是在程序运行期间完成的将符号引用替换为直接引用。

1.3 初始化(类初始化clinit-->初始化init)

执行< clinit>方法, clinit方法由编译器自动收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法

  • 初始化的顺序和源文件中的顺序一致

  • 子类的< clinit>被调用前,会先调用父类的< clinit>

  • JVM会保证clinit方法的线程安全性

初始化时,如果实例化一个新对象,会调用<init>方法对实例变量/代码块进行初始化,并执行对应的构造方法内的代码。

类加载过程是懒加载的,用到才会加载。

初始化示例

public class JVMTest2 {
static {
System.out.println("JVMTest2静态块");
} {
System.out.println("JVMTest2构造块");
} public JVMTest2() {
System.out.println("JVMTest2构造方法");
} public static void main(String[] args) {
System.out.println("main方法");
new Sub();
}
} class Super {
static {
System.out.println("Super静态代码块");
} public Super() {
System.out.println("Super构造方法");
} {
System.out.println("Super普通代码块");
}
} class Sub extends Super {
static {
System.out.println("Sub静态代码块");
} public Sub() {
System.out.println("Sub构造方法");
} {
System.out.println("Sub普通代码块");
}
} JVMTest2静态块
main方法
Super静态代码块
Sub静态代码块
Super普通代码块
Super构造方法
Sub普通代码块
Sub构造方法

执行main方法,并不需要创建JVMTest2实例。

2 类加载器

查看当前JDK类加载器

public class PrintJDKClassLoader {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
ClassLoader parentParent = parent.getParent();
System.out.println(parentParent);
}
} // JDK8
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@28a418fc
null
// JDK11
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1324409e
null

2.1 类加载器(JDK8)

类加载器初始化过程:Java通过调用jvm.dll文件创建JVM,创建一个引导类加载器(由C++实现),通过JVM启动器(sun.misc.Launcher)加载扩展类加载器和应用类加载器。

  • 启动类加载器:负责加载lib目录下的核心类库。作为JVM的一部分,由C++实现。

  • 扩展类/平台类加载器:负责加载lib目录下的ext扩展目录中的JAR 类包。

  • 应用程序类加载器:负责加载用户类路径ClassPath路径下的类包,主要就是加载用户自己写的类。

  • 自定义类加载器:负责加载用户自定义路径下的类包。

JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。

// Launcher构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
// 构造扩展类加载器,设置类加载器parent属性设为null。
var1 = Launcher.ExtClassLoader.getExtClassLoader();
// 构造应用类加载器,设置类加载器parent属性为扩展类加载器。
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
// 权限校验代码..
}
}

2.2 双亲委派模型

类加载器采用三层、双亲委派模型,类加载器的父子关系不是继承关系,而是组合关系。除了启动类加载器外,其他类加载器都是继承自ClassLoader类。

工作过程:类加载器收到类加载请求,首先判断类是否已经加载,如果未被加载,尝试将请求向上委派给父类加载器加载。当父类加载器无法完成加载任务,再由子类加载器尝试加载。

// ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 非启动类加载器
c = parent.loadClass(name, false);
} else {
// 启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载指定类
} if (c == null) {
// 调用当前类加载器的findClass方法进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

为什么使用双亲委派模型,感觉走了弯路?

双亲委派模型下,类加载请求总会被委派给最上层的启动类加载器。对于未加载的类来说,需要从底层走到顶层;如果用户定义的类已经被加载过,则不需要委派过程。

使用双亲委派机制有下面几个好处:

  • 沙箱安全机制,防止核心类库代码被篡改。
  • 避免类重复加载,父类加载器加载过,子类加载器不需要再次加载。

全盘负责委托机制

全盘负责 :即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的其它Class 通常 也由这个classloader负责载入。 委托机制 :先让parent(父)类加载器 寻找,只有在parent找不到的时候才从自己的类路径中去寻找。

参考Launcher构造方法

Thread.currentThread().setContextClassLoader(this.loader);

自定义类加载器

自定义类加载器操作主要是继承ClassLoader类,重写上面源码中的findClass(name)方法。

public class CustomClassLoaderTest {
static class CustomClassLoader extends ClassLoader {
private String classFilePath; public CustomClassLoader(String classFilePath) {
this.classFilePath = classFilePath;
}
// 载入class数据流
private byte[] loadClassFile(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classFilePath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
} protected Class<?> findClass(String name) throws ClassNotFoundException{
try {
byte[] data = loadClassFile(name);
// 加载--链接--初始化等逻辑
return defineClass(name,data,0,data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
} public static void main(String[] args) throws Exception {
CustomClassLoader classLoader = new CustomClassLoader("F:");
Class<?> clazz = classLoader.loadClass("com.lzp.java.jvm.classloader.JVMTest");
Object instance = clazz.newInstance();
Method method = clazz.getDeclaredMethod("add", null);
System.out.println(method.invoke(instance));
System.out.println(clazz.getClassLoader().getClass().getName());
}
}

自定义类加载器的父加载器是应用类加载器。CustomClassLoader是使用AppClassLoader运行的,自然而然是父类加载器。

打破双亲委派机制

在一些场景下,打破双亲委派是必要的。例如Tomcat中可能有多个应用,引用了不同的Spring版本。打破双亲委派,可以实现应用隔离。

JVM使用loadClass方法实现双亲委派机制。重写loadClass方法,便可以打破双亲委派机制。

直接删除双亲委派代码是不可行的,Java代码继承自Object,总会需要双亲委派来加载核心代码。

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
// 非自定义的类还是走双亲委派加载
if (!name.equals("com.lzp.java.jvm.classloader.JVMTest")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

注:JDK自带的核心库代码,是不允许自行配置修改的。例如,不可以将Object.class拷出来执行。沙箱隔离。

彻底剖析JVM类加载机制的更多相关文章

  1. JVM基础系列第7讲:JVM 类加载机制

    当 Java 虚拟机将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析.运行等整个过程,这个过程我们叫:Java 虚拟机的类加载机制.JVM 虚拟机执行 class 字节 ...

  2. JVM总结(四):JVM类加载机制

    这一节我们来总结一下JVM类加载机制.具体目录如下: 类加载的过程 类加载过程概括 说说引用 详解类加载全过程: 加载 验证 准备 解析 初始化 虚拟机把描述类的数据从Class文件加载到内存,并对数 ...

  3. JVM 类加载机制详解

    如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lan ...

  4. Java虚拟机(四):JVM类加载机制

    1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...

  5. JVM类加载机制详解(二)类加载器与双亲委派模型

    在上一篇JVM类加载机制详解(一)JVM类加载过程中说到,类加载机制的第一个阶段加载做的工作有: 1.通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件).而获取的方式,可 ...

  6. JVM类加载机制(转)

    原文出自:http://www.cnblogs.com/ityouknow/p/5603287.html 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运 ...

  7. JVM类加载机制详解

    引言 如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 加载 在加载阶段,虚拟机需要完成以下三件事情: 1)通过一个类的全限定名来获取定义此 ...

  8. Android动态加载--JVM 类加载机制

    动态加载,本质上是通过JVM类加载机制将插件模块加载到宿主apk中,并通过android的相关运行机制,实现插件apk的运行.因此熟悉JVM类加载的机制非常重要. 类加载机制:虚拟机把描述类的数据从C ...

  9. Java虚拟机(五):JVM 类加载机制

    一.JVM 类加载机制 JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 1. 加载: 加载是类加载过程中的第一个阶段,这个阶段会在内存中生成一个代表 ...

随机推荐

  1. C#汽车租赁系统

    类图: 父类(车类,抽象类) /// <summary> /// 车辆基本信息类.搞一个抽象类玩玩 /// </summary> public abstract class V ...

  2. 30个类手写Spring核心原理之自定义ORM(上)(6)

    本文节选自<Spring 5核心原理> 1 实现思路概述 1.1 从ResultSet说起 说到ResultSet,有Java开发经验的"小伙伴"自然最熟悉不过了,不过 ...

  3. jetbrain家的fleet(已获得预览权限)直接对标vscode , fleet有望超过vscode吗?今天我们实际操作下

    申请预览版 等待了一周终于得到了预览版的机会 今天就来简单使用下. 前言 工程管理大多使用的是maven , 在maven之前还有ant 这个应该已经没多少人在使用了,或者说新人基本不在使用ant , ...

  4. 30个类手写Spring核心原理之动态数据源切换(8)

    本文节选自<Spring 5核心原理> 阅读本文之前,请先阅读以下内容: 30个类手写Spring核心原理之自定义ORM(上)(6) 30个类手写Spring核心原理之自定义ORM(下)( ...

  5. java 图形化工具Swing 颜色文件选择器 ;JColorChooser;JFileChoose

    使用JColorChooser: JColorChooser用于创建颜色选择器对话框,该类的用法非常简单,该类主要提供了如下两个静态方法: (1),showDialog(Component compo ...

  6. java 图形化小工具Abstract Window Toolit ;布局管理器FlowLayout流式布局;BorderLayout边界布局;GridLayout网格布局;CardLayou重叠卡片布局;BoxLayout方框布局;绝对定位

    1.FlowLayout流式布局管理器: FlowLayout布局管理器中,组件像水流一样向某方向流动(排列),遇到障碍(边界)就折回,重头开始排列 .在默认情况下,FlowLayout局管理器从左向 ...

  7. java 常用类库:Object类和Objects类

    1,Object类: Object类是所有的类,数组,枚举的父类,也就是说,JAVA中允许把任何的对象赋值给Object类(包括基础数据类型),当定义一个类的时候,没有使用extends关键字显示指定 ...

  8. flutter 学习笔记

    常用属性 container 填充padding,边距margins,边框borders,背景色color, decoration: 渐变gradient-Alignment(x,y),圆角borde ...

  9. Spring学习(一)idea中创建第一个Spring项目

    1.前言 Spring框架是一个开放源代码的J2EE应用程序框架,由Rod Johnson发起,是针对bean的生命周期进行管理的轻量级容器(lightweight container). Sprin ...

  10. 【LeetCode】942. DI String Match 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...