JVM和ClassLoader

JVM和ClassLoader

2019-11-08java

目录算法

1 JVM架构总体架构
  1.1 类加载器子系统
    1.1.1 加载
    1.1.2 连接
    1.1.3 初始化
  1.2 运行时数据区(Runtime Data Area)
  1.3 执行引擎
  1.4 示例
2 classloader加载class文件的原理和机制
  2.1 Classloader 类结构分析
  2.2 实现类的热部署
  2.3 类加载器的双亲委派模型
  2.4 类加载的三种方式
  2.5 自定义类加载器的两种方式
参考数组

 

 

1 JVM架构总体架构


 

  返回安全

图1 JVM总体架构图网络

JVM被分为三个主要的子系统:数据结构

  • 类加载器子系统
  • 运行时数据区
  • 执行引擎

1.1 类加载器子系统 


  返回架构

图2 类加载器jvm

Java的动态类加载功能是由类加载器子系统处理。当它在运行时(不是编译时)首次引用一个类时,它加载、连接并初始化该类文件。ide

1.1.1 加载

加载器:类由此组件加载。启动类加载器 (BootStrap class Loader)、扩展类加载器(Extension class Loader)和应用程序类加载器(Application class Loader) 这三种类加载器帮助完成类的加载。函数

  • 启动类加载器 – 负责从启动类路径中加载类,无非就是rt.jar。这个加载器会被赋予最高优先级。
  • 扩展类加载器 – 负责加载ext 目录(jrelib)内的类.
  • 应用程序类加载器 – 负责加载应用程序级别类路径,涉及到路径的环境变量等etc.上述的类加载器会遵循委托层次算法(Delegation Hierarchy Algorithm)加载类文件,这个在后面进行讲解。 

加载过程:

  1. 经过类的全限定名来获取定义此类的二进制字节流
  2. 将这个类字节流表明的静态存储结构转为方法区的运行时数据结构
  3. 在堆中生成一个表明此类的java.lang.Class对象,做为访问方法区这些数据结构的入口。

1.1.2 连接 

校验: 字节码校验器会校验生成的字节码是否正确,若是校验失败,咱们会获得校验错误。

  • 文件格式验证:基于字节流验证,验证字节流符合当前的Class文件格式的规范,能被当前虚拟机处理。验证经过后,字节流才会进入内存的方法区进行存储。
  • 元数据验证:基于方法区的存储结构验证,对字节码进行语义验证,确保不存在不符合java语言规范的元数据信息。
  • 字节码验证:基于方法区的存储结构验证,经过对数据流和控制流的分析,保证被检验类的方法在运行时不会作出危害虚拟机的动做。
  • 符号引用验证:基于方法区的存储结构验证,发生在解析阶段,确保可以将符号引用成功的解析为直接引用,其目的是确保解析动做正常执行。换句话说就是对类自身之外的信息进行匹配性校验。

准备:分配内存并初始化默认值给全部的静态变量。
public static int value=33;

这据代码的赋值过程分两次,一是上面咱们提到的阶段,此时的value将会被赋值为0;而value=33这个过程发生在类构造器的<clinit>()方法中。

解析:全部符号引用被方法区(Method Area)的直接引用所替代。

举个例子来讲明,在com.sbbic.Person类中引用了com.sbbic.Animal类,在编译阶段,Person类并不知道Animal的实际内存地址,所以只能用com.sbbic.Animal来表明Animal真实的内存地址。在解析阶段,JVM能够经过解析该符号引用,来肯定com.sbbic.Animal类的真实内存地址(若是该类未被加载过,则先加载)。

主要有如下四种:类或接口的解析,字段解析,类方法解析,接口方法解析

解析理解:

常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用总结起来则包括了下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

其中:

  • 全限定名:就是完整类名把.改成/。
  • 描述符:字段的类型,方法的返回类型和参数列表(参数列表又包含每一个参数的类型)。

符号引用和直接引用的区别与关联:

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定已经加载到了内存中。
  • 直接引用:直接引用能够是直接指向目标的指针、相对偏移量(看下图6)或是一个能间接定位到目标的句柄(看下图5)。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同。若是有了直接引用,那说明引用的目标一定已经存在于内存之中了。

1.1.3 初始化

这是类加载的最后阶段,这里全部的静态变量会被赋初始值, 而且静态块将被执行。

java中,对于初始化阶段,有且只有如下五种状况才会对要求类马上初始化:

  • 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类;
  • 初始化类的时候,若是其父类没有被初始化过,则要先触发其父类初始化;
  • 使用java.lang.reflect包的方法进行反射调用的时候,若是类没有被初始化,则要先初始化;
  • 虚拟机启动时,用户会先初始化要执行的主类(含有main);
  • jdk 1.7后,若是java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,而且这个方法所在类没有初始化,则先初始化;

1.2 运行时数据区(Runtime Data Area)


  返回

  • The 运行时数据区域被划分为5个主要组件:
  • 方法区 (线程共享) 常量 静态变量 JIT(即时编译器)编译后代码也在方法区存放
  • 堆内存(线程共享) 垃圾回收的主要场地
  • 程序计数器 当前线程执行的字节码的位置指示器
  • Java虚拟机栈(栈内存) :保存局部变量,基本数据类型以及堆内存中对象的引用变量
  • 本地方法栈 (C栈):为JVM提供使用native方法的服务

图4 运行时数据区

 

1.3 执行引擎


  返回

分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。

解释器:解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用屡次,每次都须要从新解释。

编译器:JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但若是是重复的代码则使用JIT编译器将所有字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提升了系统的性能。

  • 中间代码生成器– 生成中间代码
  • 代码优化器– 负责优化上面生成的中间代码
  • 目标代码生成器– 负责生成机器代码或本机代码d. 探测器(Profiler) – 一个特殊的组件,负责寻找被屡次调用的方法。

垃圾回收器: 收集并删除未引用的对象。能够经过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字建立的对象。因此,若是不是用new建立的对象,你可使用finalize函数来执行清理。Java本地接口 (JNI): JNI会与本地方法库进行交互并提供执行引擎所需的本地库。本地方法库:它是一个执行引擎所需的本地库的集合。

1.4 示例


  返回

经过如下代码看JVM类加载执行过程

 1 package com.example.demo.classloader;
 2 /**
 3  * 从JVM调用的角度分析java程序堆内存空间的使用:
 4  * 当JVM进程启动的时候,会从类加载路径中找到包含main方法的入口类HelloJVM
 5  * 找到HelloJVM会直接读取该文件中的二进制数据,而且把该类的信息放到运行时的Method内存区域中。
 6  * 而后会定位到HelloJVM中的main方法的字节码中,并开始执行Main方法中的指令
 7  * 此时会建立Student实例对象,而且使用student来引用该对象(或者说给该对象命名),其内幕以下:
 8  * 第一步:JVM会直接到Method区域中去查找Student类的信息,此时发现没有Student类,就经过类加载器加载该Student类文件;
 9  * 第二步:在JVM的Method区域中加载并找到了Student类以后会在Heap区域中为Student实例对象分配内存,
10  *         而且在Student的实例对象中持有指向方法区域中的Student类的引用(内存地址);
11  * 第三步:JVM实例化完成后会在当前线程中为Stack中的reference创建实际的应用关系,此时会赋值给student
12  * 接下来就是调用方法
13  * 在JVM中方法的调用必定是属于线程的行为,也就是说方法调用自己会发生在线程的方法调用栈:
14  * 线程的方法调用栈(Method Stack Frames),每个方法的调用就是方法调用栈中的一个Frame,
15  * 该Frame包含了方法的参数,局部变量,临时数据等 student.sayHello();
16  */
17 public class HelloJVM {
18     //在JVM运行的时候会经过反射的方式到Method区域找到入口方法main
19     public static void main(String[] args) {//main方法也是放在Method方法区域中的
20         /**
21          * student(小写的)是放在主线程中的Stack区域中的
22          * Student对象实例是放在全部线程共享的Heap区域中的
23          */
24         Student student = new Student("spark");
25         /**
26          * 首先会经过student指针(或句柄)(指针就直接指向堆中的对象,句柄代表有一个中间的,student指向句柄,句柄指向对象)
27          * 找Student对象,当找到该对象后会经过对象内部指向方法区域中的指针来调用具体的方法去执行任务
28          */
29         student.sayHello();
30     }
31 }
32 class Student {
33     // name自己做为成员是放在stack区域的可是name指向的String对象是放在Heap中
34     private String name;
35     public Student(String name) {
36         this.name = name;
37     }
38     //sayHello这个方法是放在方法区中的
39     public void sayHello() {
40         System.out.println("Hello, this is " + this.name);
41     }
42 }
View Code

 

对象的访问定位

java程序须要经过引用(ref)数据来操做堆上面的对象,那么如何经过引用定位、访问到对象的具体位置。

对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式

  • 句柄访问对象
  • 直接指针访问对象。(Sun HotSpot使用这种方式)

句柄访问优势:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不须要改动引用【ref】自己。

直接指针访问优势:优点很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【多是出于Java中对象的访问时十分频繁的,平时咱们经常使用的JVM HotSpot采用此种方式】

图5 经过句柄访问对象

图6 经过指针访问对象

 

2 classloader加载class文件的原理和机制


  返回

2.1 Classloader 类结构分析

主要由四个方法,分别是 defineClass , findClass , loadClass , resolveClass

  • Class defineClass(String name,byte[] b,int len):将类文件的字节数组转换成JVM内部的java.lang.Class对象。字节数组能够从本地文件系统、远程网络获取。参数name为字节数组对应的全限定类名。

  • Class findClass(String name),经过类名去加载对应的Class对象。当咱们实现自定义的classLoader一般是重写这个方法,根据传入的类名找到对应字节码的文件,并经过调用defineClass解析出Class独享

  • Class loadClass(String name) :name参数指定类装载器须要装载类的名字,必须使用全限定类名,如:com.smart.bean.Car。该方法有一个重载方法 loadClass(String name,boolean resolve),resolve参数告诉类装载器时候须要解析该类,在初始化以前,因考虑进行类解析的工做,但并非全部的类都须要解析。若是JVM只须要知道该类是否存在或找出该类的超类,那么就不须要进行解析。

  • resolveClass手动调用这个使得被加到JVM的类被连接(解析resolve这个类?)

实现自定义 ClassLoader 通常会继承 URLClassLoader 类,由于这个类实现了大部分方法。

2.2 实现类的热部署


 

  返回

  • 同一个classLoader的两个实例加载同一个类,JVM也会识别为两个
  • 不能重复加载同一个类(全名相同,并使用同一个类加载器),会报错
  • 不该该动态加载类,由于对象被引用后,对象的属性结构被修改会引起问题

注意:使用不一样classLoader加载的同一个类文件获得的类,JVM将看成是两个不一样类,使用单例模式,强制类型转换时均可能由于这个缘由出问题。

2.3 类加载器的双亲委派模型


  返回

图3 类加载器双亲委派模型

类加载器双亲委派模型加载顺序:java的三种类加载器存在父子关系,子 加载器保存着附加在其的引用,当一个类加载器须要加载一个目标类时,会先委托父加载器去加载,而后父加载器会在本身的加载路径中搜索目标类,父加载器在本身的加载范围中找不到时,才会交给子加载器加载目标类。

采用双亲委托模式能够避免类加载混乱,并且还将类分层次了,例如java中lang包下的类在jvm启动时就被启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppClassLoader)加载,基于双亲委托模式,就算用户定义了与lang包中同样的类,最终仍是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,因此二者都不会再从新加载。固然,若是使用者经过自定义的类加载器能够强行打破这种双亲委托模型,但也不会成功的,java安全管理器抛出将会抛出java.lang.SecurityException异常。 

2.4 类加载的三种方式


  返回

  • 经过命令行启动应用时由JVM初始化加载含有main()方法的主类。
  • 经过Class.forName()方法动态加载,会默认执行初始化块(static{}),可是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
  • 经过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
//1 由new关键字建立一个类的实例,在由运行时刻用 new 方法载入 
Person person = new Person();
//2 使用Class.forName()    经过反射加载类型,并建立对象实例
Class clazz = Class.forName("Person");
Object person =clazz.newInstance();
//3 使用某个ClassLoader实例的loadClass()方法,经过该 ClassLoader 实例的 loadClass() 方法载入。应用程序能够经过继承 ClassLoader 实现本身的类装载器。
Class clazz = classLoader.loadClass("Person");
Object person =clazz.newInstance();

 

其中:

  • 1和2使用的类加载器是相同的,都是当前类加载器(即:this.getClass.getClassLoader)。
  • 3由用户指定类加载器。若是须要在当前类路径之外寻找类,则只能采用第3种方式。即第3种方式加载的类与当前类分属不一样的命名空间。
  • 1是静态加载,二、3是动态加载
  • Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);

    第2个boolean参数表示类是否须要初始化, Class.forName(className)默认是须要初始化。

    一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。

  • ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);

    第2个 boolean参数,表示目标对象是否进行连接,false表示不进行连接,由上面介绍能够,

    不进行连接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会获得执行

2.5  自定义类加载器的两种方式


  返回

  • 遵照双亲委派模型:继承ClassLoader,重写findClass()方法。
  • 破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。

一般咱们推荐采用第一种方法自定义类加载器,最大程度上的遵照双亲委派模型。 自定义类加载的目的是想要手动控制类的加载,那除了经过自定义的类加载器来手动加载类这种方式,还有其余的方式么?

利用现成的类加载器进行加载:

  • 利用当前类加载器
    Class.forName();
  • 经过系统类加载器
    Classloader.getSystemClassLoader().loadClass();
  • 经过上下文类加载器
    Thread.currentThread().getContextClassLoader().loadClass(); 

 

参考

[1] classloader加载class文件的原理和机制

[2] java 类加载器双亲委派模型

[3] 深刻理解JVM-内存模型(jmm)和GC

[4] 【深刻Java虚拟机】之二:Class类文件结构

相关文章
相关标签/搜索