文章目录
JVM的类加载机制
1、类加载机制
类(class)只有被加载到JVM中才可以运行。这个加载过程是通过类加载器来实现的,实质上就是把类文件从硬盘读取到内存中。
整个流程如下:
其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。另外,这个过程表示的是按顺序开始,不是所谓的第一步、第二步、第三步的关系,而往往是交叉混合进行,在一个阶段中可能调用或者激活另一个过程。
除此之外,任何一个类只有在运行期间使用到该类的时候才会将该类加到内存中。总之,java依赖于运行期间动态加载和动态链接来实现类的动态使用。
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”:
(1)使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
(2)初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
(3)使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
(4)虚拟机启动时,用户会先初始化要执行的主类(含有main)
(5)jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
2、类加载的方式
类的加载方式可以分为显示加载和隐式加载。
隐式加载就是程序在调用new等方式创建一个对象的时候,会隐式调用类加载器把对应的类加载到JVM中。
显示加载指的是通过Class.forName()方法把需要用到的类加载到JVM中。
一般在程序启动的时候,只把需要的类加载到JVM中,其它类等到被使用到的时候才会被加载。这样可以加快加载速度,并且可以节约程序运行过程中的内存开销。
3、类加载的步骤(过程)
3.1加载
加载过程主要完成三件事情:
(1)通过类的全限定名来获取定义此类的二进制字节流
(2)将这个类字节流代表的静态存储结构转为方法区的运行时数据结构,在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
这个过程主要就是类加载器完成。(对于HotSpot虚拟而言,Class对象较为特殊,其被放置在方法区而不是堆中)
3.2验证
此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。主要包括以下四个阶段:
(1)文件格式验证:
基于字节流验证,验证字节流符合当前的Class文件格式的规范,能被当前虚拟机处理。验证通过后,字节流才会进入内存的方法区进行存储。
(2)元数据验证:
基于方法区的存储结构验证,对字节码进行语义验证,确保不存在不符合java语言规范的元数据信息。
(3)字节码验证:
基于方法区的存储结构验证,通过对数据流和控制流的分析,保证被检验类的方法在运行时不会做出危害虚拟机的动作。
(4)符号引用验证:
基于方法区的存储结构验证,发生在解析阶段,确保能够将符号引用成功的解析为直接引用,其目的是确保解析动作正常执行。换句话说就是对类自身以外的信息进行匹配性校验。
3.3准备
仅仅为类变量(static修饰的变量)分配内存空间并且设置该类变量的初始值(这里的初始值指的是数据类型默认的零值),这里不包含用final修饰的static,因为用final修饰的类变量在javac执行编译期间就会分配,同时要注意,这里不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量会在对象实例化是随着对象一起被分配在java堆中。举例:
public static int value=33;
这据代码的赋值过程分两次,一是上面我们提到的阶段,此时的value将会被赋值为0;而value=33这个过程发生在类构造器的()方法中。
3.4解析
解析阶段主要是将常量池内的符号引用替换为直接引用的过程。符号引用是用一组符号来描述目标,可以是任何字面量,而直接引用则是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。通常而言一个符号引用在不同虚拟机实例翻译出来的直接引用一般不会相同。
和C之类的纯编译型语言不同,Java类文件在编译过程中只会生成class文件,并不会进行连接操作,这意味在编译阶段Java类并不知道引用类的实际地址,因此只能用“符号引用”来代表引用类。举个例子来说明,在com.sbbic.Person类中引用了com.sbbic.Animal类,在编译阶段,Person类并不知道Animal的实际内存地址,因此只能用com.sbbic.Animal来代表Animal真实的内存地址。在解析阶段,JVM可以通过解析该符号引用,来确定com.sbbic.Animal类的真实内存地址(如果该类未被加载过,则先加载)。
主要有以下四种:
(1)类或接口的解析
(2)字段解析
(3)类方法解析
(4)接口方法解析
3.5初始化
类加载过程的最后一步,到该阶段才真正开始执行类中定义的java代码,同样该阶段也是初始化类变量和其他资源(执行static字段和静态代码块),换句话说该阶段是执行类构造器 <clinit>()
方法的过程。
<clinit>()
方法是由编译器自动收集类中所有的类变量的赋值动作和静态语句块(static{})中的语句合并而成。<clinit>()
方法和实例构造方法不同<init>()
不同,它不需要显示的调用父类的<clinit>()
,虚拟机会保证父类的<clinit>()
方法在子类的<clinit>()
方法之前执行完成,也就是说,父类的静态语句块和静态变量优先于子类中变量赋值操作。
<clinit>()
方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量赋值的操作,那么编译器就不会为这个类生成<clinit>()
方法。接口中不能使用静态语句块,单仍然有变量赋值的操作,所以仍然可以生成<clinit>()
方法,但与类不同的执行接口<clinit>()
方法不需要先执行父接口的<clinit>()
方法,另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()
方法。
4、类加载器
把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。
4.1类加载器的分类
在java中,类的加载是动态的,并不会一次性把所有的类加载再运行,而是保证程序运行的基础类(例如基类)完全加载到JVM中,至于其它类,是在需要时才加载。
java中,可以把类分为三大类:系统类、扩展类和自定义类,对应提出三种类型的加载器:启动类加载器(Bootstrap Loader)、扩展类加载器(ExtClassLoader)、应用程序类加载器(AppClassLoader)。
(1)启动类加载器(Bootstrap Loader)
负责加载系统类(jre/lib/rt.jar)
(2)扩展类加载器(ExtClassLoader)
负责加载扩展类(jre/lib/ext/*.jar)
(3)应用程序类加载器(AppClassLoader)
复杂加载应用类(claspath指定的目录或者jar中的类)
4.2 类加载器的双亲委派模型
所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
当一个类加载器收到一个类加载的请求,它首先会将该请求委派给父类加载器去加载,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该被传入到顶层的启动类加载器(Bootstrap ClassLoader)中,只有当父类加载器反馈无法完成这个列的加载请求时(它的搜索范围内不存在这个类),子类加载器才尝试加载。其层次结构示意图如下:
不难发现,该种加载流程的好处在于:
(1)可以避免重复加载,父类已经加载了,子类就不需要再次加载
(2)更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
4.3类加载器的特点
(1)运行任何一个程序时,总是由Application Loader开始加载指定的类。
(2)一个类在收到加载类请求时,总是先交给其父类尝试加载。
(3)Bootstrap Loader是最顶级的类加载器,其父加载器为null。
4.4类加载的几个时机
(1)创建类的实例,也就是new一个对象
(2)访问某个类或接口的静态变量,或者对该静态变量赋值
(3)调用类的静态方法
(4)反射(Class.forName(“com.lyj.load”))
(5)初始化一个类的子类(会首先初始化子类的父类)
(6)JVM启动时标明的启动类,即文件名和类名相同的那个类
4.5破坏双亲委派模型
双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。
若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。
下面介绍两个例子来讲解破坏双亲委派模型的过程。
(1)JNDI破坏双亲委派模型
JNDI是Java标准服务,它的代码由启动类加载器去加载。但是JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。
为了解决这个问题,引入了一个线程上下文类加载器。 可通过Thread.setContextClassLoader()设置。
利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。
(2)Spring破坏双亲委派模型
Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。
那么Spring是如何访问WEB-INF下的用户程序呢?
使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。
利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。
为什么静态变量或者静态方法能够实现单例???
ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。
所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。