十一.吊打面试官系列-JVM优化-深入JVM类加载机制

前言

从本篇文章开始我们来探讨JVM相关的知识,内容附带JVM的启动,JVM内存模型,JVM垃圾回收机制,JVM参数调优等,跟着文章一步一步走相信你对JVM会有一个不一样的认识,如果觉得文章对你有所帮助请给个好评吧。

JVM类加载系统

1.JVM的启动流程

当我们编写好Java源代码之后如hello.java ,通过javac命令把hello.java文件编译为hello.class文件,然后通过java.exe去执行hello.class 字节码文件,这个时候Java会启动JVM虚拟机,虚拟机是通过jvm.ddl文件创建的,底层是由C++实现的,具体的启动流程如下
在这里插入图片描述
对上图做一个步骤解释:

  1. 编译源代码:使用Java编译器(javac)将Hello.java编译成字节码文件(Hello.class)。这一步将源代码转换成JVM能够理解的指令集。
  2. 启动JVM:通过命令行界面调用java.exe来启动Java虚拟机(底层是由C++来实现的),java.exe是JVM的入口点,负责加载和运行Java应用程序。
  3. 创建启动类加载器:JVM在启动时,会首先加载BootstrapClassLoader启动类加载器(这个类加载器也是c++实现的),它是Java类加载器体系的最顶层加载器,负责加载核心类库。
  4. 创建启动器:启动类加载器会加载Launcher , C++会调用Java代码创建JVM启动器:sun.misc.Launcher ,该启动器的作用是用来加载器其他的类加载器,比如:AppClassLoader应用类加载器
  5. 加载应用类:Launcher 会根据当前类Hello.class找到其ClassLoader并加装它,也就是AppClassLoader应用类加载器,它负责加载用户自定义的Java类(如Hello类)
  6. 加载Class : 通过AppClassLoader 加载Hello.class字节码文件
  7. 执行Main方法:找到class类中的main方法,JVM通过调用类的main方法作为程序的入口点来执行Java程序(这一步也是c++调用的)
  8. 运行Java程序:Java程序开始执行,直到遇到main方法结束或者发生异常而终止。

2.类加载器

当我们在Java中编写代码并引用某个类时,这个类是如何被加载到JVM中的呢?这涉及到JVM的类加载器(ClassLoader)以及类加载的流程和双亲委派机制。下面我将详细解释这些概念。

JVM的类加载器是负责将类的字节码文件(通常是.class文件)加载到JVM中,并为其生成对应的Class对象的过程。类加载器是Java运行时环境的一部分,是Java程序获取字节码文件的重要途径。

在这里插入图片描述

JVM提供了三种主要的类加载器:

  • 引导类加载器(Bootstrap ClassLoader):这是JVM的内置类加载器,主要负责加载Java的核心类库,一般对应JAVA_HOME/lib 目录中的JAR包,如java.lang.*、java.util.*等。由于它并不是Java类库的一部分,而是JVM自身的实现,所以它并不继承自java.lang.ClassLoader。
  • 扩展类加载器(Extension ClassLoader):这是Java的标准扩展类加载器,负责加载Java的扩展类库,一般对应JAVA_HOME/lib/ext目录中的JAR包。它是java.lang.ClassLoader的子类,由sun.misc.Launcher$ExtClassLoader实现。
  • 应用类加载器:Application ClassLoader:也称为系统类加载器(ApplicationClassLoader)或默认类加载器(System ClassLoader),负责加载应用程序的类路径(classpath)下的所有类包括pom.xml导入的jar。它是java.lang.ClassLoader的子类,由sun.misc.Launcher$AppClassLoader实现。在Java应用程序中,我们通常使用的就是这个类加载器。

除了以上三种主要的类加载器,我们还可以自定义类加载器,通过继承java.lang.ClassLoader类并重写其相关方法来实现。

注意:这些类加载器并没有实际上的继承关系

3.双亲委派机制

JVM的双亲委派机制是Java语言服务器级别的安全策略,主要思想是在类加载过程中,子类委托给父类加载器优先加载,即:当一个类需要被加载时,它首先会委托给其父类加载器进行加载。如果父类加载器无法加载该类,那么子类加载器才会尝试自己加载,如果父类加载过了子类就不会再加载了。这种层层委派的方式确保了类加载的有序性和唯一性
在这里插入图片描述
双亲委派的好处在于:

  1. 类加载安全策略:例如我们自己编写一个 java.lang.String 是否会覆盖java自带的String类呢?答案是无法覆盖,因为BootStrapClassLoader优先加载了java.lang.String 后,AppClassLoader在加载我们自己的java.lang.String的时候会检查重复加载,也就不会再加载了。保证了类的唯一性
  2. 保证有序性 : JVM启动必须要加载一些基础的类,比如:Object.clas 这些基础的类会通过BootstrapClassLoader 和 ExtClassLoader 优先加载后,再加载我们自己的类,否则JVM无法启动,启动也会报错。

下面是AppClassLoader的类加载源码,可以看得出来JVM在加载类之前会查找类是否已经被加载,如果没加载就会调用 parent.loadClass 让父类优先加载,如果加载失败再调用findClass自己加载

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
        	//1.首先,检查类是否已加载
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//如果父类加载器不为空,则优先委派父类进行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果父类加载器为空,则查找 Bootstrap 类加载器,如果找不到则返回null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //如果c == null 说明父类加载失败,则自己加载
                    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;
        }
    }


4.打破双亲委派

在JVM中,类加载器的双亲委派机制是一种默认的行为,它确保了类加载的有序性和安全性。然而,在某些特殊情况下,开发者可能需要打破这种机制来实现特定的功能,例如热部署、插件化开发等。以下是一些打破双亲委派机制的方法:

  1. 自定义类加载器

最直接的方式是创建自定义的类加载器,并在其加载类的过程中不遵循双亲委派机制。这可以通过在自定义类加载器的loadClass方法中直接加载类,而不是先调用父类加载器的loadClass方法来实现。

public class CustomClassLoader extends ClassLoader {  
    @Override  
    public Class<?> loadClass(String name) throws ClassNotFoundException {  
        // 自定义加载逻辑,不调用super.loadClass(name)  
        // ...  
    }  
}

然而,这样做可能会破坏Java的安全模型,因为它允许自定义类加载器加载Java核心库中的类,这可能会导致安全问题。

  1. 使用线程上下文类加载器

在Java中,每个线程都有一个与之关联的上下文类加载器(ContextClassLoader)。这个类加载器可以通过Thread.currentThread().getContextClassLoader()来获取。线程上下文类加载器为Java应用程序提供了一种在运行时动态加载类的方式,而不必受双亲委派机制的限制。

Thread.currentThread().setContextClassLoader(new CustomClassLoader());

然后,可以使用这个上下文类加载器来加载类,而不是使用默认的类加载器。

  1. 使用Java的代理类加载器

在某些情况下,可以使用Java的代理类加载器(如URLClassLoader)来加载类,这些类加载器提供了更多的灵活性来加载类。虽然它们通常遵循双亲委派机制,但可以在必要时被修改或扩展来打破这个机制。

初始之外还有其他的方式比如:使用Java 9的模块化系统(JPMS),但它提供了一种新的方式来管理类的加载和隔离。 ; 或者使用使用OSGi(Open Service Gateway initiative)它提供了自己的类加载器机制,允许不同的模块(bundle)独立地加载和管理类,这个一般我们接触的较少。

需要注意的是,打破双亲委派机制可能会导致类加载和安全性方面的问题。因此,在决定这样做之前,应该仔细考虑其潜在的影响,并确保采取了适当的措施来确保系统的稳定性和安全性。

我们熟知的Tomcat就打破了双亲委派机制,它通过自定义类加载器的方式来实现APP应用加载隔离具体的内容请看《深入源码剖析Tomcat如何打破双亲委派

5.类加载流程

JVM(Java虚拟机)的类加载流程主要包括:加载,验证,准备,解析,初始化 几个阶段:
在这里插入图片描述
类的加载、验证、准备、解析和初始化这五个阶段通常被称为类的链接(Linking)过程。

  1. 加载(Loading):

    使用到某个类时,JVM会通过类的全限定名查找和加载class文件,并通过IO读入字节码文件,将其加载到JVM中,在方法区(元空间)会存储好class,而在堆内存中会生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  2. 验证(Verification):

    确保被加载的类的正确性和安全性。包括文件格式验证、元数据验证、字节码验证和符号引用验证。

  3. 准备(Preparation):

    为类的静态变量分配内存,并将其初始化为默认值,比如 static int a = 1 , 在这里会赋初始值 0。这里不包括用final修饰的静态变量,因为final在编译的时候就会分配了,准备阶段会显式赋值

  4. 解析(Resolution):

    把类中的符号引用转换为直接引用。这主要是将类名、字段名、方法名等符号引用转换为指向方法区中的实际内存地址的直接引用,这个过程叫:静态链接,而动态链接是在程序运行期间完成的将符号引用替换为直接引用

  5. 初始化(Initialization) :

    为类的静态变量赋予正确的初始值,也就是第三步准备阶段的今天变量赋正确的初始值(如果有的话)。这个阶段会执行类构造器<clinit>()方法,这是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static块)中的语句合并产生的。

  6. 使用(Using):

    类的初始化完成后,就可以通过实例化类、调用类的静态方法等方式来使用这个类了。

  7. 卸载(Unloading):

    当类的生命周期结束时,JVM的垃圾回收机制会回收类的内存,这个过程称为类的卸载。但在Java中,类的卸载是由JVM来控制的,开发者通常不需要显式地卸载类。

在这里插入图片描述

文章对你有帮助请给个好评,下一章:JVM优化-深入JVM内存模型

  • 27
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨家巨子@俏如来

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值