JAVA的编译运行过程分析

一个java程序从源文件到运行的整个过程可以分为两个大的阶段:
1.源代码由编译器编译为字节码文件(.class文件); 2. 字节码由JVM解释运行。

这里先搞清楚一个问题,编程语言分为编译型语言和解释型语言,这两种语言有何区别?java又属于哪一种?
首先讲两种语言类型的区别:

  1. 编译型语言是将源程序全部编译成二进制代码(机器语言),然后可以直接运行该程序。特点是速度快,效率高,依靠编译器,跨平台性差。 包含C、C++、pascal等都属于这一类型语言。
  2. 解释型语言是在运行时依赖解释器,将源代码解释一行然后执行一行,直至结束。特点是执行速度慢、效率低,依靠解释器,跨平台性好。 包含JavaScript、python、Ruby等都属于这一类型语言。

然后再讨论Java语言:

文章开篇说到,java源代码的整个运行过程中,先会被编译成字节码,然后在JVM中解释运行,所以Java属于编译-解释型语言(又做 半解释语言 semi-interpreted language)。 Java语言引入字节码这个文件是为了对其在发展初期”一处编译,到处运行“理念的最好诠释,也即实质上是由于JVM的跨平台性(Windows虚拟机、Linux虚拟机等等)才造就了java语言跨平台性的特点。

一、编译过程

JDK中javac编译器负责将.java文件编译成为.class文件,也就是我们所说的字节码文件。编译器的种类很多,但我们普通程序员对java程序的编译认知应属Javac编译器,且日常工作中常常使用的Maven的编译命令,其底层也是依赖于JDK中的Javac编译器实现对java源码的编译过程的。

从Java代码的总体结构来看,编译过程大致可以分为一个准备过程和三个处理过程:
1.准备过程:初始化插入式注解处理器。
2.解析与填充符号表过程,包括:
⚪ 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
⚪ 填充符号表。产生符号地址和符号信息。
3. 插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段,本章的实战部分会设计一个插入式注解处理器来影响Javac的编译行为。
4. 分析与字节码生成过程,包括:
⚪ 标注检查。对语法的静态信息进行检查。
⚪ 数据流及控制流分析。对程序动态运行过程进行检查。
⚪ 解语法糖。将简化代码编写的语法糖还原为原有的形式。(java中的语法糖包括泛型、变长参数、自动装拆箱、遍历循环foreach等,JVM运行时并不支持这些语法,所以在编译阶段需要还原。)
⚪ 字节码生成。将前面各个步骤所生成的信息转换成字节码。
—— 《深入理解Java虚拟机 第3版》

以上是文档中对编译过程的介绍,详细过程不再赘述,可自行查阅书籍。
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中的存储内容几乎全部是程序运行的必要数据,没有空隙存在。
根据《Java虚拟机规范》规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:”无符号数“ 和 “表” 。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表一个字节、两个字节、四个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有的表都习惯性地以“_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表的结构如下:

ClassFile {
    u4 magic; | 魔数 占4个字节,用来为Class文件的标识头 0xCAFEBABE  (每4bit位表示一个十六进制数,一共32bit计4个字节)
    u2 minor_version; | 次版本号, 占2个字节
    u2 major_version; | 主版本号, 占2个字节
    u2 constant_pool_count;| 常量池容量计数池,是字节码常量池的入口,占2个字节,表示常量池中的常量项数目,也即:[该值-1]
    cp_info constant_pool[constant_pool_count-1];  | 常量池表cp_info,用来存放常量(字面值和符号引用)
    u2 access_flags;| 访问标志占2个字节,用于识别一些类或者接口层次的访问信息(这个Class是类还是接口;是否为public,是否被声明final)
    u2 this_class;	| 类索引	占2个字节 用于确定该类的全限定名,如: /org/springframe/huawei/TestClass
    u2 super_class;	|父类索引  占2个字节 用于确定该类的父类的全限定名,由于java中不允许多重继承,所以这里只有一个父类索引
    u2 interfaces_count;|接口索引容量计数
    u2 interfaces[interfaces_count];|接口索引集合,占2个字节,用于描述这个类实现了哪些接口,按实现implements关键字顺序从左往右排列
    u2 fields_count;| 字段计数 占2个字节
    field_info fields[fields_count];| 用于描述接口或者类中声明的变量,包括类级变量和实例级变量,而不包括方法中的局部变量
    u2 methods_count; | 方法表容量计数,占2个字节
    method_info methods[methods_count]; | 方法表method_info,对于Class包含的方法的描述
    u2 attributes_count; | 类属性表的容量计数,占2个字节
    attribute_info attributes[attributes_count]; | 类属性表,用于描述某些专有的信息
}

以上是一个类文件(也可以叫做类的表)的数据结构,其中常量池表cp_info、字段表fields_info、方法表method_info、属性表attribute_info中又各自有自己定义好的数据结构和使用规则。
在JDK中为我们准备好了专门用于分析Class文件字节码的工具:javap 。我们直接使用 javap -verbose + 类文件名 即可获得对应的字节码文件内容,下面我准备一个示例文件来获取其字节码文件。
源文件内容 TestClass.java

		public class TestClass {
		    private Integer a;
		    String b = "Hello!";
		    int[][] array = new int[2][2];
		
		    public void test1(int c) {
		        System.out.println(c);
		    }
		
		    public int test2(int c) {
		        return c;
		    }
		}

编译后生成 TestClass.class 文件,执行 javap -verbose TestClass.class 命令获取字节码文件内容如下:

Classfile /C:/Users/xxxx/Desktop/新建文件夹/TestClass.class
  Last modified 2020-5-23; size 584 bytes
  MD5 checksum 56261c0412fd73a720ca6ac2850e1678
  Compiled from "TestClass.java"
public class com.example.demo.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#26         // java/lang/Object."<init>":()V
   #2 = String             #27            // Hello!
   #3 = Fieldref           #8.#28         // com/example/demo/TestClass.b:Ljava/lang/String;
   #4 = Class              #15            // "[[I"
   #5 = Fieldref           #8.#29         // com/example/demo/TestClass.array:[[I
   #6 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #32.#33        // java/io/PrintStream.println:(I)V
   #8 = Class              #34            // com/example/demo/TestClass
   #9 = Class              #35            // java/lang/Object
  #10 = Utf8               a
  #11 = Utf8               Ljava/lang/Integer;
  #12 = Utf8               b
  #13 = Utf8               Ljava/lang/String;
  #14 = Utf8               array
  #15 = Utf8               [[I
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               test1
  #21 = Utf8               (I)V
  #22 = Utf8               test2
  #23 = Utf8               (I)I
  #24 = Utf8               SourceFile
  #25 = Utf8               TestClass.java
  #26 = NameAndType        #16:#17        // "<init>":()V
  #27 = Utf8               Hello!
  #28 = NameAndType        #12:#13        // b:Ljava/lang/String;
  #29 = NameAndType        #14:#15        // array:[[I
  #30 = Class              #36            // java/lang/System
  #31 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #32 = Class              #39            // java/io/PrintStream
  #33 = NameAndType        #40:#21        // println:(I)V
  #34 = Utf8               com/example/demo/TestClass
  #35 = Utf8               java/lang/Object
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
{
  java.lang.String b;
    descriptor: Ljava/lang/String;
    flags:

  int[][] array;
    descriptor: [[I
    flags:

  public com.example.demo.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String Hello!
         7: putfield      #3                  // Field b:Ljava/lang/String;
        10: aload_0
        11: iconst_2
        12: iconst_2
        13: multianewarray #4,  2             // class "[[I"
        17: putfield      #5                  // Field array:[[I
        20: return
      LineNumberTable:
        line 3: 0
        line 5: 4
        line 6: 10

  public void test1(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iload_1
         4: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
         7: return
      LineNumberTable:
        line 9: 0
        line 10: 7

  public int test2(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iload_1
         1: ireturn
      LineNumberTable:
        line 13: 0
}
SourceFile: "TestClass.java"
  1. 常量池,常量池主要存放两大类常量,字面值符号引用。字面值比较接近Java语言层面上的常量概念,如文本字符串、被声明为final的常量值等;而符号引用包含: ①被模块导出或者开放的包Package;②类和接口的全限定名;③字段的名称和描述符;④方法的名称和描述符;⑤方法句柄和方法类型;⑥动态调用点和动态常量。
    上面类文件中的Constant pool所属部分即代表的常量池,可以看到计算机帮我们把整个常量池的40个常量都计算出来了,比如在源码中我们定义了一个类字段字符串值 String b = “Hello!”; 而在常量池中对应的分成三部分来存储,字段的类型#13 = Utf8 Ljava/lang/String; 、字段的名称#12 = Utf8 b 、 字段的对应字符串的常量值#27 = Utf8 Hello!。
  2. 字段内容的描述,对于类级变量String b和 int[][] array 都显示出其对应的内容。
  3. 方法内容的描述,对于我们定义的两个方法test1和test2进行了表述。
    需要强调的是,字节码常量池中的内容将在类加载后存放到方法区的运行时常量池中。
    详细的介绍可以查阅书籍资料,这里不做过深讨论。

二、运行过程

Java程序的运行包含着类得加载过程和对字节码文件的解释执行过程。我并没有将这两个过程分先后顺序,因为这两个过程是相互交叉的— —要知道在JVM中对类的加载是需要时才去加载,而并非一次性加载完毕。
首先我们得明确,java程序有一个入口函数,也就是main方法,在执行的时候是运行这个入口函数来启动整个项目的(参考一下springboot项目中,为每一个项目创建一个xxxApplication的主函数类,而函数中会有一个入口方法)。所以,java程序的运行就始于这个主函数,主函数中执行的内容驱动着JVM后续的所有动作
当你运行一个项目时,或者说执行一个main方法时,JVM先会去运行时数据区的方法区查看是否又main方法所在的类的类文件,如果没有则会先加载main方法所在的类文件。这里引入了 虚拟机中的类加载机制 ,下面我们好好探究一下类加载机制。

2.1 类加载机制的定义

JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被成为虚拟机的类加载机制。

2.2 类加载的时机

一个类型从开始加载到虚拟机内存中,到卸载出内存为止,整个生命周期一共经历七个阶段。
类的生命周期
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,而解析过程却又可能在初始化之后再开始,这是为了支持Java语言的动态绑定特性。而其他的阶段也并不是按部就班的进行,可能会相互交叉地混合进行,在一个阶段中调用、激活另一个阶段。
而类加载的时机,也并非有一定的确定性。但是对于初始化阶段,《java虚拟机规范》中严格规定了有且只六种情况必须对类进行”初始化“,这里的初始化之前必须要进行加载、验证、准备等阶段,因此也 可以认为类的初始化时机就是类的加载时机

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行初始化,则需先出发其初始化阶段。对应的Java代码场景有
    ① 使用new关键字实例化对象的时候。
    ② 读取或设置一个类型的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外,也即常量)。
    ③调用一个类型的静态方法的时候。
    2) 使用java.lang。reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
    3) 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    4) 当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
    5) 当使用JDK7新加入的动态语言支持时,如果java。lang。invoke。MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发器初始化。
    6) 当一个接口中定义了JDK8中新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

以上的六种情况被称为对一个类型进行主动引用。 除此之外,所有的引用方式都不会触发类的初始化,被称为被动引用,常见的被动引用有:

① 通过子类引用父类的静态字段,不会导致子类被初始化
② 通过数组定义来引用类,不会触发此类的初始化。
③ 常量在编译阶段会存入调用类的字节码常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。这里涉及到字节码常量池和运行时常量池,在类加载到方法区的时候,会将类中的字节码常量池的内容(字面值和符号引用等)放入到运行时常量池,所以调用这些常量值的时候实质是到运行时常量池中引用,故不会触发类的初始化过程。

2.3 类加载的过程

2.3.1 加载

“加载”(Loading)阶段是“类加载”(Class Loading)过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事情:
1、 通过一个类的全限定名来获取定义此类的二进制字节流。
2、 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
对于非数组类的加载,既可以使用内置的引导类,也可以使用自定义的类加载器去完成;而对于数组类的加载则不同,数组类本身不通过类加载器创建,它是由JVM直接在内存中动态构造出来的,但是数组类还是跟类加载器有关,因为数组类的元素类型(Element Type,即去掉所有维度的类型)最终还是需要类加载器。
加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了,且类型数据安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象作为城区访问方法区中的类型数据的外部接口。

2.3.2 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
Java语言本身是相对安全的语言(相对C/C++),例如使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,编译器将拒绝编译。但是,Class文件并不一定是由Java源码编译而来,可以使用任何途径,包括用十六进制编辑器(如UltraEdit)直接编写。如果直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。

不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。
1、文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。可能包含以下的验证点:
⚪ 验证魔数是否0xCAFEBABE;
⚪ 主、次版本号是否正在当前虚拟机处理范围之内;
⚪ 常量池的常量中是否有不被支持的常量类型;
⚪ 指向常量的各种索引值中是否有不符合UTF-8编码的数据;

该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。

2、元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:
⚪ 这个类是否有父类;(事实上,除了java.lang.Object之外,所有的类都应当有父类)
⚪ 这个类的父类是否继承了不允许被继承的类;
⚪ 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法;
⚪ 类中的字段、方法是否于父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载、例如方法参数都一致,返回值类型却不同);
……
元数据验证的主要目的是对类的元数据信息进行语义校验。

3、字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。例如:
⚪ 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
⚪ 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
⚪ 保证方法体中的类型转换总是有效的,例如可以把子类对象赋值给父类数据类型,这是安全的,但反之则不行。
如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。

4、符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。主要校验以下内容:
⚪ 验证符号引用中通过字符串描述的权限定名是否能找到对应的类;
⚪ 在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;
⚪ 符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问

符号引用验证的主要目的是确保解析行为能够正常执行。验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

2.3.3 准备

准备阶段是为类的类变量(即静态变量,被static修饰的成员变量)分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量(非静态的成员变量)的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
例如对 public static int a=1; 在准备阶段value初始值为0 。在初始化阶段才会变为1 。
这里存在一个特殊情况,当类的成员变量被final修饰的时候,则说明为常量,常量在准备阶段就会将常量设置的值赋给它。
例如对 public static final int a = 1;在准备阶段就会将1赋给a。

2.3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

2.3.5 初始化

类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经赋值过一次系统要求的初始零值,而在初始化阶段,则会根据程序员的程序编码指定的主观计划去初始化类变量和其他资源。也可以用以下内容进行表述:
初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
这里的编译器收集的顺序是按照语句在源文件中的出现顺序决定的,这意味着静态语句块中之恩呢访问到定义在静态语句块之前的类变量,而定义在它之后的类变量,只能赋值,无法进行访问。
⚪ 类构造器()方法与类的构造函数(实例构造器()方法)不同,它不需要显示地调用父类构造器,JVM会保证在子类的()方法执行前,父类的()方法已经执行完毕。所以在JVM中第一个被执行的()方法类型一定是java.lang.Object。
⚪ 由于父类的()方法先执行,就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

2.4 类加载器及双亲委派模型

2.4.1 类加载器

类加载器用来实现类的加载动作,但在Java中对于任意一个类,都必须有加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。(通俗来讲,比较两个类是否相同,首先要求他们由同一个类加载器加载)
Java中存在着三层类加载器:

  1. 启动类加载器(Bootstrap Class Loader),又叫根加载器,这个类加载器负责加载存放在<JAVA_HOME>\lib目录下,而且是计算机能够按照文件名识别的(比如rt.jar tools.jar, 名字不符合的类库及时放在lib目录中也无法被加载)类库加载到虚拟机的内存中。
  2. 扩展类加载器(Extension Class Loader),负责加载<JAVA_HOME>\lib\ext目录中的所有类库。这里是java系统类库的扩展机制,JDK的开发团队允许用户将通用类型的类库放置在ext目录以扩展功能。(一些公司可能将自己的jar包放到ext目录中使用)。
  3. 应用程序类加载器(Application Class Loader),也可以称为系统类加载器,它负责加载用户类路径(ClassPath)上的所有类库。如果应用程序中没有自定义过类加载器,则这个就是默认的加载器。
2.4.2 双亲委派模型

类加载器的双亲委派模型的工作过程是:
如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都被传送到最顶层的启动类加载器中,只有当父类加载器反馈无法加载(在它的搜索范围中没有找到所需的类)时,子类加载器才会尝试自己去完成加载。
双亲委派模型是为了java类的安全性。以java.lang.Object类为例,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委托给启动加载器到lib目录下的rt.jar中加载,因此不论使用哪个类加载器加载这个类都能保证是同一个类。纵使是当有人恶意创建了一个自定义的java.lang.Object类,在加载的时候也会去找到rt.jar下的Object类加载而非加载自定义的Object类,且此时运行会报错。

至此类加载机制就讲述完毕了。当一个类被加载到方法区中后,便可以使用这个类的字节码文件等信息在堆空间创建实例了,也即我们所说的new 对象。下面讲讲创建对象的过程。

2.5 对象创建过程

当Java虚拟机遇到一条字节码new指令时,即涉及到了类的对象实例的创建过程。首先JVM将会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来将会为新生对象分配内存。对象所需内存的大小在类加载完成后便可以完全确定,为对象分配空间也就是在堆空间上为新对象划分出一块确定大小的内存空间。分配内存有两种方式:

① 指针碰撞法(要求堆内存规整)
Java堆中空闲内存和已使用内存分别存放在堆的两边,中间存放一个指针作为分界点的指示器,在为对象分配内存时只需要将指针向空闲区域移动创建对象所需要的内存大小即可。
② 空闲列表法
如果堆内存中已使用内存区域和空闲区域相互交错,此时虚拟机需要维护一个列表,记录哪些内存块是可用的,在分配时从列表中找到一块足够大的内存区域划分给对象实例并更新列表上的记录。

在内存分配中,因为所有的对象都是用的常量池中的同一份类信息进行分配,所以在并发下可能会存在线程安全的问题。解决这个问题有两个方案:

① CAS操作:
虚拟机采用CAS操作,加上失败重试的方式保证内存分配的原子性
②本地线程分配缓冲(TLAB):
预先为线程分配一部分堆内存空间(线程私有,所以不存在同步问题)用于对象实例的内存分配。只有当TLAB用完,需要分配新的TLAB时,才需要进行同步操作。

内存分配完毕后,JVM必需将分配到的内存空间(不包括对象头)都初始化为零值。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型对应的零值。这一步骤也被称为对象的半初始化,其目的是使类的实例变量有一个初始值。
然后,JVM要堆对象进行必要的设置,比如这个对象是哪个类的实如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息等,这些信息存放在对象的对象头(Object Header)中。这一步之后,类的一个新的对象已经产生了,但并非是我们所new的对象。
最后,要对新的对象执行构造函数(()方法),对上面的半初始化的实例变量按照我们的意图(即可能会使用有参构造创建)进行初始化,这样一个可用的对象才算被创建出来。
对象的内存空间

2.6 对象的访问定位

创建对象的目的是为了后续的访问使用,Java程序会通过栈上的reference数据来操作堆上的具体对象(JVM栈空间中每一个栈帧中存储着局部变量表、操作数栈、动态连接和方法出入口等信息,其中reference信息就存储在局部变量表上)。reference是一个指向对象的引用,这个引用的实现方式有两种:

① 句柄访问
Java堆中划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,需要两次寻址。
通过句柄访问对象

② 直接指针访问
Java堆中对象的布局中需要考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
通过直接指针访问对象

使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中实例数据指针,而reference本身不需要修改。

总结

以上讲解了java的编译运行过程,纵观整个流程无处不体现着Java面向对象的思想。
这里留坑 JVM运行时数据区的讲解 、 Java中锁机制的讲解 之后会整理。

【参考文档及资料】:
https://blog.csdn.net/zycxnanwang/article/details/106139451
https://www.cnblogs.com/fnlingnzb-learner/p/11990943.html
《深入理解Java虚拟机 第三版》

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值