1. 概述

在Java中的数据类型分为基本数据类型和引用数据类型;基本数据类型由虚拟机预先定义,引用数据类型需要进行类的加载;

按照Java虚拟机规范,从class文件到加载到内存中的类,再到类卸载出内存,它的整个生命周期包含七个阶段:Loading - Verification - Preparation - Resolution - Initialization - Using - Unloading

2. 加载过程Loading

- 加载的理解:

加载即是指将Java类的字节码文件加载到机器内存中,并且在内存中构建Java类的原型——类模板对象;所谓类模板对象,其实是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息保存到类模板中,以便在运行期可以通过类模板获取Java类的任意信息,遍历Java类中的成员变量,调用Java类中的方法;

- 加载完成的操作:

在加载类时,Java虚拟机会完成三件事情:

1). 通过类的全路径名,获取类的二进制数据流;

2). 将类的二进制数据流解析为方法区中的数据结构;

3). 在堆内存空间中创建java.lang.Class类的实例,作为方法区中该类的数据访问入口;

- 二进制流的获取方式

1). 虚拟机通过文件系统读入class后缀的文件;

2). 读入zip、jar等归档数据包,提取类文件;

3). 事先存放在数据库中的类的二进制数据;

4). 使用HTTP之类的协议通过网络进行加载;

5). 在运行时生成一段Class的二进制信息;

- 数组类的加载

数组类不是由类加载器创建的,而是由JVM在运行时根据需要直接创建的,但是数组的元素类型仍然需要依靠类加载器去创建;创建数组类的过程:

1). 如果数组中元素的类型为引用类型,则首先由类加载器加载数组中元素的类型;

2). JVM使用指定的元素类型和数组维度来创建新的数组类;

3. 链接过程Linking

- Verification

验证阶段的目的是保证加载的字节码合法、合理、符合规范;

验证阶段会进行四步检查:格式检查、语义检查、字节码验证、符号引用验证;

格式检查是在加载阶段进行的,只有格式检查通过,才会将类的二进制数据信息加载到方法区中;格式检查之外的其他检查操作将在方法区中进行;

符号引用验证:Class文件在其常量池会通过字符串记录自己将要使用的类或者方法,因此在验证阶段,虚拟机便会检查这些类或者方法是否存在,并且当前类是否有权限访问这些数据,如果一个类在系统中无法找到,则会抛出NoClassDefFoundError,如果一个方法无法找到,则会抛出NoSuchMethodError;

- Preparation

准备阶段的目的是为静态变量分配内存,并且初始化为默认值;

类型 默认初始值
byte 0
short 0
int 0
long 0L
char \u0000
float 0.0f
double 0.0
boolean false

这里不包含基本数据类型用static final修饰的情况,因为final在编译的时候就会分配,在准备阶段会进行显式初始化赋值;

这里不会为实例变量分配内存及初始化,因为类变量分配在方法区中,而实例变量会随着对象一起分配到Java堆中;

在该阶段不会有初始化的代码执行;

- Resolution

解析阶段的目的是将类、接口、字段和方法的符号引用转化为直接引用;

Java虚拟机为每个类都准备了一张虚方法表,将类的所有方法都记录在表中,当需要调用一个类的方法时候,只要知道方法在方法表中的偏移量就可以直接调用该方法。

4. 初始化阶段Initialization

初始化阶段的主要工作是为类的静态变量赋予正确的初始值;

从该阶段开始,类开始执行Java字节码,主要执行类的初始化方法:<clinit>方法;

<clinit>方法的特点:

1). 方法仅能由Java编译器生成并由JVM调用,程序开发者无法定义一个同名方法,更无法在程序中调用该方法;

2). 方法由类静态成员的赋值语句和static代码块合并构成;  

在加载一个类之前,虚拟机会优先加载其父类,因此父类的<clinit>方法会在子类的<clinit>方法之前调用。

不会出现<clinit>方法的情况:

1). 一个类中并没有声明任何的类变量,也没有静态代码块时;

2). 一个类声明类变量,但没有明确使用类变量的初始化语句以及静态代码块语句执行初始化操作;

3). 一个类中仅包含static final修饰的基本数据类型的字段,这些字段的初始化在编译时完成;

- static与final搭配问题

使用static final修饰的,并且进行显式赋值的,还不涉及到方法或者构造器调用的基本数据类型或者String类型的字面量形式,将在Linking阶段的Preparation阶段进行赋值,其他方式进行赋值的静态常量或静态变量都将在初始化的<clinit>方法中进行显式赋值;在准备阶段显式赋值的,编译的Fields字段内会包含属性ConstantValue;

- <clinit>的线程安全性

如果有多个线程同时去初始化一个类,则只会有一个线程去执行该类的<clinit>方法,其他线程都将会阻塞等待,直到活动线程的<clinit>方法执行完毕为止;但是如果在一个类的<clinit>方法中有耗时很长的操作,则可能造成多个线程阻塞,引发死锁;

- 类的初始化情况:主动使用 VS 被动使用

主动使用的情况:

1). 创建一个类的实例,如使用new关键字、克隆、反射、反序列化等;

2). 当调用类的静态方法,如使用字节码的invokestatic指令;

3). 当使用类、接口的静态字段时,如使用字节码的getstatic和putstatic指令;

4). 当使用java.lang.reflect包中方法反射类的方法时,如Class.forName();

5). 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;

6). 如果一个接口中定义了default方法,则该接口的直接或间接实现类在初始化之前,接口会先进行初始化;

7). 当虚拟机启动时,虚拟机会优先初始化main()方法所在的主类;

补充说明:

当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则不适用于接口;

当初始化一个类时,并不会先初始化它实现的接口;

当初始化一个接口时,也不会先初始化它的父接口;

被动使用的情况:

除以上的情况外,其他的情况均属于被动使用,被动使用不会引起类的初始化;

1). 当访问一个静态字段时,只有声明了该静态字段的类才会被初始化;

2). 通过数组定义的类引用,不会触发该类的初始化;

3). 引用常量不会触发类或接口的初始化,因为常量在链接阶段就已经被显式赋值;

4). 调用classLoader的loadClass()方法加载一个类时,并不是对类的主动使用,不会导致类的初始化;

5. 卸载阶段Unloading

- 类、类加载器、类的实例之间的关系

每个类加载器内部都通过Java集合保存自己加载的所有类的引用;此外,每个Class对象都会保存加载自己的类的加载器的引用,调用class对象的getClassLoader()方法,则可以获取它的类加载器;由此可见,Class实例与类加载器之间是双向关联关系;

每个类的实例可以通过getClass()方法获取到该类的Class对象;

-类的生命周期

当类被加载、链接和初始化后,类的声明周期则开始了;当代表类的Class对象不再被引用,即不可触及时,Class对象会结束生命周期,类在方法区内的数据也会被卸载,从而结束类的生命周期;

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期;

-类的卸载

1). BootstrapClassLoader加载的类型在整个运行期间是不可能被卸载的;

2). ExtentionClassLoader和APPClassLoader加载的类型在运行期间不太可能被卸载,因为系统类和扩展类加载器的实例在整个运行期间总能被直接或间接的访问到,其达到unreachable的可能性极小;

3). 用户自定义的类加载器只有在上下文环境很简单的时候才可以被卸载,而且还需要借助于强制调用虚拟机的垃圾收集功能才可以做到;

- 方法区的垃圾回收

方法区的垃圾收集主要回收两部分的内容:常量池中废弃的常量和不在使用的类型;

废弃的常量:只要常量池中的常量没有在任何地方被引用,便可以回收;

不在使用的类型:回收条件十分苛刻:1). 类的所有实例都已经被回收,即Java堆中不存在该类及其任何派生子类的实例;2). 加载该类的类加载已经被收回;3). 该类对应的java.lang.Class对象没有在任何地方被引用,也无法在任何地方通过反射访问该类的方法;

 

posted on 2023-01-09 10:21  VaeSSAQ  阅读(4)  评论(0)    收藏  举报