jvm调优之类加载器(一)
前言
JVM一直是Java程序员面试的重灾区,也是成为一名上流Java程序员必须要攻克的一座堡垒。作为开篇之作,我选择采用专题的形式给大家逐渐剖析JVM底层原理,并且结合实际工作中的一些常见讲解JVM的具体优化方案。
Java文件从编译到执行的整个过程
一个Java文件被编写出来之后,会经过Java编译器javac.exe将其编译成字节码文件(.class文件);然后再有JVM加载字节码文件,进而使用Java解释器将字节码文件解释成一行行的机器指令,然后由执行引擎调用计算机硬件进行执行。在这整个过程中JVM无疑起到了至关重要的作用,也是我们这个专题后续重点研究的方向

备注:
JVM中不仅仅有字节码解释器将字节码文件一行行地解释成机器指令,还有JIT编译期(Just In-TimeCompiler)可以将一些热点代码(执行频率特别高的代码)编译成native(本地)代码进行执行,这样可以提升代码的执行速度。
JVM的概述
JVM是运行java代码的虚拟机,包括一套字节指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方法域,JVM的作用是加载和运行字节码文件,它是运行在操作系统之上的,它与硬件没有直接的交互。sun公司定义了一套JVM规范,很多大型公司都有对这套规范进行实现,打造自己的JVM。
常见的JVM
- HotSpot VM: Oracle官方的JVM,也是我们进行Java开发通常所使用的JVM
- J9: IBM公司的JVM
- Microsoft VM: 微软公司的JVM
- Azul VM:土豪专用虚拟机,经HotSport改进得来,运行在Azul专有硬件中
- Liquid VM:直接操作硬件的JVM,不需要操作系统的支持
- TaobaoVM: 阿里巴巴对Hotspot进行的深度定制版
JVM的基本结构

JVM的体系总体上可以分成四大块而,我们这个专题也会围绕这四部分,以及相关的一些内容给大家进行展开讲解
- 类加载器加载类的机制
- JVM内存结构
- GC算法和垃圾回收
- GC分析和命令调优
字节码文件到内存中对象的步骤
一个Java文件经过编译之后,生成字节码文件,而由字节码文件到最终创建出JVM内存中的对象,则需要经过三个步骤:
- 加载(Loading)
- 链接(Linking)
- 初始化(Initialization)
我们在第一章里面,主要搞懂第一步,也就是字节码文件加载的整个流程。

类加载机制
类加载器的概述
类加载器是java.lang.ClassLoader类的子类对象或者C++代码编写的Bootstrap ClassLoader,它们的作用是加载字节码到JVM内存,得到Class类的对象。类加载器根据其加载类的范围的不同,可以分为四种类加载器:
- 启动类加载器(BootStrap ClassLoader): 这个类加载器并不是继承自java.lang.ClassLoader类,而是使用C++编写的,所以其无法直接被我们的应用程序所使用,它主要是用于加载JAVA_HOME/jre/lib目录下的类库。
- 扩展类加载器(ExtClassLoader): 这个类加载器的super class是java.lang.ClassLoader,主要加载JAVA_HOME/jre/lib/ext目录中的类库 。
- 应用类加载器(AppClassLoader): 这个类加载器的super class是java.lang.ClassLoader,主要用于加载classPath下的类,也就是加载程序开发者自己编写的Java类
- 自定义类加载器: 程序开发者可以自己继承ClassLoader类,编写自己的类加载器,从而进行自定义的类加载规则
拓展类加载器和应用类加载器的类图如下:

从源码的角度分析AppClassLoader和ExtClassLoader加载的类的范围
由于AppClassLoader和ExtClassLoader都是sun.misc.Launcher类的内部类,所以我们可以通过分析sun.misc.Launcher类的源码来获取AppClassLoader和ExtClassLoader所加载的类的范围
AppClassLoader类的源码
public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException{ //获取"java.class.path"对应的属性值,该属性值就是AppClassLoader加载的类的路径 final String s = System.getProperty("java.class.path"); final File[] path = (s == null) ? new File[0] : getClassPath(s); // Note: on bugid 4256530 // Prior implementations of this doPrivileged() block supplied // a rather restrictive ACC via a call to the private method // AppClassLoader.getContext(). This proved overly restrictive // when loading classes. Specifically it prevent // accessClassInPackage.sun.* grants from being honored. // return AccessController.doPrivileged( new PrivilegedAction<AppClassLoader>() { public AppClassLoader run() { URL[] urls = (s == null) ? new URL[0] : pathToURLs(path); return new AppClassLoader(urls, extcl); } });
ExtClassLoader类的源码
// 该方法是获取ExtClassLoader要加载的类所在的路径 private static File[] getExtDirs() { // 根据"java.ext.dirs"获取值,该值就是ExtClassLoader要加载的类的路径 String s = System.getProperty("java.ext.dirs"); File[] dirs; if (s != null) { StringTokenizer st = new StringTokenizer(s, File.pathSeparator); int count = st.countTokens(); dirs = new File[count]; for (int i = 0; i < count; i++) { dirs[i] = new File(st.nextToken()); } } else { dirs = new File[0]; } return dirs; }
而通过sun.misc.Launcher类的源码,我们发现了Bootstrap ClassLoader加载的类的路径
private static String bootClassPath = System.getProperty("sun.boot.class.path");
所以我们通过自己编写代码,来查看一下这三种类加载器加载的类的路径:
public class TestClassLoaderScope { public static void main(String[] args) { String bootPath = System.getProperty("sun.boot.class.path"); System.out.println("BootStrap ClassLoader加载的类的路径:------------------"); // 使用换行符替换路径中的分号,便于更美观地打印 System.out.println(bootPath.replaceAll(";",System.lineSeparator())); System.out.println("ExtClassLoader加载的类的路径:------------------"); String extPath = System.getProperty("java.ext.dirs"); System.out.println(extPath.replaceAll(";",System.lineSeparator())); System.out.println("AppClassLoader加载的类的路径:------------------"); String appPath = System.getProperty("java.class.path"); System.out.println(appPath.replaceAll(";",System.lineSeparator())); } }
// 测试结果
BootStrap ClassLoader加载的类的路径:------------------ C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\resources.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\rt.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\sunrsasign.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\jsse.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\jce.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\charsets.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\jfr.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\classes ExtClassLoader加载的类的路径:------------------ C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext C:\WINDOWS\Sun\Java\lib\ext AppClassLoader加载的类的路径:------------------ C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\charsets.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\deploy.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\access-bridge-32.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\cldrdata.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\dnsns.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\jaccess.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\jfxrt.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\localedata.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\nashorn.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\sunec.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\sunjce_provider.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\sunmscapi.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\sunpkcs11.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\ext\zipfs.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\javaws.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\jce.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\jfr.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\jfxswt.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\jsse.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\management-agent.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\plugin.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\resources.jar C:\Program Files (x86)\Java\jdk1.8.0_171\jre\lib\rt.jar C:\JavaEE_Relation\JavaEE101\itheima101_maven\day37_province_demo\target\test- classes 这里是我项目的类路径 C:\JavaEE_Relation\JavaEE101\itheima101_maven\day37_province_demo\target\classes C:\maven\apache-maven-3.5.2\repository\javax\servlet\javax.servlet- api\3.1.0\javax.servlet-api-3.1.0.jar C:\maven\apache-maven-3.5.2\repository\javax\servlet\jsp\jsp-api\2.0\jsp-api- 2.0.jar C:\maven\apache-maven-3.5.2\repository\mysql\mysql-connector-java\5.1.37\mysql- connector-java-5.1.37.jar C:\maven\apache-maven-3.5.2\repository\com\alibaba\druid\1.1.6\druid-1.1.6.jar C:\maven\apache-maven-3.5.2\repository\commons-dbutils\commons- dbutils\1.7\commons-dbutils-1.7.jar C:\maven\apache-maven-3.5.2\repository\com\alibaba\fastjson\1.2.71\fastjson- 1.2.71.jar C:\maven\apache-maven-3.5.2\repository\redis\clients\jedis\2.9.1\jedis-2.9.1.jar C:\maven\apache-maven-3.5.2\repository\org\slf4j\slf4j-api\1.7.22\slf4j-api- 1.7.22.jar C:\maven\apache-maven-3.5.2\repository\org\apache\commons\commons- pool2\2.6.2\commons-pool2-2.6.2.jar C:\maven\apache-maven-3.5.2\repository\org\projectlombok\lombok\1.18.12\lombok- 1.18.12.jar C:\software\IntelliJ IDEA 2019.3.2\lib\idea_rt.jar
经过上述代码及测试结果,充分说明了:
- BootStrap ClassLoader是加载jre/lib路径下的jar包,以及jre/classes目录(如果有的话)中的jar包或者字节码文件
- ExtClassLoader是加载jre/lib/ext路径下的所有的jar包
- AppClassLoader是加载类路径下的所有字节码文件
双亲委派机制
JVM是采用双亲委派机制来进行类的加载的,其具体流程如下图所示:

过程一: 自下而上检查该类是否已经被加载
我们要加载一个类,先由定义类加载器(如果有的话)检查该类是否已经被定义类加载器所加载,如果已加载则不需要再次加载,直接返回Class对象;如果没有被定义类加载器加载,则向上找应用类加载器,同样执行检查,如果已加载则直接返回Class对象,未加载则继续向上找扩展类加载器执行同样的检查,如果已加载则直接返回Class对象,未加载则继续向上找启动类加载器执行同样的检查,如果已加载则直接返回Class对象,如果未加载则执行过程二
过程二:自上而下进行类加载操作
如果过程一由启动类加载器检查了,该类依旧是未被加载的话,则由启动类加载器尝试加载该类,如果启动类加载器能够加载的话则返回Class对象,如果不能加载则向下将该类交由扩展类加载器尝试加载;如果扩展类加载器能够加载该类则返回Class对象,如果不能加载则向下将该类交由应用类加载器加载;如果应用类加载器能够加载该类则返回Class对象,如果不能加载则向下将该类交给自定义加载器(如果有的话)进行加载;如果自定义类加载器能够加载该类则返回Class对象,如果不能加载则说明说有类加载器都无法加载到该类,则会抛出ClassNotFound Exception
从源码的角度分析双亲委派机制
如果我们需要手动使用类加载器加载类的话,是调用类加载器的loadClass()方法,所以我们可以从java.lang.ClassLoader类的loadClass(name)方法着手,分析类加载器的双亲委派机制
public Class<?> loadClass(String name) throws ClassNotFoundException { //调用本类加载器的loadClass(String name, boolean resolve)方法 return loadClass(name, false); }
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(); 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(); 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; } }
为什么JVM中会选择采用双亲委派机制
- 采用双亲委派机制,可以使加载过的类不会重复加载
- 为了安全考虑,比如有人想替换系统级别的类:java.lang.String,篡改它的实现。但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。
自定义类加载器
自定义类加载器肯定需要继承java.lang.ClassLoader类,如果我们自定义一个类加载器去执行loadClass(name)方法加载类的话,java.lang.ClassLoader类其实已经写有完整的loadClass(String name, boolean resolve)方法实现双亲委派机制,但是java.lang.ClassLoader类的findClass(name)方法没有具体的加载类的代码
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
我们只需要重写findClass(name)方法实现自己类加载器的加载类操作就行
public class CustomerClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //从"d:/test"目录中获取指定name的class文件 File f = new File("d:/test/", name.replace(".", "/").concat(".class")); try { //将改文件读取成文件输入流 FileInputStream fis = new FileInputStream(f); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int b = 0; //使用ByteArrayOutputStream将改文件中的的内容写到ByteArrayOutputStream对象中 while ((b = fis.read()) != 0) { baos.write(b); } //将ByteArrayOutputStream中的字节存储到字节数组中 byte[] bytes = baos.toByteArray(); baos.close(); fis.close(); //调用java.lang.ClassLoader的defineClass()方法,生成Class对象 return defineClass(name, bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); //throws ClassNotFoundException } public static void main(String[] args) throws Exception { ClassLoader l = new CustomerClassLoader(); Class clazz = l.loadClass("com.java.chandao.jvm.User"); Class clazz1 = l.loadClass("com.java.chandao.jvm.User"); System.out.println(clazz == clazz1); User user = (User) clazz.newInstance(); // 打印是哪个类加载器加载的CustomerClassLoader: 打印结果是AppClassLoader System.out.println(l.getClass().getClassLoader()); // 打印CustomerClassLoader的父类加载器: 打印结果是AppClassLoader System.out.println(l.getParent()); } }
通过上述类加载器,我们可以加载磁盘路径"d:/test"目录中的字节码文件,因为该目录下的字节码文件无法通过已有的三个类加载器(Bootstrap ClassLoader、ExtClassLoader、AppClassLoader)进行加载,所以经过loadClass()方法的双亲委派机制,最终会调用CustomerClassLoader类的findClass(name)方法加载类,而我们定义CustomerClassLoader类的时候重写了findClass()方法,可以加载"d:/test"目录中的字节码文件
结语
通过今天的学习,我们已经了解了JVM的基本结构,以及一个类从编译到执行所经历的过程,并且深入学习了类加载器以及双亲委派机制,并且通过查看ClassLoader类的源码以及自定义类加载器更加深入地学习了类的加载机制,希望大家都能有所收获,也希望我能伴随大家在IT道路上的成长。

本博客所有文章仅用于学习、研究和交流目的,欢迎转载,转载请注明原文作者及出处。
当你的才华撑不起她的野心时,请静下心来学习吧!

浙公网安备 33010602011771号