1. 类加载机制

类加载器的创建时机

首先,当我们运行一个类时,会先获取这个类的类加载器,通过类加载器把类加载到JVM中。
Java虚拟机使用c++语言实现的,启动Java虚拟机,会创建一个引导类加载器Bootstrap ClassLoader,引导列加载器的创建也是c++语言实现的,所以获取它的时候总是打印null。

引导类加载器会通过Launcher.getLauncher()创建sun.misc.Launcher实例,这一步操作是c++代码调用Java代码,而Launcher是JVM的启动器,它的创建采用单例模式,保证了JVM只一份Launcher实例。
在Launcher实例的创建过程中会创建ExtClassLoader和AppLClassLoader的实例,这两个类加载器都继承自URLClassLoader类,除了引导类加载器没有父加载器外,其他的类加载器的根类都是ClassLoader类。

在ExtClassLoader和AppClassLoader的构造过程中,会调用父类URLClassLoader的构造器,会有一个URL[],里面存放的是类加载自己所要加载的文件的路径;还有一个参数是parent,ClassLoader类型,当构造AppClassLpader的时候会把ExtClassLoader的实例作为parent传入,然后在URLClassLoader的构造方法中会调用根类的一个构造方法,把parent设为AppClassLoader的父加载器。
父加载器并非父类,它们之间没有继承关系,只是通过parent这个成员变量设置了上下级关系

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

类加载器分类

1、引导类加载器(启动类加载器 Bootstrap ClassLoader):用来加载JRE目录下的lib目录下的核心类库
2、扩展类加载器(Extension ClassLoader): 用来加载JRE目录下lib目录下的ext扩展目录下的JAR类包
3、应用程序类加载器(AppClassLoader):用来加载classpath的类包,该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
4、自定义加载器:定制类的加载方式。自定义的类加载器默认设置AppClassLoader为父加载器 ,初始化的时候会先初始化父类加载器。

public class MyTest {
    public static void main(String[] args) {
        ClassLoader bootstrapLoader = String.class.getClassLoader();
        ClassLoader appClassLoader = MyTest.class.getClassLoader();
        ClassLoader extClassLoader = ZipInfo.class.getClassLoader();
        System.out.println("bootstrapLoader" +  bootstrapLoader);
        System.out.println("appClassLoader" + appClassLoader);
        System.out.println("extClassLoader" +extClassLoader);
    
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }
        
        System.out.println("appClassLoader加载以下文件:");
        String property = System.getProperty("java.class.path");
        System.out.println(property);
        
        System.out.println("extClassLoader加载以下文件:");
        String property1 = System.getProperty("java.ext.dirs");
        System.out.println(property1);
    }
}

运行结果:
在这里插入图片描述
ps:引导类加载器打印出来为null,因为它是用c++语言实现的对象,而非java

loadClass()

类加载过程,以及双亲委派机制的实现,都是通过ClassLoader的loadClass()方法实现,通过接受一个二进制文件名来加载这个Class。

类加载流程

加载、验证、准备、解析、初始化
一、加载:
在硬盘上查找并通过IO读取字节码文件,使用到类时才会加载类,在加载阶段会在堆中生成代表这个类的java.lang.Class对象,作为这个类的所有数据访问入口。在加载阶段,可通过系统提供的类加载器来完成加载,也可以自定义类加载器。
二、验证:
校验字节码文件是否符合Java虚拟机规范。
javap -v As.class 查反编译As的class文件。
三、准备:
给静态变量分配内存,并赋默认值(int->0,String->null),final修饰的变量是常量,会直接赋值。
四、解析:
将符号引用替换为直接引用,这是静态链接的过程。
符号引用:public,main,String等这些都可以称为符号,可以是任何字面量。
直接引用:指数据加载到内存区具备一个内存地址,根据地址可定位到目标的指针或句柄。
动态链接在类加载的时候不会链接,即代码不对被解析为内存地址,运行到它的时候才链接。
五、初始化:
对静态变量赋正确初始值。两种方式:声明类变量指定初始值、执行静态代码块为类变量赋值。
类初始化的步骤
1、假如这个类还没有被加载和链接,程序先加载并链接该类;
2、假如该类的直接父类还没被初始化,先初始化其直接父类;
3、假如类中有初始化语句,则依次执行这些初始化语句。
类初始化时机:
只有当对类的主动使用才会导致类的初始化,类的主动使用包括六种:
1、创建类实例,new;
2、访问某个类的静态变量,或者对静态变量赋值;
3、调用类的静态方法;
4、反射(例如Class.forName(…))
5、初始化某个类的紫烈,则父类也会被初始化;
6、Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类。

双亲委派机制

在loadClass()方法中,调用findLoadedClass(String)检查当前类加载器是否已经加载过该类(只是判断它是否已经存在于被加载过的类中,自己并不会去加载它),已经加载过会直接返回。
未被加载过,如果parent(父类加载器)不为空,委派parent调用loadClass方法重新加载,
如果parent为空,委派引导类加载器bootstrap classloader来加载,
如果得到的class对象还是null,代表应用类加载器,扩展类加载器,引导类加载器都没有加载过该类,
此时会调用
findClass(name)
:bootstrapClassLoader尝试自己去加载,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException
ps:ClassLoader中的findClass(name)方法是一个空方法,URLClassLoader对它进行了重写,所以这里调用来源于URLClassLoader。

图解双亲委派:
在这里插入图片描述
为什么用双亲委派机制,好处?
1、沙箱安全机制:防止jdk核心类库被随意篡改。
2、避免类的重复加载:当父类已经加载了该类,就没必要子类加载器再加载一次,保证了加载类的唯一性。

全盘负责委托+缓存机制

全盘负责委托机制:
类加载器加载类用的是全盘负责委托机制,即当一个类加载器加载一个类的时候,这个类所依赖和引用的类通常也由这个类加载器负责加载。
缓存机制:
为什么修改了class必须重启虚拟机才生效?为什么class只加载一次?
类加载使用了缓存机制,如果缓存中保存了这个class文件就直接返回,没有的话就去读取和载入class,并存入缓存。

自定义类加载器

编写自定义类加载器,继承java.lang.ClassLoader类,通过重写loadClass(String,boolean)打破双亲委派,通过重写findClass实现类如何被载入到JVM。ClassLoader类的findClass是个空方法,它的子类URLClassLoader对它进行了重写,来实现类被加载的操作,现在要自定义类加载器,因此需要重写它。

不打破双亲委派

不打破双亲委派,只重写findClass方法,自定义加载规则。

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

/**
 * 自定义类加载器
 * 重写findClass(String)实现加载过程(进行类加载)
 */
public class MyClassLoader extends ClassLoader{
    
    private String classPath;
    
    public MyClassLoader(String classPath){
        this.classPath = classPath;
    }
    
    //获取class的字节数组
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.","/");//把.替换为/
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();//返回可以从此输入流中读取(或跳过)而不会被此输入流的方法的下一次调用阻塞的剩余字节数的估计
        byte[] data = new byte[len];
        fis.read(data);//将此输入流中最多data.length字节的数据读取到字节数组中
        fis.close();
        return data;
    }
    
    //重写此方法。
    //实现类的加载(进行类加载的操作)
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
    
    public static void main(String[] args) throws Exception{
        MyClassLoader myClassLoader = new MyClassLoader("D:\\test");
        //调用的是父类的loadClass方法,即未打破双亲委派机制
        //loadClass方法内部会去调用findClass方法,这里对findClass进行了重写,因为会执行这里的findClass方法
        Class aClass = myClassLoader.loadClass("com.xwl.User1", false);
        Object obj = aClass.newInstance();
        Method method = aClass.getDeclaredMethod("say",null);
        method.invoke(obj,null);
        System.out.println("查看类加载器是谁:");
        System.out.println(aClass.getClassLoader());//MyClassLoader@49476842
    }
}

在这里插入图片描述
ps:打印com.xwl.User1的类加载器,结果是自定义的类加载器,但是如果User1类在类路径下存在的话,再打印类加载就是AppClassLoader了。因为我们并没有打破双亲委派机制,所以父加载器已经加载过的类会直接返回。

打破双亲委派

打破双亲委派机制,重写loadClass(String,boolean)和findClass方法。


import sun.misc.PerfCounter;

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

/**
 * 自定义类加载器
 * 重写loadClass(String,boolean)实现/打破双亲委派机制(进行委托)
 * 重写findClass(String)实现加载过程(进行加载)
 */
public class MyClassLoader1 extends ClassLoader{
    
    private String classPath;
    
    public MyClassLoader1(String classPath){
        this.classPath = classPath;
    }
    
    //获取class的字节数组
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.","/");//把.替换为/
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();//返回可以从此输入流中读取(或跳过)而不会被此输入流的方法的下一次调用阻塞的剩余字节数的估计
        byte[] data = new byte[len];
        fis.read(data);//将此输入流中最多data.length字节的数据读取到字节数组中
        fis.close();
        return data;
    }
    
    //重写loadClass方法,打破双亲委派(进行委托的操作)
    //可以把ClassLoader的loadClass方法复制过来,对双亲委派那部分的逻辑进行修改
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 查看当前类加载器(自定义类加载器)已经加载过的类中是否有此类
            Class<?> c = findLoadedClass(name);
            //没有加载过
            if (c == null) {
                long t0 = System.nanoTime();
                long t1 = System.nanoTime();
                //由当前类加载器(自定义类加载器)加载此类
                c = findClass(name);
                
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
        //return super.loadClass(name, resolve);
    }
    
    //重写了此方法。
    //实现类的加载(进行类加载的操作)
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
    
    public static void main(String[] args) throws Exception{
        MyClassLoader1 myClassLoader = new MyClassLoader1("D:\\test");
        Class aClass = myClassLoader.loadClass("com.xwl.User1", false);
        Object obj = aClass.newInstance();
        Method method = aClass.getDeclaredMethod("say",null);
        method.invoke(obj,null);
        System.out.println("查看类加载器是谁:");
        System.out.println(aClass.getClassLoader());
    }    
}

运行报错:java.io.FileNotFoundException: D:\test\java\lang\Object.class (系统找不到指定的路径。)
这是因为自己定义的类依赖于Object类,现在我们加载类没有双亲委派,所以自定义类加载器在自己的加载路径下找不到Object.class就会报错。把Object.class放进去:

在这里插入图片描述
再次运行依旧报错:Exception in thread “main” java.lang.SecurityException: Prohibited package name: java.lang
这是因为jdk核心类库的API,只能由系统的类加载器来加载,不允许随便定义加载器加载。
改善loadClass方法:如果是自己定义的类,不走双亲委派,如果是jdk自己定义的类,走双亲委派。

@Override
    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) {
                long t0 = System.nanoTime();
                //如果是自己定义的类,不走双亲委派
                if(name.startsWith("com.xwl")){
                    c = findClass(name);
                }else{
                    //如果是jdk自己定义的类,走双亲委派
                    c = this.getParent().loadClass(name);
                }
                long t1 = System.nanoTime();
    
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
        //return super.loadClass(name, resolve);
    }

运行结果:
在这里插入图片描述

tomcat打破了双亲委派

在这里插入图片描述
tomcat类加载器机制:
Tomcat 为实现隔离性,每个 webappClassLoader 加载自己目录下的 class 文件,而不会传递给父类加载器,打破了双亲委派机制。
tomcat类加载流程:
1、先从缓存中加载,如果存在则直接返回该类。
2、委派jvm中的父类ExtClassLoader、BootStrapClassLoader,查看缓存中是否加载过该类。
3、从当前类加载器加载(WebAppClassLoader自己加载)顺序为/WEB-INF/classes, /WEB-INF/lib/.jar
4、还没有,则从父类加载器加载,依次 App、Common、shared 加载(父类默认使用双亲委派)
5、如果仍然没有加载成功,则抛出异常。
tomcat自定义类加载器:
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader都是Tomcat自己定义的类加载器。
WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器
commonLoader:Tomcat最基本的类加载器,加载的class可以被Tomcat容器本身以及各个Webapp(web应用)访问;
catalinaLoader:Tomcat容器私有的类加载器,加载的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载的class只对当前Webapp可见;
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但**各个
***WebAppClassLoader实例之间相互隔离**。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了实现JSP的HotSwap功能。
很显然,Tomcat为了实现隔离性,打破了双亲委派,每个webappClassLoader加载自己的目录下的class文件。
Tomcat如果使用默认的双亲委派类加载机制行不行?
不行。
1.如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,双亲委派机制是不管你是什么版本的,只在乎你的全限定类名,并且只加载一份。Tomcat是个web容器,一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离
2.部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
3.web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来,web容器也有自己依赖的类库使用默认的类加载机制,保证唯一性。
4.web容器要支持jsp的修改,jsp文件要编译成class文件才能在虚拟机中运行,每个jsp对应的servlet都有一个自己的类加载器。程序运行后再修改jsp,类加载器也还是取方法区中已经存在的,不会重新加载。
jsp的热加载:web容器检测到到jsp文件发生了修改–>将它的类加载器JasperLoader置空,重新引入新的类加载器重新加载。
模拟tomcat同一个web容器下有两个web应用程序,test和test1,加载不同版本的同一个类(全限定名都一样)。
将User1类编译后把class文件放test目录下,修改User1中say()方法代码,重新编译后再把class文件放test1下。
以此来模拟版本的不同。
(利用了前面的代码)

public static void main(String[] args) throws Exception{
        MyClassLoader1 myClassLoader = new MyClassLoader1("D:\\test");
        Class aClass = myClassLoader.loadClass("com.xwl.User1", false);
        Object obj = aClass.newInstance();
        Method method = aClass.getDeclaredMethod("say",null);
        method.invoke(obj,null);
        System.out.println("查看类加载器是谁:");
        System.out.println(aClass.getClassLoader());
    
        System.out.println("==========================================");
        MyClassLoader1 myClassLoader1 = new MyClassLoader1("D:\\test1");
        Class aClass1 = myClassLoader1.loadClass("com.xwl.User1", false);
        Object obj1 = aClass1.newInstance();
        Method method1 = aClass1.getDeclaredMethod("say",null);
        method1.invoke(obj1,null);
        System.out.println("查看类加载器是谁:");
        System.out.println(aClass1.getClassLoader());
    }

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值