深入理解Java虚拟机:JVM类加载机制
作者:老实巴交
来源:SegmentFault 思否社区
类加载
首先我们需要明确,JVM是动态加载class类的,而不是一次性加载程序中所有的class文件(延迟加载机制)。当我们需要这个类的时候,比如说程序在运行过程中classA需要调用另一个classB的方法,但classB没有被加载,这时候就需要通过类加载机制动态加载classB到内存中,只有当classB被加载到内存中,它的方法才能被classA调用。
类加载:通过加载,连接和初始化三个过程将class文件中的信息加载到JVM内存中。
1、加载:
1、通过类的全限定性类名获取该类的二进制流;
怎么找呢?在系统类和指定的类路径中寻找,如果是class文件的根目录,就直接查看是否有对应的子目录及文件,如果是jar文件,则现在内存中解压该文件,再查看有无对应的类。对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的,但是数组类的元素类型是类加载器加载的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。
2、将该二进制流的静态存储结构转为方法区的运行时数据结构;
3、在堆中为该类生成一个java.lang.Class对象,作为方法区这个类的各种数据结构访问入口
而类加载器在这个过程中扮演了重要的角色,类加载器主要有两个作用:
一:用来动态地加载class类,将class的字节码形式转换为内存形式的class对象。
为什么是动态的呢?这里就涉及到JVM的延迟加载机制,JVM并不是一次性加载程序中的所有类的,而是按需加载,也就是当一个程序中遇到不认识的新类的时候,调用ClassLoader来加载这个类,加载完成后就会将Class对象存在ClassLoader中,下次就不需要加载了。一个写好的Java程序可能有很多类,这些类呢,有很多方法,在程序运行时候经常需要从一个class中调用另一个class中的方法,但是系统启动的时候不会一次性加载程序中的所有class文件,而是根据需要通过JVM的类加载机制来动态地加载某个class文件到内存中,只有当class文件被加载到内存中,才能被其他class引用,所以classLoader是用来动态加载class文件到内存中的。
比较形象的例子就是调用某个类的静态方法,那么需要加载这个类(因为静态方法在类被创建出来之后就有了),但并不需要加载这个类的实例,而实例是在被程序调用的时候才会被加载。
二:它是类的命名空间,起到了类隔离的作用。在同一个ClassLoader里面加载的类名是唯一不可重复的,不同类加载器中的同名类是两个不同的类。类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
类加载器:
启动类加载器:在Java 8 时,主要加载java的基础类,也就是JAVA_HOME/jre/lib/rt.jar下的所有class,Java 9以后它负责加载启动时的基础模块类(如 Java.base; java.manager;java.xml)。它是通过C++实现的,并不存在于JVM体系中,所以输出是null。
在JVM启动时,通过Bootstrap ClassLoader加载rt.jar,并初始化sun.misc.Launcher从而创建Extension ClassLoader和Application ClassLoader的实例。
扩展类加载器 它主要加载\lib\ext或者java.ext.dirs系统变量指定的路径中的所有类库;加载一些拓展的系统类,比如XML、加密、压缩相关的功能类。
它由sun.misc.Launcher$ExtClassLoader实现;向上继承自URLClassLoader, URLClassLoader向上继承自SecueClassLoader,SecueClassLoade继承自ClassLoader。
在java 9以后替换为平台类加载器,加载平台相关的模块,比如比如java.scripting、java.compiler、 java.corba。
应用类加载器 在Java 8 中主要加载classpath所有的jar包和类,Java 9 中用于加载应用级别的模块,入jdk.compiler; jdk.jartool; jdk.jshell。
它由sun.misc.Launcher$AppClassLoader实现。
如果应用程序中没有定义自己的加载器,则该加载器也就是默认的类加载器。该加载器可以通过java.lang.ClassLoader.getSystemClassLoader获取。
双亲委派机制
先自底向上判断该类是否被加载过,若已经被加载,直接返回
若未被加载,则自顶向上尝试加载类,当父类没有所需的类,无法加载的时候,子加载器尝试去完成加载,如果JVM内置的类加载器无法加载,则有自定义加载器进行加载
具体:当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,并进行后面的步骤,最后返回这个类在内存中的Class实例对象。
Parents Delegation Model:双亲委派模型,简单可以理解为优先级和共享两个关键词,为了防止Java核心API被替换,根加载器优先加载,如果没有,那么自顶向下加载。当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。
2、连接
1验证,主要是为了确保class文件的字节流中包含的信息符合Java虚拟机规范,保证这些信息被当作代码的时候不会危害虚拟机自身的安全。
主要是格式验证,元数据验证(是否符合Java语言规),字节码验证(确定程序语义合法,符合逻辑),符号验证(确保下一步的解析能正常执行)。它很重要,但不是必须执行的。
2准备为静态变量在方法区分配内存,并设置初始默认值。仅仅是类变量(即静态变量),而不包括实例变量,实例变量是在对象实例化后随着对象一块儿分配到Java堆中。而类变量在JDK7之前存放在方法区中,而7以后随着Class对象存放在Java堆中。(这里可能会考默认值,int 0,long 0等等)。
3虚拟机将常量池内的符号引用替换成直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。更多的问多态
注意:Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。
3、初始化
是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。
当有继承关系时,先初始化父类再初始化子类,所以创建一个子类时其实内存中存在两个对象实例。如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次,这个特性被用来实现单例的延迟初始化。