(一)剖析JVM类加载机制

目录

一、java类加载过程

1.1、类加载过程

二、类加载器和双亲委派机制

2.1、类加载器分类

2.2、类加载器的初始化过程

2.3、双亲委派机制

三、自定义类加载器与打破双亲委派机制

3.1、自定义类加载器

3.2、自定义类加载器示例

3.3、那么如何打破双亲委派呢?


一、java类加载过程

1.1、类加载过程

当我们用java命令运行某个类的main函数启动程序时,首先是由java.exe调用jvm.dll创建Java虚拟机,再由其虚拟机创建一个引导类加载器实例,调用java代码创建jvm启动器实例sun.misc.Launcher,获取运行类Launcher相应类加载器后,加载执行的类资源后调用其main执行的入口方法,执行相关的业务逻辑代码后,Java程序运行结束,JVM就销毁,相大致流程如下:

其中loadClass的类加载过程有如下几步:加载 >> 验证 >> 准备 >> 解析 >> 初始化 

1、加载:从磁盘上读取类的class文件;

2、验证:主要是验证读取的class文件的格式是否是正确的;

3、准备:将定义好的静态变量设置默认值,分配内存,如静态变量int型赋值0静态变量布尔型

赋值false等;

4、解析:将符号引用转变为直接引用,直接引用中分为静态连接和动态连接;

5、初始化:对类的静态变量初始化为指定的值,执行静态代码块;

类被加载到方法区中后主要包含运行时常量池、类信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中,如下图math实例, 作为开发人员访问方法区中类定义的入口和切入点。

注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。

jar包或war包里的类不是一次性全部加载的,是使用到时才加载

package com.jvm.zm01;

/**
 * 主类在运行过程中如果使用到其它类,会逐步加载这些类。
 * jar包或war包里的类不是一次性全部加载的,是使用到时才加载
 *@Author : hongzm
 *@Date: 2023-09-23 12:54
 *@Version: 1.0
 */
public class TestDynamicLoad {
    static {
        System.out.println("*********************load TestDynamicLoad*******************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("**************load tset****************");
        B b = null; // 这里b不会加载,除非这里执行new B()
    }

}


/**
 * 一定是先加载(如static 静态语句块)然后才初始化(实例化,如 new A())
 */
class A{
    static {
        System.out.println("**************load A****************");
    }

    public A(){
        System.out.println("**************init A****************");
    }
}


class B{
    static {
        System.out.println("**************load B****************");
    }

    public B(){
        System.out.println("**************init B****************");
    }
}


运行结果
*********************load TestDynamicLoad*******************
**************load A****************
**************init A****************
**************load tset****************

二、类加载器和双亲委派机制

2.1、类加载器分类

java中的类加载器主要可以分为四类:引导类加载器、扩展类加载器、应用类加载器、自定义加载器

1、引导类加载器BootstrapLoaderC++实现):

负责加载支撑JVM运行的位于JRE的lib目录下核心类,如rt.jar、charsets.jar等;

2、扩展类加载器ExtClassLoader

负责加载支撑JVM运行的位于JRE的lib目录下的ext拓展目录中jar类包;

3、应用程序类加载器AppClassLoader

负责加载ClassPath路径下类包(自己写的类:target目录下

4、自定义加载器

负责加载用户自定义路径下的类;

package com.jvm.zm02;

import com.sun.crypto.provider.DESKeyFactory;
import sun.misc.Launcher;

import java.net.URL;

/**
 *
 *@Author : hongzm
 *@Date: 2023-09-23 13:03
 *@Version: 1.0
 */
public class TestJDKClassLoader {

    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());//rt.jar包下  输出null,因为引导类加载器是用c++实现,在java环境取不到信息
        System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());// jre/lib/ext包下    输出sun.misc.Launcher$ExtClassLoader
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());//用户自定义    输出sun.misc.Launcher$AppClassLoader

        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");// 引导类加载加载的
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urLs.length; i++) {
            System.out.println(urLs[i]);
        }
        System.out.println();

        System.out.println("extClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println();

        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));

    }
}


执行结果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/classes

extClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext;
C:\WINDOWS\Sun\Java\lib\ext

appClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_201\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\javaws.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\management-agent.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\plugin.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\rt.jar;
C:\Program Files\JetBrains\IntelliJ IDEA 2018.3.3\lib\idea_rt.jar

Process finished with exit code 0

2.2、类加载器的初始化过程

创建JVM启动器实例sun.misc.Launcher。在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)

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

1)构造扩展类加载器var1 = Launcher.ExtClassLoader.getExtClassLoader();,在构造的过程中将其父加载器设置为null;

2)构造应用类加载器this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);,在构造的过程中将其父加载器设置为ExtClassLoader;

2.3、双亲委派机制

1、先看一个类加载到JVM的过程是一个什么样的

接2.2小结所说,JVM默认使用Launcher的getClassLoader()方法返回的类加载器为:AppClassLoader实例,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,大体流程如下:

首先,会检查指定的类是否已经加载过了,如果加载过了,直接返回,底层的逻辑代码如下:

如果这个类没有被加载过,就先判断其是否有父加载类,如果有,就调父加载类进行加载,如果没有,就调用bootstrap加载类加载,图示如下,

AppClassLoader父加载器是ExtClassLoader,

如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载,如下图示:

那我们再回到最初的问题,加载一个类的过程,首先是会先判断该类在已加载的列表中是否存在,存在直接返回,不存在就先判断是否有父加载类,有的话,委托给父加载器加载,以此类推,如果引导类bootstrap加载到不到目标类,则向下退回加载类的请求,由子类自己加载,以此逐级退回,这样一个过程,我们就称之为类加载的双亲委派机制,简单的说就是先找父亲加载,加载不到再由儿子自己加载。

2、那为什么要设计双亲委派机制呢

主要有以下两方面:

沙箱安全机制:主要是能够保护java核心类库资源不会被其他新写的同包同名的类给覆盖掉,这样子可以防止核心的API库被篡改;
类不被重复加载:当父类已经加载过该类了,那就没有必要让子加载类再重新加载一遍,这可以保证加载类的唯一性;

示例验证

用户编写一个java.lang.String类,里面有一个main(String[] args)方法,执行main方法;

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("**************My String Class**************");
    }
}

运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

分析:
由双亲委派机制可知,点击执行main方法后,在加载类过程中,应用程序类加载器向上委托拓展类加载器,拓展类加载器再向上委托引导类加载器,引导类加载器加载JRE的lib目录下核心类,
加载到包含路径为java.lang.String的类(没有main方法),就直接返回了,不会再去加载用户编写的同名类(沙箱安全机制),故报找不到main方法报错。

三、自定义类加载器与打破双亲委派机制

3.1、自定义类加载器

java.lang.ClassLoader 类有两个核心方法:loadClass(实现双亲委派逻辑),findClass(默认实现是空方法);

所以我们自定义一个类加载器只需要两步:

1)继承java.lang.ClassLoader

2)重写findClass方法默认实现是空方法

3.2、自定义类加载器示例

示例1:

将工程目录下的User.class文件拷贝到指定目录下,如D:\test\com\jvm\zm01,测试自定义类加载器加载class文件

/**
 * 自定义类加载器
 *@Author : hongzm
 *@Date: 2023-09-23 13:32
 *@Version: 1.0
 */
public class MyClassLoader extends ClassLoader{
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + 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 = loadByte(name);
                //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    }
   public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.jvm.zm01.User");
        System.out.println(clazz.getClassLoader());
    }


运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2

再次印证双亲委派机制,父加载器AppClassLoader先加载了User.class后,自定义加载器就不会重复加载

示例2:

删除项目里面的User.class文件,再测试

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.jvm.zm01.User");
        System.out.println(clazz.getClassLoader());
    }

运行结果:
com.jvm.zm02.MyClassLoaderTest$MyClassLoader@4554617c

3.3、那么如何打破双亲委派呢

打破双亲委派机制也就是不委托父类去加载自定义的类,必须再重写loadClass方法(打破双亲委派),也就是项目User.class文件不删除,实现用自定义类加载器加载我们自己的User.class,

/**
         *  重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();

                    //非自定义的类还是走双亲委派加载
                    if (!name.startsWith("com.jvm")){
                        c = this.getParent().loadClass(name);
                    }else{
                        c = findClass(name);
                    }

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
/**
     * 打破双亲委派
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //尝试用自己改写类加载机制去加载自己写的java.lang.String.class
        Class clazz = classLoader.loadClass("com.jvm.zm01.User");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }

运行结果
**********自定义类加载器加载类调用方法***********
com.jvm.zm02.MyClassLoaderTest$MyClassLoader

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值