Java类加载机制深度刨析

1、Java类从加载到运行到结束的大致过程

1、如何运行Java文件

java文件运行时需要两个命令javac和java

//javac编译生成一个.class文件
javac HelloWorld.java
//java命令运行HelloWorld文件
java HelloWorld

2、一个Java类从加载到运行的过程

特别提示:类似于java.exejavac.exejvm以及引导类加载器实例都是由C或C++所编写的,与计算机底层进行交互。
一个Java类从加载到运行直致JVM销毁的过程如下图所示:
在这里插入图片描述
上图分析为语言性描述大概步骤如下:

1、使用javac调用javac.exe程序编译生成一个.class文件,使Java底层系统可以识别。
2、使用java调用java.exe程序去执行以下操作
      2.1、调用底层的库函数jvm.dll创建一个java虚拟机。
      2.2、在创建虚拟机的同时,会创建一个引导类加载器实例。
3、调用java虚拟机中的启动实例sun.msic.Launcher调用对应的java代码去创建和启动其他的类加载器。
4、通过sun.msic.Launcher.getClassLoader()方法调用例如AppClassLoader的实例去加载我们本地的实现类,classLoader.loadClass(“本地的文件路径”)。
5、加载到之后,由C或C++发起的调用类中的main()方法开始执行程序。
6、程序执行完成之后,JVM销毁。

3、Java类加载(LoadClass)的过程

1、加载:将编译好的class文件丢到JVM虚拟机内存中。
2、验证:验证.class文件是否符合格式,例如用Sublime打开的.class文件格式如下,cafe babe是.class文件的开头格式,后面是JVM独有的字节码文件。

在这里插入图片描述

3、准备:给类的所有静态变量分配内存,并赋予默认值
4、解析:将符号引用替换为直接引用,相当于一个静态链接的操作。
符号:如下图所示,红框中的变量名,方法名,甚至于括号等等,都是符号。
符号引用:现在只能引用到这些符号,称之为符号引用。
直接引用:将直接指向他们具体的实现,因为这些符号下面有一个地址,指向的是JVM内存中存放的编译后的文件,底层系统可以直接操作这些文件。
静态链接:类加载时候进行的符号引用替换为直接引用,只是一个简单的地址的转换。
动态链接:在程序运行的过程中符号引用替换为直接引用,比如说下图中main方法中调用的compile方法,他在类加载时并不会去关心方法里面的东西,只有在使用的时候才会去动态加载方法中的具体操作。

在这里插入图片描述

5、初始化:对静态变量初始化值,并执行静态代码块。
注:这里需要提到一个小知识点,类的构造方法先执行还是静态代码块先执行。

1、第一种情况:
在这里插入图片描述

我们初始化了一个实例A,却没有初始化实例B,由于是懒加载,所以B的构造方法和静态方法不执行,执行结果如下图所示,而且我们还发现,静态方法是在构造方法之前执行的,这是因为==静态方法是在类加载时就执行,而构造方法是类加载完成之后为了实例化而执行的。==执行结果如下:

在这里插入图片描述
2、第二种情况:

在这里插入图片描述
当我们将B b = null;改为B b = new B();,其他代码不变,然后运行会发现B会被加载,所以证明了前面所说的懒加载的东西,运行截图如下:
在这里插入图片描述

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

1、类加载器分类

引导类加载器:负责加载JVM运行的位于jre/lib目录下的核心类库,比如rt.jar、charsets.jar等库函数
扩展类加载器:负责加载JVM运行时位于jre/lib/ext目录下的核心jar包。
应用程序类加载器:主要加载ClassPath路径下的类包,主要就是我们自己写的类。 自定义类加载器:负责加载自己定义路径下的类包。

2、双亲委派机制的形成

在这里插入图片描述

3、双亲委派机制加载类的过程

双亲委派机制的代码实现在java/lang/ClassLoader.java文件中的loadClass方法,会执行findClass("")方法去执行真正的加载类的过程。

函数名称作用
loadClass向上委派和调用findClass(可以打破双亲委派 )
findClass具体去执行路径下的类加载 (可以自定义类加载器 )

在这里插入图片描述

当加载一个类的时候,首先系统会调用AppClassLoader,然后会做如下操作:
1、AppClassLoader会检查自己有没有加载过这个类,如果没有,则向上委托给ExtClassLoader。
2、ExtClassLoader会检查自己有没有加载过这个类,如果没有,则向上委托给BootStrapClassLoader。
3、BootStrapClassLoader会尝试去加载这个类,如果加载不到,则会向下说明让ExtClassLoader去加载。
4、当ExtClassLoader加载不到时,则会向下说明让AppClassLoader去加载,若加载不到将会说明让自定义加载器去加载。
5、自定义类加载器开始加载,若还加载不到,则报错,在这几个步骤中,无论哪个类加载器已经加载了这个类或者加载成功了这个类,都将结束操作,返回给系统信息。

有的人会有这样的问题:

问题:为什么非要从AppClassLoader开始加载,而不直接从扩展类或者引导类直接加载,那样的话不是更快?不需要走一个来回。
参考答案:我是这样想的,在一个Web系统中,可能有90%以上的类都是由AppClassLoader加载的,当第一次加载的时候是需要向上委托再回来加载,感觉好像绕了路,可是当第二次第三次加载的时候,就可以直接判断已经加载过了这个类,就无需在向上委托了,效率就提高了很多。真正的答案估计只有开发双亲委派机制的人才知道,我这里只是一个猜测,引导了一下大家的思路,参考即可。

4、为什么要设计双亲委派机制

沙箱安全机制:不能重复去写原生库中的类,保证核心API不被篡改,安全性的保障。
避免类的重复加载:父加载器已经加载过的,子类加载器不用再去加载,保证了类被加载的唯一性。

举例:比如,java原生库中有一个java.lang.String类,如果,有一个黑客也写一个一模一样的类(类中有病毒)放在网上的某一个jdk中,我们在使用这个jdk时,如果用了String这个类,将会触发到这个病毒,但是有了双亲委派机制的话,这个类就不会被加载到,从而就提高了安全性。

5、全盘负责委托机制

当一个ClassLoader去加载一个类的时候,该类引用的类也会由这个CLassLoader去加载,除非已经明显指定了需要另外一个类去加载这个类。

6、实现自定义类加载器

相当于重写了findClass方法

package JVM;

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

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;
        }

        @Override
        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:/test");
        //D盘创建一个目录,将Math类丢进去
        Class clazz = classLoader.loadClass("JVM.MyLoadClassTestClass");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("print",null);
        method.invoke(obj,null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

7、打破双亲委派机制

相当于重写了loadClass方法

package JVM;

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

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;
        }

        @Override
        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();
            }
        }

        @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 (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        // 判断是否是"JVM"开头的类路径
                        if(!name.startsWith("JVM")){
                            c = this.getParent().loadClass(name);
                        }
                        else{
                            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;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //D盘创建一个目录,将Math类丢进去
        Class clazz = classLoader.loadClass("JVM.MyLoadClassTestClass");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("print",null);
        method.invoke(obj,null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

8、模拟Tomcat打破双亲委派机制

如果把一个Webapp应用程序看成一个war包的话,tomcat在每生成一个war包的时候都会给这个war包生成一个相应的类加载器,这个类加载器打破了双亲委派机制,自己可以加载自己独特的东西,各个war包之间相互不影响。

在这里插入图片描述

9、为什么改变Jsp文件不需要重启系统

修改Jsp文件不需要重启系统的原因是,每一个Jsp都对应了一个独有的Jsp类加载器,这个类加载器里面有一个定时任务,会时刻检查Jsp文件是否有被修改,如果被修改了,则将重新加载Jsp文件,所有Jsp文件在修改完之后不需要重启系统,从而形成了热加载。Jsp独有的类加载器如上图所示,我这里只是提供了一种Jsp的热加载方式,可能有很多。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值