JVM 学习笔记(三)- 从虚拟机的启动-类加载开始

1. 虚拟机的启动

Java虚拟机的启动是经过引导类加载器(Bootstrap Class Loader)建立一个初始类(Initial Class)来完成的,这个是由虚拟机的具体实现指定的。html

2. 虚拟机的执行

  1. 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
  2. 程序开始执行时他才运行,程序结束时,他就中止运行。
  3. 执行一个所谓的Java程序时,电脑是真真正正地在执行一个Java虚拟机进程

咱们在执行一个简单的程序Main方法程序的时候,实际上会加载很是多的类。当咱们在代码中加入挂起代码,而后在挂起期间采用JPS指令查看JVM当前所在运行的进程号(pid)以下: Main类独立地占用了一个进程号,而其余的辅助运行类也在运行;当程序执行完成后,天然而然地Main方法消失了 java

3. 线程

在HotSpot虚拟机中,每一个线程都与操做系统的本地线程直接映射。当一个Java线程准备好执行之后,此时一个操做系统的本地线程也同时建立,Java线程执行终止后,本地线程也会回收。操做系统负责全部线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功以后,他就会调用Java线程中的run()方法。线程又区分为守护线程非守护线程若是程序中剩下的只有守护线程,那么虚拟机也会退出数据库

一个Java程序背后其实有不少的线程:安全

  • 虚拟机线程:这种线程的操做时须要JVM到达安全点才会出现,这些操做必须在不一样的线程中发生的缘由是:他们都须要JVM达到安全点,这样堆才不会变化。这种线程的执行的任务包括“STOP-THE-WORLD”,即让全部线程都终止的垃圾收集、线程栈收集、线程挂起、偏向锁撤销。
  • 周期任务线程:这种线程是时间周期的体现,好比中断等等,他们通常用于周期性操做的调度执行。
  • GC线程:这种线程对在JVM里不一样种类的垃圾收集行为提供了支持。
  • 编译线程:这种线程在运行时会将字节码编译成本地代码。
  • 信号调度线程:这种线程接受信号发送给JVM,在它内部经过调用适当的的方法进行处理。
  1. JVM安全点:在虚拟机在进行可达性分析时,HotSpot虚拟机会在特定的位置记录在哪有引用,这些特定的位置就叫作安全点。这是GC方面的知识,以后会作解析。

4. 类加载过程

咱们知道,JVM读入Class文件进行加载。通常的读入方式都是存在于本地的磁盘中,可是实际上,类加载支持:本地、网络、JAR包、甚至是运行时计算生成获得的Class文件。markdown

4.1 类加载的宏观过程

  1. .Class文件存放在本地磁盘上,能够理解为设计师在纸上画的模板,最终这个模板在执行的时候是要加载到JVM当中来的,根据该模板,JVM能够实例化出N个如出一辙的实例。
  2. .Class文件加载到JVM中,被称为DNA元数据模板,放在方法区。
  3. 在.Class文件 -> JVM -> 最终成为元数据模板,这个过程就须要一个运输工具(Class Loader),扮演一个快递员的操做。而这个运输工具就是咱们今天的主角:类加载子系统。

4.2 类加载的详细过程

4.2.1. 加载阶段(狭义上的加载)

加载阶段主要的任务就是,读入Class文件,构建静态存储结构,在内存中生成元数据模板。详细过程以下:网络

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

加载的几个来源:数据结构

  • 本地系统
  • 网络
  • JAR、压缩包等等
  • 运行时计算生成,例如:动态代理技术
  • 由其余文件生成
  • 从专有的数据库中提取.Class文件
  • 从加密文件中获取,典型的防Class文件被反编译的保护措施

4.2.2. 连接阶段

连接阶段主要是验证Class文件的有效性、准确性和为类中的信息初始化变量值、执行静态方法。连接阶段又划分为三个小的阶段:多线程

4.2.2.1. 验证阶段

验证的目的在于确保Class文件的字节流中包含信息符合当前的虚拟机要求,保证被加载类的正确性,保证虚拟机自身的安全。包括:函数

  • 文件格式验证:可以被Java虚拟机识别的文件二进制头的十六进制表示均是CA FE BA BE(Cafe babe)。
  • 元数据验证
  • 字节码验证
  • 符号引用验证

何为元数据?【百度百科】工具

  1. 元数据(Metadata),为描述数据的数据,主要是描述数据属性的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。
  2. 元数据最大的好处是,它使信息的描述和分类能够实现格式化,从而为机器处理创造了可能。

其余的内容能够看看这篇博客:元数据(MetaData)

4.2.2.2. 准备阶段

类变量分配内存而且设置该类变量的默认初始值。即零值。有以下代码:

class Test{
		public static int a = 1;
	}
复制代码

在准备阶段,此时的类变量a的值仅仅是0,而不是1。

  1. 这里不包括被final修饰的static变量,由于Final在编译阶段就会分配一个固定的值,编译期即把结果放入了常量池。在运行时被初始化,能够直接将这个固定死的值赋值给它。赋值后不可修改,可是常量池中只能引用到基本数据类型+String
  2. 这里也不会为实例变量分配初始化。由于类变量会分配在方法区中,而实例变量则是对象,会和其余对象同样分配到Java区中。(这里不是说类变量不是对象,而是说类变量自己是一种特殊的对象。他被分配在方法区中,而不是和其余new出来的对象同样分配在堆中。)

其实,准备阶段的操做能够简单地理解为:只构建一个最为简单的类,除非咱们定下了写死的final staitc且是(8+1)的基本数据类型之外,都是赋默认零值,引用类型为null。

4.2.2.3. 解析阶段
  1. 将常量池内的符号引用转换为直接引用。
  2. 符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明肯定义在《Java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针相对偏移量或一个间接定位到目标的句柄
  3. 解析动做主要针对类或者是接口、字段、类方法、接口类型、方法类型等等,对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等.

4.2.3. 初始化阶段

做为第二个大的阶段连接阶段,仅仅是完成了类的载入和数据类型的初始化,除了部分常量值,其余的类对象、实例对象都没有被正确赋值。而第三个阶段初始化阶段则是完成遗留下的问题的。

  1. 初始化阶段执行的是:类构造器方法<Clinit>()方法,注意,构造器方法和构造方法不一样。
  2. 构造器方法中的指令按照语句在源文件中出现的顺序执行。
  3. <Clinit>()方法不一样于类的构造器:构造器是虚拟机视角下的<init>()
  4. 若该类已有父类,那么会保证子类的<Clinit>()方法执行前,父类的<Clinit>()方法语句执行完成。
  5. 虚拟机必须保证一个类的<Clinit>()方法在多线程的状况下被同步加锁,一个类的静态代码块的初始化只能执行一次。这也是咱们静态内部类实现线程安全的懒汉式单例的重要理论基础。
  1. 单例模式

分为饿汉式和懒汉式,饿汉式天生线程安全,可是作不到懒加载。而懒汉式可以实现线程安全,可是须要加锁处理,不然多线程可能会建立不一样的实例。目前的几种实现线程安全的懒汉式单例方式有常见的加锁处理。而静态内部类的特性可使得它自然地实现懒汉式的单例模式。 首先是线程安全,在上文的3.5中,咱们知道,一个类的静态代码块的初始化会被JVM加锁,这样一来,咱们就不须要手动加锁了。 其次是懒加载,只有咱们调用到的时候,类加载器才会为咱们加载静态内部类,不然是不会加载的,这样一来,咱们就实现了线程安全的懒汉式单例模式。

2.构造器、构造函数、<init> 实例构造器<Clinit>()类构造器

  1. 构造函数:也叫构造方法,就是咱们写代码里面new一个类的构造方法。
  2. 构造器:Javac编译,生成的一个函数,是在字节码层面存在的“函数”。它其实对一些代码的整合后生成的函数。
  3. <init> 实例构造器:针对的是实例构造。
  4. ·<Clinit>()类构造器:cinit针对是类。

数量上来来说<init> 实例构造器至少存在一个, <Clinit>()类构造器构造器只存在一个. 由于类对象在JVM内存中只会存在一个(同一个类加载器)。 3. 枚举类。枚举类是一种比较特殊的类,它的底层实质上仍是Class,只不过是成员变量被public static final修饰的成员变量(经过类名调用),因此它是在static静态代码块中一块儿初始化的。因为java类的加载和初始化过程都是线程安全的,因此建立一个enum类型是线程安全的,因此用枚举类实现一个线程安全的单例是可行的。

4.3 静态实例变量的赋值变化过程:

####代码1:

pivate static int number = 1;
复制代码
序号 阶段
1 加载阶段 -
2 连接-验证 -
3 连接-准备 0
4 连接-解析 0
5 初始化阶段 10

代码2:

private final static int number = 10;
复制代码
序号 阶段
1 加载阶段 -
2 连接-验证 -
3 连接-准备 10
4 连接-解析 10
5 初始化阶段 10

注意,这里的各类虚拟机的实现各有不一样,例如HotSpot虚拟机在验证-准备阶段就已经赋初值了,可是JVM规范是要在初始化阶段才赋初值的。详细可见参考来源1

代码3:

private int number = 10;
复制代码

在具体的实例建立的时候才会赋初值。

##5 类加载器 前面,咱们介绍了一个类被加载进入JVM的不一样的阶段,然而具体执行加载过程的是咱们的类加载器(Class Loader)

类加载器划分红三种:

  1. BootStrap ClassLoader:这是由C/C++编写的类架子啊器,嵌套在JVM内部。用来加载Java的核心类库,用于加载:JAVA_HOME/jre/lib/rt.jar、resources.jar或者是sun.boot.class.path目录下的文件
  1. BootStrapClassLoader没有父加载器,可是他是扩展类加载器的父加载器。
  2. BootStrapClassLoader只能加载包名为java、javax、sun开头的类。
  1. Extension Classloader
    • i. 扩展类加载器:由Java语言编写,派生于BootStrapClassLoader。从java.ext.dirs系统属性所制定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(或者扩展目录)下加载类库。若是用户建立的JAR放在此目录下,也会由扩展类加载器进行加载。

    • ii. 应用程序类加载器(系统类加载器):由Java语言编写,派生于BootStrapClassLoader,负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库。该类是默认的类加载器。通常来讲,Java应用的类都是由它来完成加载。

    • iii. 用户自定义的类加载器。

6. 双亲委派机制

网路上关于双亲委派机制的讲解不少,通俗地说,受权委派机制就是当类加载的时候,咱们将任务"承包"给了类加载器,然而类加载器并不会本身去加载类,而是优先提交给其父类去加载,若是父类能加载,那么我本身就不加载,使用父类加载获得的类数据。

这样作的目的是为了保护类加载的来源,防止类的重复加载和核心API被恶意篡改致使的代码泄漏或者是功能缺失。

7. 主动使用和被动使用

JVM必须知道一个类型是由BootStrap ClassLoader仍是其余的ClassLoader加载的。

若是一个类型是用户类加载器加载的,那么JVM会将这个类加载器的一个引用做为类型信息的一部分保存在方法区中,当解析一个类型到另外一个类型的引用时,JVM须要保证两个类型的类加载器是相同的。

JVM对类的使用分红主动使用和被动使用。其中主动使用又细分为七种状况:

  1. 建立类的实例

  2. 访问某个类或者接口的静态变量,或者对该静态变量赋值。

  3. 调用类的静态方法

  4. 反射

  5. 初始化一个类的子类

  6. Java虚拟机启动时被标明为启动类的类

  7. JDK7开始提供的动态语言支持Java.lang.invoke.MethodHandle实例的解析结果REF_getStaticREF_putStaticREF_invokeStatic句柄对应的类没有初始化则初始化。

除了以上的七种状况,其余的调用都被视做是对类的被动调用,都不会致使类的初始化。

若是同一个类被不一样的类加载器所加载,那么这两个类是不一样的类。

8. 总结

.Class文件会被类加载子系统读入JVM中的方法区当中,在方法区中,存放了最基本的类信息,而类加载子系统运行的各个阶段会为咱们类赋值、调用静态方法等等。如图中的红线,就是类加载的主要过程。

参考来源
  1. 《深刻理解Java虚拟机-JVM高级特性与最佳实践》 - 周志明著
  2. 你知道Java中final和static修饰的变量是在何时赋值的吗?
  3. java枚举类是怎么初始化的,为何说枚举类是线程安全的
扩展阅读
  1. Java 类的热替换 —— 概念、设计与实现
相关文章
相关标签/搜索