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道路上的成长。 

 

 

posted @ 2020-09-22 16:56  Richard&Leevi  阅读(164)  评论(2)    收藏  举报