JVM笔记【一】java和Tomcat类加载机制

JVM笔记一java和Tomcat类加载机制

java和Tomcat类加载机制
  • Java类加载

复制代码
  * loadClass加载步骤
  • 类加载机制

  • 类加载器初始化过程

  • 双亲委派机制

  • 全盘负责委托机制

  • 类关系图

  • 自定义类加载器

  • 打破双亲委派机制

  • Tomcat类加载器

复制代码
  * 为了解决以上问题,tomcat是如何实现类加载机制的?

Java类加载

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到jvm中。类似流程图如下。

loadClass加载步骤

类全生命周期:加载 >>验证>>准备>>解析>>初始化 >>使用>>卸载。

加载:在硬盘上查找并通过IO兑入字节码文件,使用到类时才会加载,例如调用类的main方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象(推荐 ,放在堆中,作为访问这个类在方法区中类元数据的各个数据的接口。

验证:校验字节码文件的正确性。如:文件格式的验证,元数据的验证,字节码的验证,符号引用的验证。

准备:给类的静态变量分配内存,并赋予默认值,此处给默认值,不一定是我们程序中赋予的值。如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步

解析:将符号引用 替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或者句柄等(直接引用),这是所谓的静态链接 过程(类加载期间完成),动态链接 是在程序运行期间完成的,将符号引用替换为直接引用(// TODO 待补充),解析后的信息存储在ConstantPoolCache类实例中

初始化:对类的静态变量初始化为指定的值,执行静态代码块,比如:initData 的666值是此时才赋予的。构造放在在静态代码块之后。

复制代码
public static final int initData=666;

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

注意:主类在运行过程中如果使用到其他类,会逐步加载这些列。jar包或者war包里的类不是一次性全部加载的,是使用到时才加载。代码示例如下:

复制代码
public class TestDynamicLoad {
    static {
        System.out.println("*************load TestDynamicLoad************");
    }

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

}

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

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

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

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

结果:

复制代码
*************load TestDynamicLoad************
*************load A************
*************initial A************
*************load test************

类加载机制

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

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

类加载器初始化过程

由上面的加载过程图可知,JVM启动,实例sun.misc.Launcher类,而sun.misc.Launcher构造方法内部,创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

JVM会默认调用Launcher中的getClassLoader()方法,返回的加载器会是AppClassLoader来加载我们的应用程序。代码截取

复制代码
//Launcher的构造方法
public Launcher() {
 Launcher.ExtClassLoader var1;
 try {
 //构造扩展类加载器,在构造的过程中将其父加载器设置为null
 var1 = Launcher.ExtClassLoader.getExtClassLoader();
 } catch (IOException var10) {
 throw new InternalError("Could not create extension class loader", var10);
 }

 try {
 //构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
 //Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自
己写的应用程序
 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
 } catch (IOException var9) {
 throw new InternalError("Could not create application class loader", var9);
 }

 Thread.currentThread().setContextClassLoader(this.loader);
 String var2 = System.getProperty("java.security.manager");
 。。。 。。。 //省略一些不需关注代码

}


 public ExtClassLoader(File[] var1) throws IOException {
  // Launcher.ExtClassLoader.getExtClassLoader(); 实例化会走到地方,他父加载器传的是null
  super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
  SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

双亲委派机制

JVM类加载器是有亲子层级结构的,如下图:

这里类加载其实就是一个双亲委派机制,加载某个类时,launcher的getClassLoader会给出appClassLoader这个加载器,调用其最上层抽象ClassLoader的loadClass方法,其流程是会先找自己有没**有加载过(并不是加载)**这个类,如果有直接返回,如果没有,则委托父加载器寻找目标类,而此时父加载器(扩展类加载器)同样是实现ClassLoader的类,同样走loadClass方法,逻辑也是找自身是否加载过此类,如果没有,则继续委托其父加载器;如果依然找不到目标类,则在自己的类加载路径中查找并载入目标类。

比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载 器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。

复制代码
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //  // 检查当前类加载器是否已经加载了该类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) { //如果当前加载器父加载器不为空则委托父加载器加载该类
                        c = parent.loadClass(name, false);
                    } else { //如果当前加载器父加载器为空则委托引导类加载器加载该类
                        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();
                    //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                    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;
        }

双亲委派说简单的,就是先找父亲加载,不行再有儿子自己加载,此处注意:父类加载器和父类不是一个概念。

全盘负责委托机制

"全盘负责"是指当一个ClassLoader装载一个类时,除非显示的使用另一个ClassLoader,该类所有依赖及引用的类也有这个ClassLoader一次载入完毕。(因为正常情况下,依赖和引用类是只有在使用的时候才加载的。)

类关系图

自定义类加载器

自定义类加载器只需要继承java.lang.ClassLoader类,该类由两个核心方法,一个是loadClass(String,boolean),实现了双亲委派机制,还有一个是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法(app和ext因为都是继承URLClassLoader,此方法在URLClassLoader中实现了)。

复制代码
package com.tuling.jvm;

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

/**
 * @Description: 自定义类加载器
 * @ClassName: MyClassLoaderTest
 * @Author: 
 * @Date: 2021/8/11 0:42
 **/
public class MyClassLoaderTest {
    static 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 {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:"+ File.separator+"program_test");
        //D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

打破双亲委派机制

复制代码
package com.tuling.jvm;

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

/**
 * @Description: 双亲委派和反
 * @ClassName: MyClassLoaderTest
 * @Author: 
 * @Date: 2021/8/11 0:42
 **/
public class MyClassLoaderTest {
    static 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();
            }
        }

        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         *
         * @param name
         * @param resolve
         * @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 (name.startsWith("com.tuling.jvm")) {
                        c = findClass(name);
                    } else {
                        c = super.loadClass(name, false);
                    }
                }

                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }


    public static void main(String args[]) throws Exception {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:" + File.separator + "program_test");
        //D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

Tomcat类加载器

思考:tomcat是web容器,他需要解决什么问题?

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

为了解决以上问题,tomcat是如何实现类加载机制的?

  • commonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个WebApp访问;
  • catalinaClassLoader:tomcat容器私有的类加载器,加载路径中的class对于WebApp不可见。
  • sharedClassLoader:各个WebApp共享的类加载器,加载路径中的class对于所有WebApp可见,但是对于Tomcat容器不可见
  • WebAppClassLoader:各个WebApp私有的类加载器,加载路径中的class只对于当前WebApp可见,比如加载war包里的相关类,每个war包应用都有自己的WebAppClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载给咱的spring版本。webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制
    委派关系:
  1. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
  2. WebAppClassLoader可以使用SharedClassLoader加载到的类,但WebAppClassLoader实例之间相互隔离。
  3. JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

加载类关系

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

课后小问题

1.为什么先执行静态代码块,后执行构造方法?

答:因为在初始化的时候,会执行静态代码块的内容,而构造方法是在new对象的时候调用的。这也正证实加载并不会new一个完整对象出来。而仅仅只会有一个class对象到堆中。

2.为什么要设计双亲委派机制?

答:(1)沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。

(2)避免类的重复加载:当父亲已经加载了该类时,就没有必要子Classloader在加载一次,保证被加载的类的唯一性。

3.Tomcat如果使用默认的双亲委派类加载机制行不行? 为什么?

答:不行。这就涉及到Tomcat要解决的问题。

  • 如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加载器是不管类的版本,只根据全名查找,且只有一份。
  • 如何实现jsp的热加载问题,因为jsp会编译成calss文件,如果修改了jsp,但是类名并没有变,类加载器会直接取方法区中的已存在的信息,修改的并不会被加载到。所以,Tomcat的处理办法是,当你修改jsp,他会卸载掉这个jsp的类加载器,重新创建新的类加载器,加载jsp文件。

4.Tomcat打破了类加载机制,是否可以恶意定义HashMap?是否安全

答:不可以,因为上层类加载器依然没有变,根据列关系图,都会走到secureClassLoader,做安全校验。

相关推荐
学到头秃的suhian4 小时前
JVM-类加载机制
java·jvm
NEFU AB-IN10 小时前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海16 小时前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗17 小时前
JVM整理
jvm
echoyu.17 小时前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考1 天前
JVM中内存管理的策略
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z1 天前
【JVM】详解 线程与协程
java·jvm
thginWalker2 天前
深入浅出 Java 虚拟机之实战部分
jvm
程序员卷卷狗3 天前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm