2.类加载机制

312 阅读6分钟

概述

虚拟机从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目间没有任何分隔符。当遇到8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。


类加载子系统工作步骤

  • 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识;
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
  • 加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

1.加载

1.1 什么时候会加载类

  • 遇到new、getstatic、putstatic、invokestatic字节码指令时
  • 使用java.lang.reflect的方法对类进行反射时
  • 初始化一个类时,发现父类还没初始化时
  • 虚拟机启动时,用户指定的主类时
  • 使用JDK1.7的动态语言支持时

1.2 从哪加载

  • 从zip包加载,如JAR,WAR等
  • 从网络中获取,如applt
  • 运行时计算生成,如java.lang.reflect.proxy中的"*$Proxy"就是类的二进制流
  • 由其他文件生成的,如JSP
  • 从数据库中读取,这种场景比较少见

1.3 怎么加载

  • 通过一个类的全限定名获取定义此类的二进制字节流;
  • 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据;
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2.链接

2.1 验证

  • 目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全
  • 主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。 (待补充)

2.2 准备

  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
  • 这里进行分配的是类变量(被static修饰过)而不包括实例变量
  • 类不会为实例变量分配初始化,类变量会分配在方法去中,而实例变量是会随着对象一起分配到java堆中

2.3 解析

  • 解析阶段是虚拟机将常量池中的符号引用转换为直接引用

符号引用是:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。

直接引用是:有了直接引用,那引用的目标必定已经被加载入内存中了.

i.直接指向目标的指针(比如,指向Class对象、类变量、类方法的直接引用可能是指向方法区的指针)
ii.相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
iii.一个能间接定位到目标的句柄

  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTAT_Methodref_info等。
  • 解析步骤:

类或接口解析:当前类为A,假如符号引用为B,需要解析成类或接口C的至直接引用,三个步骤

i.如果C不是数组类型,则将代表类B的全限定名传递给A,由A的类加载器去加载C
ii.如是C是数组类型,并且数组内为对象,将会按第一点的规则去加载对象,接着有虚拟机生成一个对象,代表数组维度和元素的数据对象
iii.上述没异常,则已经生成对象,接着验证一下A是否有对C的访问权限,无权限则报java.lang.IIlegalAccessError异常

字段解析:要解析字段,首先将字段所属的类或接口的符号引用,如这步出现异常,则直接失败

i.如果C本身包含了引用字段,则查找结束
ii.如果C中实现了接口或父类,则由下向上递归查找各个接口和父类,如果包含了,则查找结束
iii.查找结束后,验证权限,无权限则报java.lang.IIlegalAccessError异常。如果查找失败,抛出java.lang.NoSuchFieldError异常

类方法解析:类方法和接口方法的符号引用是分开的,如果发现方法中表明C是接口的话,则抛出java.lang.IncompatibleClassChangeError异常.

i.如果C本身包含了引用方法,则查找结束
ii.如果C中实现了父类,则由下向上递归查找各个父类,如果包含了,则查找结束。 如果C是抽象类,则抛出java.lang.AbstractMethodError异常
iii.查找结束后,验证权限,无权限则报java.lang.IIlegalAccessError异常。如果查找失败,抛出java.lang.NoSuchFieldError异常

接口方法解析:如果发现方法中表明C是类的话,则抛出java.lang.IncompatibleClassChangeError异常.

i.如果C本身包含了引用方法,则查找结束
ii.如果C中实现了父接口,则由下向上递归查找各个父接口,如果包含了,则查找结束 iii.查找失败,抛出java.lang.NoSuchFieldError异常,接口种方法默认是public的,不存在访问权限的问题

3.初始化

  • 初始化就是执行字节码中的()方法,clinit方法是由代码中的类变量的赋值动作和静态代码块(static{})合并产生的。没有类变量或静态代码块不会生成clinit方法

*构造器方法中指令按语句在源文件中出现的顺序执行,编译器收集的顺序是按照源文件中的顺序决定的,静态代码块能访问在它之前的类变量,之后的不能访问。

  • Clinit不同于类构造器(init),不需要显式调用父类构造器,子类的clinit执行前,父类的clinit肯定已经执行完成,所以虚拟机启动的时候,第一个加载的执行的clinit方法肯定是java.lang.Object类的
  • 虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。
  • 接口有也会有clinit方法,(接口没有静态代码,但有初始化赋值),接口的clinit不需要先执行父接口的clinit方法。

类加载器

1.加载器介绍

  • 通过一个类的全限定名来获取到描述此类的二进制字节流,这个动作放到
  • 类加载器实现类的加载动作,同时用于确定一个类。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就不相等。(比如通过自定义加载器)
  • JVM支持两种类型的加载器,分别为引导类加载器(BootStrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
  • 在程序中我们最常见的类加载器始终只有三个,如下所示:

2.自定义类与核心类库的加载器

  • 对于用户自定义类来说:使用系统类加载器AppClassLoader进行加载
  • java核心类库都是使用引导类加载器BootStrapClassLoader加载的
public static void main(String[] args) {

        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取其上层:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d

        //获取其上层:获取不到引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //对于用户自定义类来说:默认使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null
    } 
    

3.虚拟机自带的加载器

  • 启动类加载器(引导类加载器,BootStrap ClassLoader)
    • 这个类加载使用C/C++语言实现的,嵌套在JVM内部
    • 它用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
    • 并不继承自java.lang.ClassLoader,没有父加载器
    • 加载拓展类和应用程序类加载器,并指定为他们的父加载器
    • 处于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类
  • 拓展类加载器(Extension ClassLoader)
    • java语言编写 ,由sun.misc.Launcher$ExtClassLoader实现
    • 派生于ClassLoader类
    • 父类加载器为启动类加载器
    • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
  • 应用程序类加载器(系统类加载器,AppClassLoader)
    • java语言编写, 由sun.misc.Launcher$AppClassLoader实现。
    • 派生于ClassLoader类
    • 父类加载器为拓展类加载器
    • 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
    • 该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
    • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
public static void main(String[] args) {
        System.out.println("**********启动类加载器**************");
        //获取BootstrapClassLoader能够加载的api的路径
        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urLs) {
            System.out.println(element.toExternalForm());
        }
        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);

        System.out.println("***********扩展类加载器*************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }

        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d

    }

4.用户自定义类加载器

隔离加载类
拓展加载源
防止源码泄漏
多版本Jar包

操作步骤1.继承ClassLoader类 2. 重写findClass方法

public class CustomClassLoader extends ClassLoader {

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {

    try {
      byte[] result = getClassFromCustomPath(name);
      if (result == null) {
        throw new FileNotFoundException();
      } else {
        return defineClass(name, result, 0, result.length);
      }
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }

    throw new ClassNotFoundException(name);
  }

  private byte[] getClassFromCustomPath(String name) {
    //从自定义路径中加载指定类:细节略
    //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
    return null;
  }
}

5.双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将她的class文件加载到内存生成的class对象。而且加载某个类的class文件时,java虚拟机采用的是双亲微拍模式,即把请求交由父类处理,它是一种任务委派 模式

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        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();
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    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;
        }
    }

5.1双亲委派机制的作用

  • 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
  • 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

沙箱安全:防止恶意代码污染java源代码

比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染

类的主动使用和被动使用

  • 创建类的实例
  • 访问某各类或接口的静态变量,或者对静态变量赋值
  • 调用类的静态方法
  • 反射 比如Class.forName(com.dsh.jvm.xxx)
  • 初始化一个类的子类
  • java虚拟机启动时被标明为启动类的类
  • JDK 7 开始提供的动态语言支持
  • 同上方的1.1 什么时候会加载类

JVM完整目录

1. jvm概述
2.类加载机制
3.运行时数据区[PC寄存器、虚拟机栈、本地方法栈]
4.运行时数据区[堆]
5.运行时数据区[方法区]
6.暂缺
7. 运行时数据区[对象的实例化内存布局与访问定位、直接内存]
8.执行引擎(Execution Engine)
9.字符串常量池
10.垃圾回收[概述、相关算法]
11.垃圾回收[垃圾回收相关概念]
12.垃圾回收[垃圾回收器]
13.常见的OOM
14. JDK命令行工具