理解JVM(6)Class装载系统

本文详细介绍了Java类加载过程,包括加载、验证、准备、解析和初始化五个阶段,并深入探讨了双亲委托机制及其可能引发的问题,同时给出了类热替换的实现思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Class文件的装载流程

这里写图片描述

类装载的条件(主动使用)

  • 创建一个实例,使用new,或者反射,序列化,克隆
  • 当调用类的static方法,使用了invokestatic指令
  • 使用了类或接口的static字段,使用了getstatic或putstatic指令,但final static的字段除外,这是常量池里的
  • 使用反射,反射类的方法时
  • 当初始化子类时,要先初始化其父类
  • 有启动虚拟机的main()方法的那个类

被动使用

  • 子类调用父类的static字段,会让父类初始化,子类不会被初始化。只有直接定义该字段的类,才会被初始化。而子类此时已经被加载了,只是没有被初始化。使用-XX:+TraceClassLoading能跟踪类的初始化。
  • final常量会直接放进常量池,javac在编译时候会将常量直接植入目标类,不再使用被引用类。

1.加载类

这是类装载的第一阶段。通过类的全名获取类的二进制流,解析二进制流为方法区的数据结构,创建Class对象,表示这个类。
    //通过forName()来获取Class实例
    val clazz = Class.forName("java.lang.String")
    val methods = clazz.declaredMethods
    for (i in methods){
        val mod = Modifier.toString(i.modifiers)
        print("$mod ${i.name} (")
        val clazzs = i.parameterTypes
        if (clazzs.size == 0) print(")")
        for (j in clazzs){
            print("${j.simpleName} ,")
        }
        print(")")
        println("")
    }

2.验证类

3.准备

虚拟机会为这个类分配相应的内存空间,并设置初始值。如int被初始化为0,其中boolean类型比较特殊,内部实现还是int,boolean默认为0,即false。

4.解析

解析阶段就是将类,接口,字段,方法的符号引用转为直接引用。如果直接引用存在,则肯定系统中存在某类,如果只存在符号引用,则不一定存在该对象。
public class Test {
    public static void main(String...args){
        System.out.println("lalalalala");
    }
}

//先javac编译,再javap -c查看 
Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String...);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String lalalalala
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}
其中invokevirtual使用类常量池#4,找到后,再沿着递归树往下找。

这里写图片描述

初始化

初始化是类加载的最后一个阶段。重要工作是执行类的初始化方法。会生成一个(clinit)函数的调用,虚拟机会保证其线程安全性,当多线程试图初始化同一个类时,只有一个线程可以进入clinit,其他线程必须等待。
所以有死锁的可能性
public class StaticA {
    static {
        try {
            Thread.sleep(1000);
        }catch (Exception r){}
        try {
            Class.forName("StaticB");
        }catch (Exception r){}

        System.out.println("StaticA init OK");
    }
}

public class StaticB {
    static {
        try {
            Thread.sleep(1000);
        }catch (Exception r){}
        try {
            Class.forName("StaticA");
        }catch (Exception r){}

        System.out.println("StaticB init OK");
    }
}

public class Main {

    public static void main(String...args) {
        new Thread(() -> {
            try {
                Class.forName("StaticA");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            System.out.println("A finished");
        }).start();

        new Thread(() -> {
            try {
                Class.forName("StaticB");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            System.out.println("B finished");
        }).start();
    }

}
线程1试图去初始化StaticA,线程2试图去初始化StaticB,在StaticA的初始化过程中,会去尝试初始化StaticB, 这种状况下会导致死锁。最坑的是,用jstack去看的时候,也不能发现死锁。

ClassLoader

ClassLoader主要作用是从系统外部获取Class二进制数据流,然后交给JVM进行连接,初始化等操作。因此ClassLoader在整个装载阶段只影响类的加载,而无法通过ClassLoader去改变类的连接和初始化行为。
  • loadClass(String name):给定类名,加载一个类,返回Class实例
  • defineClass(byte[] b):给定二进制流,定义一个类。只在ClassLoader的子类中才能使用。
  • findClass(String name):查找一个类,会在loadClass()时候用得到,用于自定义查找类的逻辑。
  • findLoadedClass(String name): 查找已经被加载的类,final方法

这里写图片描述

双亲委托机制

在加载的时候,系统会判断当前类是否已经被加载,如果已经被加载了,就会直接返回可用的类,否则就会请求委托双亲加载,如果双亲加载失败,则会自己加载。
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
        synchronized(this.getClassLoadingLock(var1)) {
            Class var4 = this.findLoadedClass(var1);
            if (var4 == null) {
                long var5 = System.nanoTime();

                try {
                    if (this.parent != null) {
                        var4 = this.parent.loadClass(var1, false);
                    } else {
                        var4 = this.findBootstrapClassOrNull(var1);
                    }
                } catch (ClassNotFoundException var10) {
                    ;
                }

                if (var4 == null) {
                    long var7 = System.nanoTime();
                    var4 = this.findClass(var1);
                    PerfCounter.getParentDelegationTime().addTime(var7 - var5);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
                    PerfCounter.getFindClasses().increment();
                }
            }

            if (var2) {
                this.resolveClass(var4);
            }

            return var4;
        }
    }

双亲委派的弊端

即顶层的ClassLoader无法访问底层的ClassLoader加载的类。在一般情况下顶层的ClassLoader加载的系统类,应用类加载器加载应用类,应用类自然能访问系统类。但特殊情况下,系统类访问应用类就出现问题。比如,系统类提供一个接口,该接口由应用类自己实现,并绑定一个一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中,这时就会出现工厂方法无法创建由类加载器加载的应用实例的问题。(如JDBC)

从JDBC看破坏双亲委派

java中存在一种SPI(Service Provider Interface)机制,该机制将不同厂商的实现抽象成一种通用的规范接口或者模块,比如这里的JDBC模块,不同数据库厂商针对统一的JDBC规范进行定制化的实现,对于JVM来说JDBC相关的类文件在rt.jar中,典型的有java.sql.Drvier.class、java.sql.DriverManager.class等文件
如上面所说rt.jar是由引导类加载器BootStrap ClassLoader负责加载的,而我们引入的第三方实现比如com.mysql.jdbc.Driver.class却是由应用类加载器加载的,在程序运行时要对具体数据库实现进行操作时才能知道需要用的具体实现类,这就出现问题了,BootStrap是不能逆向加载Application中的实现类的,但具体操作时又必须使用具体的实现类
object JDBCUtil {
    init {
        Class.forName("com.mysql.jdbc.Driver")
    }

    @Throws(SQLException::class)
    fun getConnection(url: String): Connection {
        return DriverManager.getConnection(url)
    }
}

//跟踪一下
public static Connection getConnection(String var0) throws SQLException {
        Properties var1 = new Properties();
        return getConnection(var0, var1, Reflection.getCallerClass());
        //这里的Reflection.getCallerClass()就是上面的JDBCUtil类
}

//在跟踪一下
private static Connection getConnection(String var0, Properties var1, Class<?> var2) throws SQLException {
        ClassLoader var3 = var2 != null ? var2.getClassLoader() : null;
        Class var4 = DriverManager.class;
        //
        synchronized(DriverManager.class) {
            if (var3 == null) {
                //这个var3是应用类加载器
                var3 = Thread.currentThread().getContextClassLoader();
            }
        }

        if (var0 == null) {
            throw new SQLException("The url cannot be null", "08001");
        } else {
            println("DriverManager.getConnection(\"" + var0 + "\")");
            SQLException var10 = null;
            Iterator var5 = registeredDrivers.iterator();

            while(true) {
                while(var5.hasNext()) {
                    DriverInfo var6 = (DriverInfo)var5.next();
                    //这里是破坏的核心的代码
                    if (isDriverAllowed(var6.driver, var3)) {。。。。。

//var0是mysql.Driver
//var1是上面获取的应用类加载器
private static boolean isDriverAllowed(Driver var0, ClassLoader var1) {
        boolean var2 = false;
        if (var0 != null) {
            Class var3 = null;

            try {
                //这步是加载类的关键,将要加载的类和应用类加载器传入
                var3 = Class.forName(var0.getClass().getName(), true, var1);
            } catch (Exception var5) {
                var2 = false;
            }

            var2 = var3 == var0.getClass();
        }

        return var2;
    }


public static Class<?> forName(String var0, boolean var1, ClassLoader var2) throws ClassNotFoundException {
        Class var3 = null;
        SecurityManager var4 = System.getSecurityManager();
        if (var4 != null) {
            var3 = Reflection.getCallerClass();
            if (VM.isSystemDomainLoader(var2)) {
                ClassLoader var5 = ClassLoader.getClassLoader(var3);
                if (!VM.isSystemDomainLoader(var5)) {
                    var4.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }

        //var0是要加载的类全名,mysql.Driver
        //var2是App ClassLoader
        //var3是DriverManager.class
        return forName0(var0, var1, var2, var3);
    }


热替换

同一个类,只要是由不同的类加载器加载的,在JVM是两个Class对象。所以实现热部署的思路就是创建新的ClassLoader去加载改变的类。

这里写图片描述

自定义类加载器
class MyClassLoader(val fileName: String) : ClassLoader(){
    override fun findClass(className: String?): Class<*> {
        var clazz: Class<*>? = this.findLoadedClass(className)
        if (clazz == null){
            val classFile = getClassFile(className);
            val fis = FileInputStream(classFile)
            val fileC = fis.channel
            val baos = ByteArrayOutputStream()
            val outC = Channels.newChannel(baos)
            val buffer = ByteBuffer.allocateDirect(1024)
            while (true){
                val i = fileC.read(buffer)
                if (i == 0 || i == -1){
                    break
                }
                buffer.flip()
                outC.write(buffer)
                buffer.clear()
            }
            fis.close()
            val bytes = baos.toByteArray()
            clazz = defineClass(className,bytes,0,bytes.size)
        }
        return clazz!!
    }




    fun getClassFile(className: String?): String{
        return className!!
    }
}
要热加载的类
class HotA{
    fun hot(){
        println("I am hot A")
    }
}

入口文件

fun main(args: Array<String>){
    while (true){
        val loader = MyClassLoader("src/main/")
        val clazz = loader.loadClass("HotA")
        val instance = clazz.newInstance()
        val methodHot = instance.javaClass.getMethod("hot")
        methodHot.invoke(instance)
        Thread.sleep(10000)

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值