Java内存管理-掌握虚拟机类加载机制(四)

勿在流沙筑高台,出来混早晚要还的。java

作一个积极的人程序员

编码、改bug、提高本身数据库

我有一个乐园,面向编程,春暖花开!编程

上一篇介绍了整个JVM运行时的区域,以及简单对比了JDK7和JDK8中JVM运行时区域的一些变化,也顺便总结了哪些区域会发生异常(内存溢出)问题。前一篇的话仍是很是重要,请你们务必要多多阅读学习和掌握,由于这些基础的知识点会关联后续的一系列问题内容,若是前面没有先有必定的基础知识储备,到后面的一些篇章介绍你可能会蒙B的,可能会有一种what the fuck的感受,这TMD到底在说什么。因此墙裂建议先好好阅读前面的博文。安全

本章介绍JVM中类加载的机制,经过类加载的机制的学习咱们能够知道类加载的整个流程是什么,让咱们知其然也能知其因此然。网络

知识地图:数据结构

本文地图

1、思考:简单示例

下面代码是两个简单的示例,请先思考30秒,最初回答,输出的结果究竟是什么?多线程

/** * 示例1 */ class StaticLoad { private static StaticLoad staticLoad = new StaticLoad(); public static int count1; public static int count2 = 0; private StaticLoad() { count1++; count2++; } public static StaticLoad getStaticLoadInstance(){ return staticLoad; } } public class TestStaticLoadDemo { public static void main(String[] args) { StaticLoad staticLoad = StaticLoad.getStaticLoadInstance(); System.out.println("count1 = " + staticLoad.count1); System.out.println("count2 = " + staticLoad.count2); } } 

示例1打印结果:布局

  • A :1 和 0学习

  • B :1 和 1

/** * 示例2 */ class StaticLoad { public static int count1; public static int count2 = 0; private static StaticLoad staticLoad = new StaticLoad(); private StaticLoad() { count1++; count2++; } public static StaticLoad getStaticLoadInstance(){ return staticLoad; } } public class TestStaticLoadDemo { public static void main(String[] args) { StaticLoad staticLoad = StaticLoad.getStaticLoadInstance(); System.out.println("count1 = " + staticLoad.count1); System.out.println("count2 = " + staticLoad.count2); } } 

示例2打印结果:

  • A :1 和 0

  • B :1 和 1

两个例子惟一的区别下面这行代码的顺序!

private static StaticLoad staticLoad = new StaticLoad(); 

若是你可以选择出正确结果,并彻底知道答案。那今天这一篇文章就不用看了,若是你在两个答案之间犹豫,那么请继续往下看,好好阅读完本篇,我相信你会有答案的。

2、类加载的过程

在来简单回顾一下JVM运行流程, java源文件程序 使用 javac 进行编译 ,编译字节码 class文件!

JVM 在指定位置读取class文件而后加载到内存中(字节码解析成二进制的代码、指令)。

JVM运行流程

JVM基本结构:

类加载器、执行引擎、运行时数据区、本地接口。

Class FIle ---> ClassLoader ---> 运行时数据区---->执行引擎,须要调用本地库接口--->本地方法库。

本文主要是在ClassLoader 这一个点作作介绍,慢慢的咱们会把这一整套都串联起来。

思考:类加载机制是什么?

JVM把编译好的class文件加载的内存,并对数据进行校验、转换解析和初始化,最终造成JVM能够直接使用的Java类型的过程就是加载机制。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了七个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

其中验证、准备、解析三个部分统称连接!本篇也只会介绍到初始化,后面的周期在后面文章在作介绍。

类的生命周期

加载、验证、准备、初始化和卸载这五个阶段顺序是肯定的,类的加载过程必须按照这种顺序来进行,而解析阶段不必定;它在某些状况下能够在初始化以后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段一般都是互相交叉的混合式进行的,一般会在一个阶段执行的过程当中调用或激活另一个阶段。

一、加载阶段

什么状况下须要开始类加载的第一个阶段:加载。 JAVA虚拟机规范并无进行强制约束,交给虚拟机的具体实现自由把握。

加载阶段是“类加载”过程当中的一个阶段,这个阶段一般也被称做“装载”,在加载阶段,虚拟机主要完成如下3件事情:

1.经过“类全名”来获取定义此类的二进制字节流

2.将字节流所表明的静态存储结构转换为方法区的运行时数据结构

3.在java堆中生成一个表明这个类的java.lang.Class对象,做为方法区这些数据的访问入口(因此咱们可以经过低调用类.getClass() )

注:若是不理解,建议先背下来,记住!

虚拟机规范的这3点要求其实并不规范,好比:经过“类全名”来获取定义此类的二进制字节流”并无指明二进制流必需要从一个本地class文件中获取,准确地说是根本没有指明要从哪里获取及怎样获取(记住这个对后面咱们实现自定义类加载有帮助)。许多java技术也玩出其余花样:

  • 从Zip包中读取,这很常见,最终成为往后JAR、EAR、WAR格式的基础。

  • 从网络获取(URLClassLoader),下载.class文件

  • 运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用ProxyGenerator.generateProxyClass来为特定接口生成$Prxoy的代理类的二进制字节流。

  • 由Java源文件动态编译为.class,最经常使用方式!

  • 从数据库中读取.class文件,这种场景相对少见。

  • ……

相对于类加载过程的其余阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动做)是开发期可控性最强的阶段,由于加载阶段可使用系统提供的类加载器(ClassLoader)来完成,也能够由用户自定义的类加载器完成,开发人员能够经过定义本身的类加载器去控制字节流的获取方式。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。而后在java堆中实例化一个java.lang.Class类的对象,这个对象做为程序访问方法区中的这些类型数据的外部接口。加载阶段与连接阶段的部份内容(如一部分字节码文件格式验证动做)是交叉进行的,加载阶段还没有完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动做,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的前后顺序。

若是上面那么多记不住: 请必定记住这句: 加载阶段也就是查找获取类的二进制数据(磁盘或者网络)动做,将类的数据(Class的信息:类的定义或者结构)放入方法区 (内存)

一图说明:

加载阶段

二、链接阶段(验证、准备、解析)

只有二进制文件载入成功了,才能进行下面的阶段!

2.1 验证

验证就是字面意思,以前也提供JVM实际上是有一套本身的规范,因此加载到JVM中数据是须要进行验证的。

验证是连接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身安全。

验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

  • 1.文件格式验证

    验证class文件格式规范,例如: class文件是否已魔术0xCAFEBABE开头 , 主、次版本号是否在当前虚拟机处理范围以内等。

  • 2.元数据验证

这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object以外,全部的类都应当有父类)、这个类是否继承了不容许被继承的类(被final修饰的)、若是这个类的父类是抽象类,是否实现了起父类或接口中要求实现的全部方法。

  • 3.字节码验证

    进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会作出危害虚拟机安全的行为。如:保证访法体中的类型转换有效,例如能够把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体之外的字节码命令上。

  • 4.符号引用验证

符号引用中经过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

2.2 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点:

第一:这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在java堆中。

第二:这里所说的初始值“一般状况”下是数据类型的零值(默认值),假设一个类变量定义为:

public static int value = 123; 

首先为int类型的静态变量value分配4个字节的内存空间,并赋予变量value的初始值为0而不是123。由于这时候还没有开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,因此把value赋值为123的动做将在初始化阶段才会被执行。

基本数据类型的零值:

| 数据类型 | 零值 | 数据类型 | 零值 | | -------- | -------- | --------- | ----- | | int | 0 | boolean | false | | long | 0L | float | 0.0f | | short | (short)0 | double | 0.0d | | char | '\u0000' | reference | null | | byte | (byte)0 | | |

上面所说的“一般状况”下初始值是零值,那相对于一些特殊的状况,若是类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设上面类变量value定义为:

public static final int value = 123; // 注意 final 

编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

上面说了这么一长串意思是: 若是一个被static 修饰的变量加了final,则在准备阶段就会赋值为设置的值了,不然只是设置为零值(也能够认为默认值)。

2.3 解析

解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。

符号引用:符号引用是一组符号来描述所引用的目标对象,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不必定已经加载到内存中。Java虚拟机明确在Class文件格式中定义的符号引用的字面量形式。

直接引用直接引用能够是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同,若是有了直接引用,那引用的目标一定已经在内存中存在。

这里重点理解加粗的两个名称,个人理解是由虚指变为实指,举个不是很恰当的例子,方便理解:

玩斗地主: 每一局输的人手里的一张牌表明 一块钱,此时一张牌虚指(符号引用)一块钱。
等一局游戏结束,将牌兑换为钱(直接引用)的时候,那就是实指了。

在解析的阶段,解析动做主要针对7类符号引用进行,它们的名称以及对于常量池中的常量类型和解析报错信息以下:

| 解析动做 | 符号引用 | 解析可能的报错 | | ---------- | ------------------------------- | ----------------------------------------------------------- | | 类或接口 | CONSTANTClassInfo | java.land.IllegalAccessError | | 字段 | CONSTANTFieldrefInfo | java.land.IllegalAccessError 或 java.land.NoSuchFieldError | | 类方法 | CONSTANTMethodefInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError | | 接口方法 | CONSTANTInterfaceMethoderInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError | | 方法类型 | CONSTANTMethodTypeInfo | | | 方法句柄 | CONSTANTMethodhandlerInfo | | | 调用限定符 | CONSTANTInvokeDynamicInfo | |

解析的整个阶段在虚拟机中仍是比较复杂的,远比上面介绍的复杂的多,可是不少特别细节的东西咱们能够暂时先忽略,先有个大概的认识和了解以后有时间在慢慢深刻了。

小总结:

验证:确保被加载的类的正确性

准备:为 类的 静态变量 分配内存,并将其初始化为默认值

解析:把类中的符号引用转换为直接引用

三、初始化阶段

类初始阶段是类加载过程的最后一步,在上面提到的类加载过程当中,除了加载阶段用户应用程序能够经过自定义类加载器参与以外,其他的动做所有由虚拟机主导和控制。初始化阶段,是真正开始执行类中定义的Java程序代码(或者说是字节码)

在准备阶段,变量已经赋值过一次系统要求的初始值(零值),而在初始化阶段,则根据程序员经过程序制定的主观计划去初始化类变量和其余资源。(或者从另外一个角度表达:初始化阶段是执行类构造器<clinit>()方法的过程。)


tips:

类构造器 和 构造方法有什么关系?

类构造器:构造class对象,类对象;构造方法:实例化对象!先要执行类构造器才能执行构造方法!也就是说先要有这个类,才能对类进行实例化。

在类构造器中构造器中先执行static变量,在执行static{}块,有多个static变量的话按照代码顺序执行。,以下图例子,顺序不对,编译都不能经过!

执行顺序


在初始化阶段,虚拟机规范则是严格规定有且只有5种状况必须当即对类进行“初始化”(而加载、验证、准备、解析要在此以前执行),5种状况分别是:

第一:遇到newgetstaticputstaticinvokestatic这4条字节码指令时。若是类没有进行过初始化,则须要先触发其初始化。生成这4条指令的最多见的Java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。

备注:静态属性和静态方法,对应的指令为getstaticputstaticinvokestatic。可能你对这些字节码指令有点蒙B,没有关系,可暂时忽略,记住一个new就行。

第二:使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。

第三:当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。

第四:当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类。

第五:当使用JDK1.7的动态语言支持时,若是一个java.invoke.MethodHandle 实例最后解析结果REFgetStatic、REFputStatic、REF_invokeStatic 的方法句柄。而且这个方法句柄所对应的类没有初始化,则须要先触发其初始化。

<clinit>()方法相关的内容比较多,只须要记住一点:虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步,若是多个线程同时去初始化一个类,那么只会有一个线程执行这个类的<clinit>()方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。(一个类在虚拟机中只会被加载一次,是什么机制保证只能被加载一次,后面文章进行讲解!)

3、分析示例和简单总结

上面的内容所有看完以后,我想你应该就知道最开始的简单示例的答案了。

示例1答案就是: A

示例2答案就是: B

示例1具体分析:首先指定一个要执行的主类(包含main()方法)也就是TestStaticLoadDemo,执行main()方法运行StaticLoad.getStaticLoadInstance(),调用StaticLoad类的静态方法的时候,开始加载StaticLoad

第一步:给全部静态变量分配内存,并赋予零值。以下

public class TestStaticLoadDemo { public static void main(String[] args) { StaticLoad staticLoad = StaticLoad.getStaticLoadInstance(); // ① System.out.println("count1 = " + staticLoad.count1); System.out.println("count2 = " + staticLoad.count2); } } // ② public static StaticLoad getStaticLoadInstance(){ return staticLoad; } // ③ private static StaticLoad staticLoad = null; public static int count1 = 0; public static int count2 = 0; // count2 = 0 并非代码中的count2 = 0 的含义,是赋予的默认零值! 

第二步:赋值完进行初始化,把右边的值赋左边,static执行顺序从上到下,以下

private static StaticLoad staticLoad = new StaticLoad();// ① public static int count1; // ⑤ public static int count2 = 0; //⑥ private StaticLoad() { // ② count1++; //③ count2++; //④ } 

第三步:赋值完整,打印结果

public class TestStaticLoadDemo { public static void main(String[] args) { StaticLoad staticLoad = StaticLoad.getStaticLoadInstance(); System.out.println("count1 = " + staticLoad.count1); // ① System.out.println("count2 = " + staticLoad.count2); // ② } } 

实例2能够按照上面的分析过程自行进行分析,这里就不在分析了。

最后在总结一下本文主要讲解的类的生命周期中的三个阶段:加载,链接(验证、准备、解析)、初始化。

参考资料

《深刻理解Java虚拟机》

推荐阅读

Java的线程安全、单例模式、JVM内存结构等知识梳理
Java内存管理-程序运行过程(一)
Java内存管理-初始JVM和JVM启动流程(二)
Java内存管理-JVM内存模型以及JDK7和JDK内存模型对比总结(三)


谢谢你的阅读,若是您以为这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你天天开心愉快!


 

无论作什么,只要坚持下去就会看到不同!在路上,不卑不亢!

博客首页 : http://blog.csdn.net/u010648555

相关文章
相关标签/搜索