jvm之类加载和双亲委派机制

本文详细介绍了Java程序的运行过程,包括类加载的懒加载特性,加载过程的六个步骤(加载、验证、准备、解析、初始化、使用、卸载),以及类加载器的工作原理。重点讨论了双亲委派机制,如何防止核心API被篡改,确保类的唯一性。同时,文章提供了一个自定义类加载器的例子,展示了如何打破双亲委派机制,并分析了Tomcat如何在类加载上实现应用隔离和热加载功能。
摘要由CSDN通过智能技术生成

从类加载到双亲委派机制

java程序运行过程

public class TestDynamincLoaded {

  static {
    System.out.println("***** load test dynamic ");
  }

  public static void main(String[] args) {
    new A();
    System.out.println("***** load test *****");
    B b = null;
  }

}

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

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

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

说明了JVM的类加载是懒加载,也就是用到了某个类时才会去加载这个类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
此程序运行过程分为如下几个过程:

  1. 调用dll创建java虚拟机;
  2. 创建bootstrap class loader,由于是c++创建的,器parent属性为null;
  3. 创建JVM启动Launcher,Launcher中会创建AppClassLoader和ExtClassLoader;
  4. 获取要运行的类的实例 加载 要运行的类;通过调用loadClass方法加载类;
  5. 加载完成后会执行main方法;
  6. 查询运行完后会销毁JVN;
    到此,这个程序就运行结束了。
    在这里插入图片描述
    上图中的类加载过程分为如下几个步骤:
    加载—>验证—>准备—>解析—>初始化—>使用–>卸载
  • 加载:通过IO流加载字节码文件;在加载阶段会在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区的这个类的各种数据的访问入口;
  • 验证:校验字节码文件的正确性,比如字节码文件的格式;
  • 准备:给类的静态变量分配内存并赋予默认值;
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用;
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块;

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

类加载器和双亲委派机制

类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器:

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等;
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包;
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那 些类;
  • 自定义加载器:负责加载用户自定义路径下的类包;

示例:

public class TestJDKClassLoader {

  public static void main(String[] args) {
    //java的核心类库是由BootStrapClassLoader加载的, BootStrapClassLoader是由c++创建的,所以返回null
    System.out.println(String.class.getClassLoader());
    //ExtClassLoader加载的
    System.out.println(DESKeyFactory.class.getClassLoader());
    //AppClassLoader
    System.out.println(TestJDKClassLoader.class.getClassLoader());

    System.out.println("=================");

    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    ClassLoader extClassLoader = appClassLoader.getParent();
    ClassLoader bootstrapClassLoader = extClassLoader.getParent();

    System.out.println("the bootstrapClassLoader " + bootstrapClassLoader);
    System.out.println("the extClassLoader " + extClassLoader);
    System.out.println("the appClassLoader " + appClassLoader);

    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("========使用extClassLoader加载以下文件=========");
    System.out.println(System.getProperty("java.ext.dirs"));

    System.out.println("=========appClassLoader加载文件===============");
    System.out.println(System.getProperty("java.class.path"));
  }
}

JVM类加载器的层级结构如下:
在这里插入图片描述
这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再
委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的
类加载路径中查找并载入目标类。比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。

双亲委派机制存在的意义:

  • 沙箱安全机制,自己写的Java.lang.String.class类不会被加载,这样便可以防止核心API库被修改;
  • 避免类的重复加载:如果父类加载器已经加载了某个类,子类加载器就没有必要再去加载一遍;

自定义类加载器示例

/**
 * 自定义类加载器,重写findClass方法
 */
public class MyClassLoader extends ClassLoader{

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0 , data.length);
        } catch (Exception e) {
            throw new ClassNotFoundException(e.getMessage() + e.getCause());
        }
    }
    
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.", "/");
        FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
        int len = inputStream.available();
        byte[] data = new byte[len];
        inputStream.read(data);
        inputStream.close();
        return data;
    }
}
public class User {
    public void sout() {
        System.out.println("==============自定义类加载器============");
    }
}
public class MyClassLoaderTest {

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("D:/test");
        //需要在指定目录下放置User类编译后的class文件
        Class clazz = myClassLoader.loadClass("User");
        Object o = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(o, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

这里需要注意的是:如果程序编译后的User class文件没有删除,那么最后的输出结果会是AppClassLoader,这是因为AppClassLoader已经加载了程序生成的User class文件。
在这里插入图片描述
删除程序生成的User类的class文件,只保留test目录下的class文件,运行后的结果如下:
在这里插入图片描述
虽然我们自定义的类加载器继承的是ClassLoader,但是它的父类是AppClassLoader。

打破双亲委派机制

/**
 * 自定义类加载器
 */
public class MyClassLoader extends ClassLoader {

    private String classPath;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            throw new ClassNotFoundException(e.getMessage() + e.getCause());
        }
    }

    /**
     * 重写类加载方法,实现自己的加载逻辑,不委派给父类加载
     *
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> clazz = findLoadedClass(name);

            if (clazz == null) {
                long t1 = System.nanoTime();

                if (!name.startsWith("mytest")) {
                    //委派给父类加载
                    clazz = this.getParent().loadClass(name);
                } else {
                    //以mytest开头的类使用自己的类加载器加载
                    clazz = findClass(name);
                }

                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClassTime().increment();
            }

            if (resolve) {
                resolveClass(clazz);
            }
            return clazz;
        }
    }
    
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.", "/");
        FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
        int len = inputStream.available();
        byte[] data = new byte[len];
        inputStream.read(data);
        inputStream.close();
        return data;
    }
}

测试

public class MyClassLoaderTest {

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("D:/");
        Class clazz = myClassLoader.loadClass("mytest.User1");
        Object o = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(o, null);

        System.out.println(clazz.getClassLoader());

        System.out.println();

        MyClassLoader myClassLoader1 = new MyClassLoader("D:/");
        Class clazz1 = myClassLoader1.loadClass("mytest.User1");
        Object o1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("sout", null);
        method1.invoke(o1, null);

        System.out.println(clazz1.getClassLoader());
    }
}

结果如下:可以看到使用了我们自定义的加载器加载的User1的class文件,而原本应该是使用AppClassLoader加载的。
在这里插入图片描述

同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。

Tomcat打破双亲委派机制

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的
    不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是
    独立的,保证相互隔离;
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程
    序,那么要有10份相同的类库加载进虚拟机;
  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的
    类库和程序的类库隔离开来;
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中
    运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启;

在这里插入图片描述
tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;

从图中的委派关系中可以看出:

  • CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,
    从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则
    与对方相互隔离;
  • WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader
    实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值