java虚拟机类加载机制

时间:2021-08-08 10:18:43

1、概述:

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

java类的生命周期:从类被加载到虚拟机内存中开始,到卸载出内存为止,类的生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段
java虚拟机类加载机制

在Java语言中,类型的加载和连接过程都是在程序运行期间完成的(Java可以动态扩展的语言特性就是依赖运行期动态加载、动态连接这个特点实现的),这样会在类加载时稍微增加一些性能开销,但是却为Java应用程序提供高度的灵活性。

加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的,解析阶段则不一定,在某些情况下,解析阶段有可能在初始化阶段结束后开始,以支持Java的动态绑定。

2、加载(Loading):“加载”是类加载阶段的一个过程

2.1、主要完成以下3件事情:

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

说明:1、对于第一条“通过这个类的全限定名来获取这个类的二进制流”获取的途径很多:可以从JAR、EAR等压缩包中获取、可以从网络中获取、可以运行时计算生成,这种场景应用最多的是动态代理、可以从其他文件中生成,典型的jsp文件等等。2、对于第三条,并没有明确规定是存放在java堆内存中,对于HotSpot虚拟机是存放在方法区里面。

2.2、类与类加载器:

类加载器虽然只用于实现类的加载动作,但是他在java程序中起的作用却远远不限于类加载阶段。对于任意一个类,都需要有加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。这句话可以说得通俗一些:比较两个类是否“相等”,只有这两个类是由同一个类加载器的前提下才有意义,否则,即使这两个类源自于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这连个类就必定不相等。

2.3、JVM加载类过程:

当我们使用命令来执行某一个Java程序(比如Test.class)的时候:java Test
(1) java.exe 会帮助我们找到JRE ,接着找到位于JRE内部的 jvm.dll ,这才是真正的Java虚拟机器 , 最后加载动态库,激活Java虚拟机器。

(2) 虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器 —-Bootstrap Loader(启动类加载器 ) 。

(3) Bootstrap Loader所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的 ExtClassLoader(扩展类加载器)  ,并设定其 Parent为null,代表其父加载器为 BootstrapLoader 。 Launcher$ExtClassLoader.class

(4) 然后Bootstrap Loader再要求加载 Launcher.java之中的 AppClassLoader(用户自定义类加载器 ) ,并设定其 Parent为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。  Launcher$AppClassLoader.class

2.4、类加载双亲委派模型:
从虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 语言实现, 是虚拟机自身的一部分:另一种就是所有其它的类加载器, 这些类加载器用Java 语言实现,独立于虚拟机外部,并且全都继承与抽象类 java.lang.ClassLoader。

从Java 开发人员的角度来看,类加载器还可以划分的更细致一些,绝大多数Java 程序都会用到以下3种系统提供的类加载器。
1. 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放%JAVA_HOME%\lib 目录中的类库加载到虚拟机内存中;
2. 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher.ExtClassLoader 实现,它负责加载%JAVA_HOME%\lib\ext 目录中的的所有类库,开发者可以直接使用扩展类加载器。;
3. 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher.AppClassLoader 实现,开发者可以使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

java虚拟机类加载机制

双亲委派模型的工作过程:如果一个类加载器收到了类加载器的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜说范围中没有找到所需的类时),子加载类才会尝试自己去加载。

使用上级委托机制组织类加载器之间的关系的好处是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:java.lang.Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委托给启动类加载器进行加载,启动类加载器在其搜索范围内只能搜索到rt.jar中的java.lang.Object类,这样可以保证Object类始终由启动类加载器从rt.jar中的java.lang.Object加载得到,确保了Object类的唯一性。
如果没有使用上级委托机制,由各个类加载器自行加载的话,如果用户自己实现一个名为java.lang.Object类,并用自定义的类加载器进行加载,系统中将出现多个不同的Object类,Java类型体系中最基础的行为将无法保证,应用程序也将变得一片混乱

3、连接(Linking):
3.1、验证(VerifyError): 这是连接阶段的第一步,这一阶段的目的就是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常。

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这个阶段的验证时给予字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储所以后面的验证阶段都是给予方法区的存储结构进行的;
  • 元数据验证:对类的元数据信息进行语义分析,保证不存在不符合java语言规范的元数据信息。例如,这个类是否有父类(除java.lang.Object之外都是有父类的)、这个类是否继承了不允许继承的类(被final修饰的)等等;
  • 字节码验证:进行数据流和控制流分析,确定程序语义是合法的、复合逻辑的;
  • 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),对常量池中的各种符号引用的信息进行匹配性的校验。

3.2、准备(Preparation):正式为类变量分配内存并设置类变量初始值的阶段。
强调两个方面:1、该阶段进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量,实例变量将在对象初始化时随对象一起分配在堆内存中;2、这里所说的初始值“通常情况下”是指数据类型的零值,如一个类变量定义为:public static int a =1。基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。引用类型的默认值为null。

3.3、解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程。
那么什么是符号引用,什么又是直接引用呢?
我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过*局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如山东省滨州市滨城区18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而山东省滨州市滨城区18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

3、初始化(Initialization): 在准备阶段,变量已经赋过一次初始值,在初始化阶段,则是根据程序员的要求去初始化类变量和其它资源,简单说,初始化阶段即虚拟机执行类构造器clinit()方法的过程。

对于初始化阶段,虚拟机规范严格规定了有且只有5种情况必须立即对类进行”初始化“(当然加载,验证,准备自然需要在此之前开始),称为类的主动引用:
1、遇到new、getstatic、putstatic或invokestatic这四个关键字时,如果没有进行初始化,需要触发其初始化。生成这四个指令的常见的java场景是:使用new关键字实例化对象,读取或设置一个类的静态字段(被final修饰、已在编译器把结果存放到了常量此的静态字段除外),以及调用一个静态类的方法;
2、使用java.lang.reflect包中的方法对类进行反射调用时,如果类还没有初始化,则必须首先对其初始化;
3、当初始化一个类时,如果其父类还没有初始化,则必须首先初始化其父类;
4、当虚拟机启动时,需要指定一个主类(main方法所在的类),虚拟机会首选初始化这个主类
5、当使用jdk1.7的动态语言支持时

除此之外,其他引用类的方式不会触发初始化,称为被动引用,如下面的几个例子:

被动引用的例子之一:通过子类引用父类的静态变量,不会导致子类的初始化。

public class SuperClass {

static {
System.out.println("SuperClass init!");
}

public static int value = 123;

}

public class SubClass extends SuperClass {

static {
System.out.println("SubClass init !");
}

}

public class NotInitialization {

public static void main(String[] args) {
System.out.println(SubClass.value);
}

}

输出结果:SuperClass init!
     123
结果证明:对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类的静态变量,只会触发父类的初始化而不会触发子类的初始化。

被动引用的例子之二:通过数组定义来引用类,不会触发此类的初始化。

public class NotInitialization {

public static void main(String[] args) {
SuperClass [] sca = new SuperClass[10];
}

}

被动引用的例子之三:常量在编译阶段会存放入调用类的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

public class ConstClass {

static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";

}

public class NotInitialization {

public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}

}

输出结果:hello world
结果证明:并没有输出“ConstClass init!”,虽然在源码中引用到了ConstClass 类的常量HELLOWORLD,但其实在编译期就已经通过常量传播优化,已经将此常量的值“hello world”存储到了NotInitialization 的常量池中,以后NotInitialization 对常量ConstClass.HELLOWORLD的引用实际都被转化为NotInitialization 类对自身常量池的引用。也就是说,实际上NotInitialization 的Class文件之中并没有ConstClass 类的符号引用入口,这两个类在编译成class之后就没有了任何联系了。