1. 虚拟机的启动
Java虚拟机的启动是通过引导类加载器(Bootstrap Class Loader)创建一个初始类(Initial Class)来完成的,这个是由虚拟机的具体实现指定的。
2. 虚拟机的执行
- 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。
- 程序开始执行时他才运行,程序结束时,他就停止运行。
- 执行一个所谓的Java程序时,电脑是真真正正地在执行一个
Java虚拟机进程。
我们在执行一个简单的程序Main方法程序的时候,实际上会加载非常多的类。当我们在代码中加入挂起代码,然后在挂起期间采用JPS指令查看JVM当前所在运行的进程号(pid)如下:
Main类独立地占用了一个进程号,而其他的辅助运行类也在运行;当程序执行完成后,自然而然地Main方法消失了
3. 线程
在HotSpot虚拟机中,每个线程都与操作系统的本地线程直接映射。当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建,Java线程执行终止后,本地线程也会回收。操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功之后,他就会调用Java线程中的run()方法。线程又区分为守护线程和非守护线程,如果程序中剩下的只有守护线程,那么虚拟机也会退出。
一个Java程序背后其实有很多的线程:
虚拟机线程:这种线程的操作时需要JVM到达安全点才会出现,这些操作必须在不同的线程中发生的原因是:他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行的任务包括“STOP-THE-WORLD”,即让所有线程都终止的垃圾收集、线程栈收集、线程挂起、偏向锁撤销。周期任务线程:这种线程是时间周期的体现,比如中断等等,他们一般用于周期性操作的调度执行。GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。编译线程:这种线程在运行时会将字节码编译成本地代码。信号调度线程:这种线程接受信号发送给JVM,在它内部通过调用适当的的方法进行处理。
- JVM安全点:在虚拟机在进行
可达性分析时,HotSpot虚拟机会在特定的位置记录在哪有引用,这些特定的位置就叫做安全点。这是GC方面的知识,之后会做解析。
4. 类加载过程
我们知道,JVM读入Class文件进行加载。一般的读入方式都是存在于本地的磁盘中,但是实际上,类加载支持:本地、网络、JAR包、甚至是运行时计算生成得到的Class文件。
4.1 类加载的宏观过程
- .Class文件存放在本地磁盘上,可以理解为设计师在纸上画的模板,最终这个模板在执行的时候是要加载到JVM当中来的,根据该模板,JVM可以实例化出N个一模一样的实例。
- .Class文件加载到JVM中,被称为DNA元数据模板,放在方法区。
- 在.Class文件 -> JVM -> 最终成为元数据模板,这个过程就需要一个
运输工具(Class Loader),扮演一个快递员的操作。而这个运输工具就是我们今天的主角:类加载子系统。
4.2 类加载的详细过程
4.2.1. 加载阶段(狭义上的加载)
加载阶段主要的任务就是,读入Class文件,构建静态存储结构,在内存中生成元数据模板。详细过程如下:
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将字节流 所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载的几个来源:
- 本地系统
- 网络
- JAR、压缩包等等
- 运行时计算生成,例如:动态代理技术
- 由其他文件生成
- 从专有的数据库中提取.Class文件
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
4.2.2. 链接阶段
链接阶段主要是验证Class文件的有效性、准确性和为类中的信息初始化变量值、执行静态方法。链接阶段又划分为三个小的阶段:
4.2.2.1. 验证阶段
验证的目的在于确保Class文件的字节流中包含信息符合当前的虚拟机要求,保证被加载类的正确性,保证虚拟机自身的安全。包括:
- 文件格式验证:能够被Java虚拟机识别的文件二进制头的十六进制表示均是
CA FE BA BE(Cafe babe)。 - 元数据验证
- 字节码验证
- 符号引用验证
何为元数据?【百度百科】
元数据(Metadata),为描述数据的数据,主要是描述数据属性的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。- 元数据最大的好处是,它使信息的描述和分类可以实现格式化,从而为机器处理创造了可能。 其他的内容可以看看这篇博客:元数据(MetaData)
4.2.2.2. 准备阶段
为类变量分配内存并且设置该类变量的默认初始值。即零值。有如下代码:
class Test{
public static int a = 1;
}
在准备阶段,此时的类变量a的值仅仅是0,而不是1。
- 这里不包括被final修饰的static变量,因为Final在编译阶段就会分配一个固定的值,编译期即把结果放入了常量池。在运行时被初始化,可以直接将这个固定死的值赋值给它。赋值后不可修改,但是常量池中只能引用到基本数据类型+String。
- 这里也不会为实例变量分配初始化。因为类变量会分配在方法区中,而实例变量则是对象,会和其他对象一样分配到Java区中。(这里不是说类变量不是对象,而是说类变量本身是一种特殊的对象。他被分配在方法区中,而不是和其他new出来的对象一样分配在堆中。)
其实,准备阶段的操作可以简单地理解为:只构建一个最为简单的类,除非我们定下了写死的final staitc且是(8+1)的基本数据类型以外,都是赋默认零值,引用类型为null。
4.2.2.3. 解析阶段
- 将常量池内的符号引用转换为直接引用。
- 符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的
指针、相对偏移量或一个间接定位到目标的句柄。 - 解析动作主要针对类或者是接口、字段、类方法、接口类型、方法类型等等,对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等.
4.2.3. 初始化阶段
作为第二个大的阶段链接阶段,仅仅是完成了类的载入和数据类型的初始化,除了部分常量值,其他的类对象、实例对象都没有被正确赋值。而第三个阶段初始化阶段则是完成遗留下的问题的。
- 初始化阶段执行的是:类构造器方法
<Clinit>()方法,注意,构造器方法和构造方法不同。 - 构造器方法中的指令按照语句在源文件中出现的顺序执行。
<Clinit>()方法不同于类的构造器:构造器是虚拟机视角下的<init>()。- 若该类已有父类,那么会保证子类的
<Clinit>()方法执行前,父类的<Clinit>()方法语句执行完成。 - 虚拟机必须保证一个类的
<Clinit>()方法在多线程的情况下被同步加锁,一个类的静态代码块的初始化只能执行一次。这也是我们静态内部类实现线程安全的懒汉式单例的重要理论基础。
单例模式
分为饿汉式和懒汉式,饿汉式天生线程安全,但是做不到懒加载。而懒汉式能够实现线程安全,但是需要加锁处理,否则多线程可能会创建不同的实例。目前的几种实现线程安全的懒汉式单例方式有常见的加锁处理。而静态内部类的特性可以使得它天然地实现懒汉式的单例模式。 首先是线程安全,在上文的3.5中,我们知道,一个类的静态代码块的初始化会被JVM加锁,这样一来,我们就不需要手动加锁了。 其次是懒加载,只有我们调用到的时候,类加载器才会为我们加载静态内部类,否则是不会加载的,这样一来,我们就实现了线程安全的懒汉式单例模式。
2.构造器、构造函数、
<init> 实例构造器和<Clinit>()类构造器
构造函数:也叫构造方法,就是我们写代码里面new一个类的构造方法。构造器:Javac编译,生成的一个函数,是在字节码层面存在的“函数”。它其实对一些代码的整合后生成的函数。<init> 实例构造器:针对的是实例构造。- ·
<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)。
类加载器划分成三种:
BootStrap ClassLoader:这是由C/C++编写的类架子啊器,嵌套在JVM内部。用来加载Java的核心类库,用于加载:JAVA_HOME/jre/lib/rt.jar、resources.jar或者是sun.boot.class.path目录下的文件。
BootStrapClassLoader没有父加载器,但是他是扩展类加载器的父加载器。BootStrapClassLoader只能加载包名为java、javax、sun开头的类。
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对类的使用分成主动使用和被动使用。其中主动使用又细分为七种情况:
-
创建类的实例
-
访问某个类或者接口的静态变量,或者对该静态变量赋值。
-
调用类的静态方法
-
反射
-
初始化一个类的子类
-
Java虚拟机启动时被标明为启动类的类
-
JDK7开始提供的动态语言支持
Java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化则初始化。
除了以上的七种情况,其他的调用都被视作是对类的被动调用,都不会导致类的初始化。
如果同一个类被不同的类加载器所加载,那么这两个类是不同的类。
8. 总结
.Class文件会被类加载子系统读入JVM中的方法区当中,在方法区中,存放了最基本的类信息,而类加载子系统运行的各个阶段会为我们类赋值、调用静态方法等等。如图中的红线,就是类加载的主要过程。
参考来源
- 《深入理解Java虚拟机-JVM高级特性与最佳实践》 - 周志明著
- 你知道Java中final和static修饰的变量是在什么时候赋值的吗?
- java枚举类是怎么初始化的,为什么说枚举类是线程安全的