一、类的生命周期
类加载过程包含:加载、验证、准备、解析和初始化 ,一共包括 5****个阶段。
(1)加载:
简单来说就是将java类的字节码文件加载到机器内存中。在加载类时,Java虚拟机必须完成以下3件事情:
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为
Metaspace
元空间区的运行时存储结构。 - 在内存中生成一个代表该类的
Class
对象,作为元空间区中该类各种数据的访问入口。
(2)验证:
确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(3)准备:
为类的静态分配内存,并将其初始化为默认值当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。Java虚拟机为各类型变量默认的初始值如表所示。
|---------------|-------------|
| 类型 | 默认初始值 |
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0 |
| char | \u0000 |
| boolean | false |
| reference | null |
没有对static fianl修饰的变量进行赋值,因为final修饰的变量已经在编译阶段就进行了赋默认值,而在准备阶段进行的是显式赋值
(4)解析:
将常量池的符号引用替换为直接引用的过程 。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java
的动态绑定。
符号引用就是一些自变量的引用,和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在Class类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下println()方法被调用时,系统需要明确知道该方法的位置。
(5)初始化:
初始化阶段才真正开始执行类中定义的 Java
程序代码。初始化阶段是虚拟机执行类构造器 <clinit>()方法的过程。
对于<clinit>()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
正是因为函数<clinit>()带,因此,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行<clinit>()方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。
二、类加载的主动引用和被动引用
(1)主动引用(会发生初始化)
- 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 加载一个类,如果其父类还未加载,则先触发该父类的加载。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含
main()
方法的类),虚拟机会先加载这个类。 - 当一个接口中定义了
JDK8
新加入的默认方法(被default
关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
(2)被动引用(看上去会,其实不会发生初始化):
- 通过子类引用父类的静态字段,不会导致子类初始化
- 通过数组定义类引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
三、类加载器
类加载器是Java虚拟机中的一个子系统,用于将类的字节码文件加载到内存中,并转换为Java的类对象。Java虚拟机需要根据类的全名来定位并加载类的字节码文件,而类加载器负责从文件系统、网络等位置查找类文件,并加载到Java虚拟机中。
Java虚拟机中有三种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器。
(1)Bootstrap ClassLoader:启动加载器
它负责加载Java的核心类(如String、System等)。它比较特殊,因为它是由原生C++代码实现的,并不是java.lang.ClassLoader的子类。启动类加载器无法被 Java 程序直接引用,所以下面的运行结果为null:
(2)Extension ClassLoader:扩展类加载器
它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext)中JAR包的类,我们可以通过把自己开发的类打包成JAR文件放入扩展目录来为Java扩展核心类以外的新功能。
(3)System ClassLoader(或Application ClassLoader):应用程序(系统)类加载器
它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader来获取系统类加载器:
四. 对象的创建过程
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查 通过后,接下来虚拟机将为新生对象分配内存 。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java
堆中划分出来。内存 分配的查找方式 有 "指针碰撞" 和 "空闲列表" 两种。
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java
代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置 ,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 构造方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java
程序的视角来看,对象创建才刚开始,<init>
构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new
指令之后会接着执行 <init>
构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。