jvm调优一:从源码级别了解jvm类加载机制

类加载运行全过程

在windows系统下,当我们要执行xxx.java的main方法时,其实就是要运行这个类的字节码文件。比如说在idea工具当中点击右键执行某个类的main方法时,整个运行过程是怎样的呢?大致过程如下:

  1. 由java.exe调用底层jvm.dll文件创建java虚拟机(C++实现)。在这个过程中创建引导类加载器实例(C++实现)。这一步就是由C++创建了一个jvm并实例了一个引导类加载器
  2. 由C++调用jvm的启动所需要的程序(java代码),比如sun.misc.Launcher。这个类由引导类加载器加载,通过这个类创建其他类加载器。这一步就是由c++调用java代码创建类加载器
  3. 通过类加载器加载具体的类。
  4. 当类加载到jvm当中后,由C++调用类的main方法
  5. main方法结束后jvm销毁

以上就是执行某个类的main方法的大致过程。其中一二四步骤涉及到C++的代码。这里暂时不做研究。本小结主要研究第三步,通过类加载器加载类的过程。

类加载器加载类的过程

在这里插入图片描述
类加载器加载类主要经过这几个步骤:加载、验证、准备、解析、初始化。

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的 main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口。下篇在研究jvm的各个内存区域。这一步主要把字节码文件加载到jvm中。加载呢不是立即就加载到jvm的内存中还要经过下面几个步骤。
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值。比如说 static int aa = 5; 在准备阶段就会给aa分配内存空间,并赋予int类型的默认值0,就是说这时候aa = 0
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下篇会讲到动态链接。
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

这里解释下什么是符号引用、直接引用、动态链接、静态链接。比如说代码里面的main方法、返回类型、修饰符、方法名等等都可以称之为符号引用。当我们把字节码加载到jvm当中这些符号引用都会有一个对应的内存地址,这个内存地址就是直接引用。在这个过程中,如果是在类加载期间完成的称之为静态链接,如果运行时完成称之为动态链接。

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

package common;
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;
	}
	static 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************");
		}
	}
}

运行结果:
在这里插入图片描述
根据上述的类加载流程,当运行main方法时就是,开始加载了TestDynamicLoad,加载的过程中初始化步骤会调用静态代码块。然后执行new A();开始加载A。之后只对B进行了定义,并没有真正是使用,所以是不会加载的。

类加载器和双亲委派机制

类加载器类型

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

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

用个示例演示下各个类加载器:

package common;

import sun.misc.Launcher;

import java.net.URL;

public class TestJDKClassLoader {
	public static void main(String[] args) {
		System.out.println(String.class.getClassLoader());
		System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
		System.out.println(TestJDKClassLoader.class.getClassLoader());
		System.out.println("---------------------------------------------------");
		ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
		ClassLoader extClassloader = appClassLoader.getParent();
		ClassLoader bootstrapLoader = extClassloader.getParent();
		System.out.println("the bootstrapLoader : " + bootstrapLoader);
		System.out.println("the extClassloader : " + extClassloader);
		System.out.println("the appClassLoader : " + 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"));
	}
}

其中com.sun.crypto.provider.DESKeyFactory 是属于扩展包下面的类,位于JRE的lib目录下的ext扩展目录中的JAR。第一个分隔线上面的三行代码就是打印各自的类加载器。看看运行结果
在这里插入图片描述
可以看到String类的类加载器是null。为什么是null ? 不应该是引导类加载器。对就是引导类加载器。开头就说了引导类加载器是由C++实现的所以在java环境中看不到。

DESKeyFactory位于ext扩展目下所以类加载器是ExtClassLoader
TestJDKClassLoader 是自己写的所以类加载器是AppClassLoader

为什么类加载器前面都有sun.misc.Launcher。开头说了ExtClassLoader和AppClassLoader是由sun.misc.Launcher这个类创建的。那么我们看看Launcher 是如何创建类加载器的。下面暂时Launcher的部分源码。
在这里插入图片描述
C++调用getLauncher()方法。这个方法直接返回launcher,这个launcher就是第一个红框里面的launcher。执行了new launcher(); 所以接着launcher 的构造方法。
launcher 的构造方法
在这里插入图片描述
看源码就要看重点,在来看看getExtClassLoader()这个方法。
getExtClassLoader():
在这里插入图片描述
ExtClassLoader是Launcher的静态内部类。继承了URLClassLoader。重点是红框部分,new Launcher.ExtClassLoader。所以ExtClassLoader就被new出来了。当然还有很多细节,具体细节可以去源码,就不深入解读。AppClassLoader同理。
URLClassLoader 就是根据传进来的路径去加载类。

双亲委派机制

下图就是双亲委派的流程
在这里插入图片描述
双亲委派机制:当需要加载类的时候,比如说需要加载A这个类,默认由应用程序类加载器(AppClassLoader)去加载,这时候AppClassLoader会去已经加载过的类里面去找有没有,有就返回,如果没有,委托父加载器,也就是扩展类加载器(ExtClassLoader),去ExtClassLoader加载过的类里面去找,如果还是没有继续委托引导类加载器(bootstrapLoader),去bootstrapLoader加载过的类里面去找,如果没有就由bootstrapLoader开始加载。由于A类是自己定义的类。bootstrapLoader加载失败,就由ExtClassLoader去加载,同样ExtClassLoader加载失败,就由AppClassLoader去加载。
整个过程就是双亲委派机制。
看到这里可能会有一些疑问?

  1. 为什么一开始就要通过AppClassLoader去找有没有加载过类,而不是直接从bootstrapLoader 去找呢。这样不就节省了从AppClassLoader到ExtClassLoader 再到bootstrapLoader 这个过程了吗。
  2. 为什么要设计双亲委派机制

下面会解答上述问题。

全盘负责委托机制

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类 所依赖及引用的其他类也由这个ClassLoder载入。

类加载器如何加载类

先回顾一下类加载的过程。Windows下C++通过jvm.dll创建jvm,同时实例化引导类加载器。引导类加载器调用Launcher 的getLauncher()方法得到Launcher 对象,通过Launcher 的构造方法创建出ExtClassLoaderAppClassLoader这两个类加载器。这些上面有讲到过。
当类加载器创建好后,需要加载类时,C++会调用Launcher 的getClassLoader()方法获取当前类的类加载器。在调用这个类加载器的loadClass() 方法去加载类。
来看看Launcher的另一部分源码
在这里插入图片描述
C++会调用Launcher 的getClassLoader()直接返回定义的private ClassLoader loader 。那么这个loader是在什么时候给他赋值了呢。在回过头来看看Launcher 的构造方法源码
在这里插入图片描述
也就是说在Launcher 的构造方法中给private ClassLoader loader 赋的值一开始是AppClassLoader。
这也就是为什么一开始是AppClassLoader先去找有没有加载过这个类。那为什么这么设计呢?
我们项目中95%的类都是自己定义的,都是AppClassLoader去加载。第一次去加载的时候或许比较麻烦,但是第二次用到的时候AppClassLoader就能会直接找到。如果从bootstrapLoader 开始去加载,每一次需要用到类的时候都会经过bootstrapLoader和ExtClassLoader 的,而这两个加载器是加载不到我们定义的类的。所以这样会更麻烦。 以上就回答了第一个疑问。
回到正题。刚才说了C++会调用Launcher 的getClassLoader()方法获取当前类的类加载器。在调用这个类加载器的loadClass() 方法去加载类。
来看看这部分源码,以AppClassLoader为样例
在这里插入图片描述
在这里插入图片描述
最终这个loadClass()方法会调用父类的loadClass()方法。来看看继承结构
在这里插入图片描述
所有的类加载器的父类是ClassLoader。ExtClassLoader 不是AppClassLoader的父类,而是父加载器,不要理解错了。所以最终会调到ClassLoader的loadClass()方法。双亲委派机制的逻辑就在这个方法里面

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //去找已经加载过的类里面有没有要加载的类,有就返回
            // findLoadedClass 这个方法最终调用的是C++的本地方法
            Class<?> c = findLoadedClass(name);
            //c != null 说明这个类已经加载过了  会直接返回
            // c== null  执行下面的逻辑
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //判断父加载器是不是为空,如果不为空调用父加载器的loadClass方法。
                        //父加载器的loadClass方法最终还是会调到ClassLoader的loadClass这个方法
                        c = parent.loadClass(name, false);
                    } else {
                       //如果父加载器为空,调用引导类加载器去找有没有加载过,没有就会去加载。
                       //findBootstrapClassOrNull 调用的也是C++的方法
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				// 上面的if方法就是从AppClassLoader开始委托到引导类加载器。并通过引导类加载器去加载
				//下面的if 就是从ExtClassLoader 开始加载类
                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;
        }
    }

最终加载类的是通过 c = findClass(name);这行代码去加载。ExtClassLoader和AppClassLoader 都没有这个方法,实际上调用的是他们的父类URLClassLoader的findClass方法 。所以来看看URLClassLoader.findClass 源码

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        //核心部分
                        //根据当前类加载器的加载范围 判断有没有需要加载的类
                        //  res == null 说明没有  就执行 else 方法
                        // res != null  执行defineClass 方法
                        if (res != null) {
                            try {
                            	//这个方法是把类加载到jvm当中 加载过程就是验证、准备、解析、初始化
                            	// defineClass调用是C++ 
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

以上源码 重要部分都通过注释方式说明了代码的含义,而且大部分代码都是调用C++的代码。下面通过一个例子说下整个过程。
假设需要加载一个Order类。Order 是web应用的一个类。加载大致过程:

  1. C++调用getLauncher()方法得到一个单例的Launcher对象。在Launcher的构造方法 会先创建AppClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
  2. C++调用Launcher的 getClassLoader()方法 获取类加载器。此时获取到的就是AppClassLoader
  3. C++调用AppClassLoader 的loadClass()方法。AppClassLoader 的loadClass()方法 最终会调到ClassLoader的loadClass()方法。
  4. 在ClassLoader的loadClass()方法中 先执行Class<?> c = findLoadedClass(name); 去找AppClassLoader加载过的类里面有没有Order这个类。因为是第一次加载 所以返回null。
  5. 判断 AppClassLoader 的父加载器是不是等于null 。在这里插入图片描述
    AppClassLoader 的父加载器是 ExtClassLoader。所以调用ExtClassLoader的loadClass()。 最终还是会调用到ClassLoader的loadClass()方法中。重复第四步。这时找的是ExtClassLoader加载过的类有没有Order 类。也没有。重复第五步。判断ExtClassLoader父加载器是不是空。此时 为空 执行 了else 的方法 c = findBootstrapClassOrNull(name); 注意:此时AppClassLoader的加载过程没有跑完。在AppClassLoader加载过程中调用了ExtClassLoader的加载过程。等ExtClassLoader加载完成后,还会回调AppClassLoader 的加载过程。
  6. c = findBootstrapClassOrNull(name); 这一步就是通过引导类加载器去找有没有加载过Order 这个类。如果没有就通过引导类加载器去加载。引导类加载器加载不到这个类所以返回null
  7. 接着执行下面这些代码
    在这里插入图片描述
    如果返回null 执行 findClass(name); 此时的加载器是ExtClassLoader,ExtClassLoader的流程还没跑完。所以调用到ExtClassLoader的 findClass(name); 方法。 由于ExtClassLoader没有这个方法,实际上调用的是其父类URLClassLoader的findClass方法。
  8. 在URLClassLoader的findClass方法中执行下面的代码。此时还在ExtClassLoader的执行流程内
    在这里插入图片描述
    ExtClassLoader自然加载不到Order类。 所以res == null 执行else 方法 返回了null 到此 ExtClassLoader的执行流程结束。
  9. ExtClassLoader的执行流程结束 返回到了AppClassLoader 的执行流程 同样执行下面的代码
    在这里插入图片描述
    此时调用的是AppClassLoader的findClass方法。最终还是调用到URLClassLoader的findClass方法。重复第八步。
  10. 通过AppClassLoader 可以加载到Order这个类。所以res != null 执行return defineClass(name, res);
    在这里插入图片描述
  11. return defineClass(name, res);这一步中去加载Order 这个类。defineClass 调用的是C++的本地方法。加载过程就是 验证、准备、解析、初始化这几个步骤。最终把Order类加载到了jvm当中。

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

比如说 自定义一个 java.lang.String.class这个类。这个类里面有一些僵尸程序或者病毒什么的。jvm启动的时候引导类加载器会加载jvm的核心代码库,就是jre下的lib目录下的核心类库,比如 rt.jar、charsets.jar等 。 java.lang.String.class就包括在其中。那么jvm还会不会加载我们自己定义的java.lang.String.class?如果加载到了自定义的 java.lang.String.class,这个类有病毒僵尸程序什么的很不安全。
实际上呢根据双亲委派机制,引导类加载器加载到了jre下的String这个类。当需要加载自定义的String类时,根据双亲委派委托到了引导类加载器。而自定义的String类的全限定名和jre下的String的全限定名一样,此时引导类加载器发现 我在jre下面已经加载过这个String类了,就不需要加载了。所以jvm内存当中只有一个String类就是 jre下的String类。
这样做有什么好处呢。

  1. 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
  2. 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

补充

这一部分还有自定义类加载器、如何打破双亲委派机制和tomcat 如何打破双亲委派机制 还没写。后续当做调优一的番外来写吧,不然篇幅太长。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值