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总体架构图网络
JVM被分为三个主要的子系统:数据结构
返回架构
图2 类加载器jvm
Java的动态类加载功能是由类加载器子系统处理。当它在运行时(不是编译时)首次引用一个类时,它加载、连接并初始化该类文件。ide
加载器:类由此组件加载。启动类加载器 (BootStrap class Loader)、扩展类加载器(Extension class Loader)和应用程序类加载器(Application class Loader) 这三种类加载器帮助完成类的加载。函数
加载过程:
校验: 字节码校验器会校验生成的字节码是否正确,若是校验失败,咱们会获得校验错误。
准备:分配内存并初始化默认值给全部的静态变量。
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的常量值等。而符号引用总结起来则包括了下面三类常量:
其中:
符号引用和直接引用的区别与关联:
这是类加载的最后阶段,这里全部的静态变量会被赋初始值, 而且静态块将被执行。
java中,对于初始化阶段,有且只有如下五种状况才会对要求类马上初始化:
图4 运行时数据区
分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。
解释器:解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用屡次,每次都须要从新解释。
编译器:JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但若是是重复的代码则使用JIT编译器将所有字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提升了系统的性能。
垃圾回收器: 收集并删除未引用的对象。能够经过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字建立的对象。因此,若是不是用new建立的对象,你可使用finalize函数来执行清理。Java本地接口 (JNI): JNI会与本地方法库进行交互并提供执行引擎所需的本地库。本地方法库:它是一个执行引擎所需的本地库的集合。
经过如下代码看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 }
对象的访问定位
java程序须要经过引用(ref)数据来操做堆上面的对象,那么如何经过引用定位、访问到对象的具体位置。
对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
句柄访问优势:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不须要改动引用【ref】自己。
直接指针访问优势:优点很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【多是出于Java中对象的访问时十分频繁的,平时咱们经常使用的JVM HotSpot采用此种方式】
图5 经过句柄访问对象
图6 经过指针访问对象
主要由四个方法,分别是 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加载的同一个类文件获得的类,JVM将看成是两个不一样类,使用单例模式,强制类型转换时均可能由于这个缘由出问题。
图3 类加载器双亲委派模型
类加载器双亲委派模型加载顺序:java的三种类加载器存在父子关系,子 加载器保存着附加在其的引用,当一个类加载器须要加载一个目标类时,会先委托父加载器去加载,而后父加载器会在本身的加载路径中搜索目标类,父加载器在本身的加载范围中找不到时,才会交给子加载器加载目标类。
采用双亲委托模式能够避免类加载混乱,并且还将类分层次了,例如java中lang包下的类在jvm启动时就被启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppClassLoader)加载,基于双亲委托模式,就算用户定义了与lang包中同样的类,最终仍是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,因此二者都不会再从新加载。固然,若是使用者经过自定义的类加载器能够强行打破这种双亲委托模型,但也不会成功的,java安全管理器抛出将会抛出java.lang.SecurityException异常。
//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();
其中:
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表示不进行连接,由上面介绍能够,
不进行连接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会获得执行
一般咱们推荐采用第一种方法自定义类加载器,最大程度上的遵照双亲委派模型。 自定义类加载的目的是想要手动控制类的加载,那除了经过自定义的类加载器来手动加载类这种方式,还有其余的方式么?
利用现成的类加载器进行加载:
[1] classloader加载class文件的原理和机制
[2] java 类加载器双亲委派模型