4506 字
23 分钟
JVM理论
2025-01-15

架构图#

HotSpotJVMArch

HotSpotJVMArch2

编译#

半编译半解释#

这里指的是编译以后!一部分代码解释运行,热点代码由JIT编译成机器码直接运行。

ClassFile1

前端编译器 vs 后端编译器#

后端编译器:JIT

前端编译器:javac、Eclipse中的ECJ(增量式编译:编译改变的代码)

字节码#

Class#

结构如下:

  • 魔数
  • Class文件版本
  • 常量池
  • 访问标识
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

常量池索引从1开始

符号引用 vs 直接引用#

  • 符号引用使用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到了内存中。

  • 直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的。

类的加载 ***#

简述Java类加载机制(百度 滴滴 腾讯 京东)

类的加载过程(生命周期)#

类加载分几步?#

苏宁、国美、蚂蚁金服、百度、美团、京东

ClassLoad

都谁需要加载?

在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型需要进行类的加载。

1. Loading(装载)阶段#

做了什么事?#

把磁盘上的 Class File 加载到内存中,生成一个Class (类模板对象),类结构会存储在方法区(JDK1.8以前:永久代;JDK1.8及以后:元空间)

ClassInMem

Class的构造器是私有的,只有JVM可以创建。

数组类的加载有什么不同?#

创建数组类有些特殊,因为数组类本身并不是由类加载器去负责创建,而是JVM在运行时根据需要而直接创建的。

2. Linking(链接)阶段#

① Verification(验证)#

看看 Class文件 是不是合法 Class文件

如果该阶段不能通过,则虚拟机不会正确装载这个类

② Preparation(准备)#

为类的静态变量分配内存,初始化为默认值

  1. 这里不包含用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。

  2. 这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。

  3. 这个阶段并不会像初始化阶段那样有初始化或代码被执行。

③ Resolution(解析)#

将类、接口、字段和方法从符号引用转换为直接引用

举例:输出操作System.out.println()对应的字节码invokevirtual #24 <java/io/PrintStream.println>,得到指针或者偏移量

3. Initialization(初始化)阶段#

简言之,为类的静态变量赋予正确的初始值。

到了初始化阶段,才开始真正开始执行类中定义的Java程序代码。

初始化阶段的重要工作是执行类的初始化方法: <clinit>() 方法

  • 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法在Java中调用该方法,虽然该方法也是由字节码指令所组成。
  • 它是由类静态成员的赋值语句以及static语句块合并产生的。

<clinit>():只有在给类中的 static变量 显示赋值 或在静态代码块中赋值了,才会生成此方法。

<init>():一定会出现在Class的method表中。

子类加载前会先加载父类#

父类的<clinit>()总会在子类<clinit>()之前被调用。即:父类的static块优先级高于子类。

哪些场景不会生成clinit方法#
public class InitializationTest1 {
    // 对于非静态的字段,不管是否显式赋值,都不会生成<clinit>()
    public int num = 1;
    // 静态字段,但没有显式赋值,不会生成<clinit>()
    public static int num1;
    // final以后就不是“变”量了,不会生成<clinit>()
    public static final int num2 = 1;
}
static与final搭配问题#
public class InitializationTest2 {
	public static int a = 1; // 在初始化阶段赋值 有<clinit>
	public static final int INT_CONSTANT = 10;	//在链接阶段的Prepare环节赋值

	public static Integer INTEGER_CONSTANT1 = Integer.valueof(100);// 在初始化阶段赋值 有<clinit>
	public static final Integer INTEGER_CONSTANT2 =Integer.valueOf(1000);// 在初始化阶段赋值 有<clinit>
	
    public static final String s0 = "helloworld0"; // 在链接阶段的准备阶段赋值
	public static final String s1 = new String("helloworld1");// 在初始化阶段赋值
    
	public static String s2 = "helloworld2"; // 在初始化阶段赋值
	public static final int NUM1 = new Random().nextInt(bound:10); // 在初始化阶段赋值
    
    static a = 9;
    static final b = a;		// 要先有a才行,b一开始先赋值默认值
}

使用static + final 修饰的成员变量,称为:全局常量

什么时候在链接阶段准备环节:给此全局常量赋值是字面量或常量,不涉及方法或构造器的调用

clinit()的调用会死锁吗#

会死锁

面试题#
  • 哪些情况会触发类的加载?(京东) / 类加载的时机(百度)

类的加载 = 装载 + 链接 + 初始化

主动使用:

  1. 当创建一个类的实例,比如new,或通过反射克隆反序列化
  2. 调用类的静态方法时,即当使用了字节码invokestatic
  • Class的forName(“Java.lang.String”)和Class的getClassLoader()的loadClass(“Java.lang.String”)有什么区别?(百度)

4. Using(使用)#

5. Unloading(卸载)#

类的加载器#

什么是类加载器?类加载器有哪些?(苏宁、拼多多、百度、腾讯、字节)

作用#

类加载器是 JVM 执行类加载机制的前提。

ClassLoader作用ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。

显式加载 vs 隐式加载#

  • 显式加载:主动加载,在代码中调用ClassLoader加载class对象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加载class对象。

  • 隐式加载:不直接在代码中调用ClassLoader加载class对象,如在加载某个class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类通过JVM自动加载到内存中。

    例如:System.out.println()会自动将流加载到内存中

类加载机制的必要性#

  • 避免开发中遇到 java.lang.ClassNotFoundException 异常或 java.lang.NoClassDefFoundError 异常时手足无措,只有了解类加载机制才能在出现异常时快速定位问题。
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要和类加载器打交道了。
  • 开发人员可以编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义处理逻辑。

类的唯一性#

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java中的唯一性。每一个类加载器,都拥有一个独立的类名称空间;比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即时这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

基本特征#

  • 双亲委派模型
  • 可见性:子类加载器可以访问父类加载器加载的类型,反之不可。
  • 单一性:父类加载器加载过的类型,子类不会重复加载。类加载器”邻居“之间,同一类型仍可以被加载多次,因为互相并不可见。

ClassLoader#

1. 分类#

分成两类

  • 引导类加载器(Bootstrap ClassLoader)(C编写)
  • 自定义类加载器(User-Defined ClassLoader)(Java编写)

ClassLoader

最上面的是引导类加载器,下面四个是自定义类加载器。在整个图中,下面的加载器可以调用上面的,上面不可调用下面的。

2. 子父类加载器的关系#

看似是继承关系,实际上是包含关系!

class ClassLoader {
    ClassLoader parent;	// 父类加载器
    public ClassLoader(ClassLoader parent) {
        this.parent = parent;
    }
}

3. 具体类的加载器介绍#

引导类加载器#

引导类加载器(Bootstrap ClassLoader)

  • 使用C/C++实现,嵌套在JVM内部
  • 用来加载Java的核心库,用于提供JVM自身需要的类
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • 出于安全考虑,引导类加载器只加载包名为javajavaxsun开头的类
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器

扩展类加载器#

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 继承于ClassLoader
  • 父类加载器为启动类加载器
  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK安装目录jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

系统类加载器#

系统类加载器(AppClassLoader)

  • Java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 继承于 ClassLoader
  • 父类加载器为扩展类加载器
  • 负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器
  • 是用户自定义类加载器的默认父加载器
  • 通过 ClassLoadergetSystemClassLoader() 方法可以获取该类加载器

4. 用户自定义类加载器#

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Ec1ipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
  • 同时,自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
  • 所有用户自定义类加载器通常需要继承于抽象类java.lang.ClassLoader。
public class ClassLoaderTest {
    public static void main(String[l args) {
        // 获取系统该类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
        // 获取扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);
        // 试图获取引导类加载器:失败
        // 引导类加载器是C语言编写的 所以获取不到
        ClassLoader bootstrapclassLoader = extClassLoader.getParent();
        System.out.println(bootstrapclassLoader);
    }
}

5. ClassLoader源码解析#

public abstract class ClassLoader {
		private final ClassLoader parent;	// 看似是继承关系,实际上应该是包含关系
  	public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
  	// 双亲委派机制
  	protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 看看是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
              	// 没加载过
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {	// 先去看父类加载器
                        c = parent.loadClass(name, false);
                    } else {							// parent是null,说明父类加载器是引导类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 所有父类加载器都不能加载这个类,只能自己加载
                    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();
                }
            }
          	// 此时已经有 Class
          	// 看看是否需要解析,默认不解析只Loading
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

自定义类的加载器#

为什么要自定义类加载器?#

  • 隔离加载类

在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。

(类的仲裁—>类冲突)

  • 修改类加载的方式

类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载

  • 扩展加载源

比如从数据库、网络、甚至是电视机机项盒进行加载

  • 防止源码泄漏

Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

实现方式(两种)#

面试题:手写一个类加载器Demo(百度)

已知有类User

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

编译成.class文件后放在某个目录下

package cn.lettle.classloader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

// 自定义类加载器
public class UserDefineClassLoader extends ClassLoader {
    private String rootPath;

    public UserDefineClassLoader(String _rootpath){
        rootPath = _rootpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 转换为以文件路径表示的文件
        String filepath = classToFilePath(name);
        // 获取指定路径的class文件对应的二进制流数据
        byte[] data = getBytesFromPath(filepath);
        // 自定义 ClassLoader 内部调用 defineClass()
        return defineClass(name, data, 0, data.length);
    }

    private byte[] getBytesFromPath(String filepath) {
        FileInputStream fis = null;
        ByteArrayOutputStream baos = null;
        try {
            fis = new FileInputStream(filepath);
            baos = new ByteArrayOutputStream();

            byte[] buffer = new byte[1024];
            int len;

            while ((len = fis.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fis != null)
                    fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    private String classToFilePath (String name) {
        return rootPath + "/" + name.replace('.', '/') + ".class";
    }

    // 希望可以加载 com.lettle.test.classloader.User
    public static void main(String[] args) {
        try {
            UserDefineClassLoader loader1 = new UserDefineClassLoader("/Users/lettle/Documents/Java/JavaTests/src");
            Class userClass1 = loader1.findClass("cn.lettle.classloader.User");
            System.out.println(userClass1);

            UserDefineClassLoader loader2 = new UserDefineClassLoader("/Users/lettle/Documents/Java/JavaTests/src");
            Class userClass2 = loader2.findClass("cn.lettle.classloader.User");
            System.out.println(userClass2);


            // 两个类不是一个类加载器来加载的,故 false
            // 这样就实现类的隔离了
            System.out.println(userClass1 == userClass2);

            System.out.println(userClass1.getClassLoader());
            System.out.println(userClass1.getClassLoader().getParent());


        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

输出:

class cn.lettle.classloader.User
class cn.lettle.classloader.User
false
cn.lettle.classloader.UserDefineClassLoader@735f7ae5
jdk.internal.loader.ClassLoaders$AppClassLoader@34340fab

相关机制#

双亲委派机制#

parentLoader

定义#

双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现。该

接口的逻辑如下:

  1. 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
  2. 判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name,false)接口进行加载。
  3. 反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNu11(name)接口,让引导类加载器进行加载。
  4. 如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。

双亲委派的模型就隐藏在这第2和第3步中。

举例#

假设当前加载的是java.lang.Object这个类,很显然,该类属于JDK中核心得不能再核心的一个类,因此一定只能由引导类加载器进行加载。当JVM准备加载java.lang.Object时,JVM默认会使用系统类加载器去加载,按照上面4步加载的逻辑,在第1步从系统类的缓存中肯定查找不到该类,于是进入第2步。由于从系统类加载器的父加载器是扩展类加载器,于是扩展类加载器继续从第1步开始重复。由于扩展类加载器的缓存中也一定查找不到该类,因此进入第2步。扩展类的父加载器是null,因此系统调用findClass(String),最终通过引导类加载器进行加载。

思考#

如果在自定义的类加载器中重写java.lang.ClassLoader.loadClass(String)java.lang.ClassLoader.loadClass(String,boolean)方法,抹去其中的双亲委派机制,仅保留上面这4步中的第1步与第4步,那么是不是就能够加载核心类库了呢?这也不行!因为JDK还为核心类库提供了一层保护机制。不管是自定义的类加载器,还是系统类加载器或扩展类加载器,最终都必须调用java.lang.ClassLoader.defineClass(String, byte[], int, int,ProtectionDomain)方法,而该方法会执行preDefineClass()接口,该接口中提供了对JDK核心类库的保护。

优势#

  • 避免重复加载
  • 保护程序安全,防止核心API被篡改

弊端#

  • 委托过程单向:只能从下面的调用上面的

破坏双亲委派机制及举例#

  • 为了避免双亲委派机制的弊端
  1. JDBC

JDBCClassloader

  1. 热替换

什么是tomcat类加载机制?#

Tomcat

Tomcat的类加载机制是违背了双亲委派机制的

沙箱安全机制#

类加载结构新的变化#

JVM理论
https://fuwari.vercel.app/posts/jvm/
作者
Lettle
发布于
2025-01-15
许可协议
CC BY-NC-SA 4.0