JVM被分为三个主要的子系统:html
(1)类加载器子系统(2)运行时数据区(3)执行引擎java
(1)类加载子系统负责从文件系统或者网络中加载class文件,class文件在文件开有特定的文件标识(0xCAFEBABE)。数据库
(2)类加载器(Class Loader
)只负责class文件的加载,至于它是否能够运行,则由执行引擎(Execution Engine)决定。bootstrap
(3)加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。数组
(4)Class对象是存放在堆区的。安全
假若有一个Car.java
文件,编译后生成一个Car.class
字节码文件:bash
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为链接(Linking)。完整的流程图以下所示:网络
加载、验证、准备、初始化和卸载这五个阶段的顺序是肯定的。为了支持Java语言的运行时绑定,解析阶段也能够是在初始化以后进行的。(以上顺序流程指的是程序开始的顺序,在实际运行中,这些阶段一般都是互相交叉地混合进行的,会在一个阶段执行的过程当中调用、激活另外一个阶段)。数据结构
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程当中的一个阶段,JVM须要完成三件事:多线程
java.lang.Class
对象,做为方法区这个类的各类数据的访问入口。加载class文件的方式
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。
元数据验证
第二阶段是对字节码描述的信息进行语义分析。
字节码验证
经过数据流分析和控制流分析,肯定程序语义是合法的、符合逻辑的。
符号引用验证
对类自身之外(常量池中的各类符号引用)的各种信息进行匹配性校验,通俗来讲就是,该类是否缺乏或者被禁止访问它依赖的某些外部类、方法、字段等资源。
咱们能够经过安装IDEA的插件——jclasslib Bytecode viewer
,来查看咱们的Class文件:
安装完成后,咱们编译完一个class文件后,点击View--> Show Bytecode With Jclasslib
便可显示咱们安装的插件来查看字节码。
例以下面这段代码:
public class Hello { private static int a = 1; // 准备阶段为0,在下个阶段,也就是初始化的时候才是1。 public static void main(String[] args) { System.out.println(a); } }
初始化阶段就是执行类构造器法<clinit>()的过程。此方法不需定义,是javac
编译器自动收集类中的全部类变量的赋值动做和静态代码块(static{}块)中的语句合并而来,编译器收集的顺序是由语句在源文件中出现的顺序决定的。
<clinit>()
方法<clinit>
()不一样于类的构造器函数。(关联:构造器函数是虚拟机视角下的<init>
()方法。若该类具备父类,JVM会保证子类的<clinit>()
执行前,父类的<clinit>()
已经执行完毕。所以在Java虚拟机中第一个被执行的<clinit>()
方法的类型确定是java.lang.Object
。
public class ClassInitTest { private static int num = 1; static { num = 2; number = 20; System.out.println(num); System.out.println(number); //报错,非法的前向引用 } private static int number = 10; // prepare:number = 0--> number-->initial: 20-->10 public static void main(String[] args) { System.out.println(ClassInitTest.num); // 2 System.out.println(ClassInitTest.number); // 10 } }
关于涉及到父类时候的变量赋值过程:
public class ClinitTest { static class Father { public static int A = 1; static { A = 2; } } static class Son extends Father { public static int B = A; } public static void main(String[] args) { // 加载Father类,其次加载Son类 System.out.println(Son.B); } }
咱们输出结果为 2,也就是说首先加载ClinitTest的时候,会找到main方法,而后执行Son的初始化,可是Son继承了Father,所以还须要执行Father的初始化,同时将A赋值为2。咱们经过反编译获得Father的加载过程,首先咱们看到原来的值被赋值成1,而后又被复制成2,最后返回:
iconst_1 putstatic #2 <com/kai/jvm/ClinitTest1$Father.A> iconst_2 putstatic #2 <com/kai/jvm/ClinitTest1$Father.A> return
虚拟机必须保证一个类的<clinit>()
方法在多线程下被同步加锁。
public class DeadThreadTest { public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 线程t1开始"); new DeadThread(); }, "t1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 线程t2开始"); new DeadThread(); }, "t2").start(); } } class DeadThread { static { if (true) { System.out.println(Thread.currentThread().getName() + "\t 初始化当前类"); while(true) { } } } }
上面的代码,输出结果为:
线程t1开始 线程t2开始 线程t2 初始化当前类
从上面能够看出只可以执行一次初始化,其中一条线程一直在阻塞等待。
在类加载阶段中,实现“经过一个类的全限定名来获取描述该类的二进制字节流”这个动做的代码就被称为“类加载器”(ClassLoader)。
JVM支持两种类型的类加载器 ,分别为启动类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来说,自定义类加载器通常指的是程序中由开发人员自定义的一类类加载器,可是Java虚拟机规范却没有这么定义,而是将全部派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
不管类加载器的类型如何划分,在程序中咱们最多见的类加载器主要有3类,以下所示:
Tips:各种加载器之间的关系不是传统意义上的继承关系。
咱们经过一个类,获取不一样的加载器:
public class ClassLoaderTest { public static void main(String[] args) { // 获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); // 获取扩展类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); // 获取启动类加载器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); // 获取自定义加载器 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); // 获取String类型的加载器 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); } }
获得的结果,从结果能够看出启动类加载器没法经过代码直接获取,同时目前用户代码所使用的加载器为系统类加载器。同时咱们经过获取String类型的加载器,发现是null,这间接说明了String类型是经过启动类加载器进行加载的。(Java的核心类库都是使用启动类加载器进行加载的)
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@4554617c null sun.misc.Launcher$AppClassLoader@18b4aac2 null
咱们经过下面代码验证一下:
public class ClassLoaderTest { public static void main(String[] args) { System.out.println("*********启动类加载器************"); // 获取BootstrapClassLoader可以加载的API的路径 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) { System.out.println(url.toExternalForm()); } // 从上面路径中,随意选择一个类,来看看他的类加载器是什么:获得的是null,则说明是启动类加载器 ClassLoader classLoader = Provider.class.getClassLoader(); System.out.println(classLoader); } }
获得的结果(%20是空格):
*********启动类加载器************ file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/classes null
咱们经过下面代码验证一下:
public class ClassLoaderTest { public static void main(String[] args) { System.out.println("*********扩展类加载器************"); String extDirs = System.getProperty("java.ext.dirs"); for (String path : extDirs.split(";")) { System.out.println(path); } // Java\lib\ext目录下随意选择一个类,查看他的类加载器是什么 ClassLoader classLoader = CurveDB.class.getClassLoader(); System.out.println(classLoader); } }
获得的结果:
*********扩展类加载器************ C:\Program Files\Java\jdk1.8.0_151\jre\lib\ext C:\WINDOWS\Sun\Java\lib\ext sun.misc.Launcher$ExtClassLoader@7ea987ac
在Java的平常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,咱们还能够自定义类加载器,来定制类的加载方式。
为何要自定义类加载器?
用户自定义类加载器实现步骤:
ClassLoader类,它是一个抽象类,其后全部的类加载器都继承自ClassLoader(不包括启动类加载器)。
方法名称 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 |
findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 |
findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 链接指定的一个Java类 |
获取ClassLoader的途径:
clazz.getClassLoader()
Thread.currentThread().getContextClassLoader()
ClassLoader.getSystemClassLoader()
DriverManager.getCallerClassLoader()
Java虚拟机对class文件采用的是按需加载的方式,也就是说当须要使用该类时才会将它的class文件加载到内存生成class对象。并且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把优先将请求交由父类处理,它是一种任务委派模式。
下面用一个例子说明:
public class StringTest { public static void main(String[] args) { String string = new String(); System.out.println("Hello World!"); } }
而后自定义一个java.lang.String类:
public class String { static { System.out.println("这是自定义的String类的静态代码块!"); } }
执行结果:Hello World!
当咱们加载jdbc.jar 用于实现数据库链接的时候,首先咱们须要知道的是 jdbc.jar是基于SPI接口进行实现的,因此在加载的时候,会进行双亲委派,最终从启动类加载器中加载 SPI核心类。而后再加载SPI接口实现类,就进行反向委派,经过线程上下文类加载器进行实现jdbc.jar
的加载。
保护程序安全,防止核心API被随意篡改
Java安全模型的核心就是Java沙箱(sandbox)。沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,而且严格限制代码对本地系统资源访问,经过这样的措施来保证对代码的有效隔离,防止对本地系统形成破坏。
组成Java沙箱的基本组件以下:
Java安全模型的前三个部分——类加载体系结构、class文件检验器、Java虚拟机(及语言)的安全特性一块儿达到一个共同的目的:保持Java虚拟 机的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意代码或有漏洞的代码侵犯。相反,这个安全模型的第四个组成部分是安全管理器,它主要 用于保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯。这个安全管理器是一个单独的对象,在运行的Java虚拟机中,它在对于外部资源的访 问控制起中枢做用。
例如,自定义一个java.lang.String类,可是在加载自定义String类的时候会率先使用启动类加载器加载,而启动类加载器在加载的过程当中会先加载jdk自带的文件(rt.jar包java.lang.中javalangString.class),报错信息说没有main方法,就是由于加载的是rt.jar包中的string类。这样能够保证对java核心源代码的保护,这就是沙箱安全机制。
public class String { static { System.out.println("这是自定义的String类的静态代码块!"); } // 错误 public static void main(String[] args) { System.out.println("Hello World!"); } }
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
换句话说,在JVM中,即便这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不一样,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启动加载器加载的仍是由用户类加载器加载的。若是一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用做为类型信息的一部分保存在方法区中。当解析一个类型到另外一个类型的引用的时候,JVM须要保证这两个类型的类加载器是相同的。
Java程序对类的使用方式分为:主动使用和被动使用。
主动使用,又分为七种状况:
除了以上七种状况,其余使用Java类的方式都被看做是对类的被动使用,都不会致使类的初始化。