JVM虚拟机-探究Java内存区域与对象创建过程

JVM虚拟机-探究Java内存区域与对象创建过程


欢迎访问我的个人博客
参考书籍:《深入理解JAVA虚拟机》

这里写目录标题

  • JVM虚拟机-探究Java内存区域与对象创建过程
    • 1.概述
    • 2.运行时数据区域
      • 2.1.程序计数器
      • 2.2.Java虚拟机栈
      • 2.3.本地方法栈
      • 2.4.Java堆
      • 2.5.方法区
      • 2.6.运行时常量池
    • 3.HotSpot虚拟机对象探秘
      • 3.1.对象的创建
      • 3.2.对象的内存布局
      • 3.3.对象的访问定位
    • 4.本文知识点思维导图(原图可私信)

1.概述

对于Java程序来说,在虚拟机自动内存管理机制的帮助下,不容易出现内存泄漏和内存溢出的问题。对于程序员来说,不需要向C/C++程序员一样书写delete/free的代码。但是正因为将内存管理的权利交给了虚拟机,JAVA程序一旦出现内存泄漏和内存溢出的问题,如果程序员不了解Java是如何使用内存的,那么问题的排查将会是一件异常苦难的工作。

2.运行时数据区域

Java虚拟机在执行Java程序时,会将Java虚拟机管理的内存划分为若干个不同的数据区域。这些数据区域有着不同的用途。Java虚拟机所管理的内存包括以下几个区域。
JVM虚拟机-探究Java内存区域与对象创建过程_第1张图片

2.1.程序计数器

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行字节码的行号指示器,在Java虚拟机的概念模型中,字节码解释器工作时通过改变程序计数器的值选取下一条需要执行的字节码指令。

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式进行实现。因此为了线程切换后,程序计数器能够指到正确的位置,每个线程都有一个独立的程序计数器(线程的工作内存中),各线程独立存储,互不影响。

2.2.Java虚拟机栈

Java虚拟机栈与程序计数器一样,也是线程私有的,生命周期和线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存储了编译器已知的各种Java虚拟机基本数据类型,对象引用(可能指向Java堆中的句柄池,可能直接指向Java堆中的对象)。

2.3.本地方法栈

本地方法栈与虚拟机栈发挥的作用类似,其区别是Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用的到的本地方法(Native)服务。

2.4.Java堆

堆是虚拟机所管理的内存中最大的一块,Java堆是一块被线程共享的区域,在虚拟机启动时创建。此区域唯一的作用就是存放对象实例,Java中几乎所有的对象实例都在这里分配内存。。

Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称为“GC”堆。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆会经常出现新生代,老生代等名词。这里的区域划分仅仅只是一种垃圾收集器的设计风格,不代表这是具体实现的固有内存分区。

从分配内存的角度来看,线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。但无论从什么角度,无论如何划分,都无法改变Java堆中存储内容的共性,无论哪个区域,存储的都只能是对象的实例,将Java堆的戏份只是为了更好的回收内存/更快地分配内存。

2.5.方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量,即时编译器编译后的代码缓存等数据。

2.6.运行时常量池

运行时常量池是方法区的一部分。除了有类的版本、字段、方法、接口等描述信息之外,还有一项是常量池表,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

3.HotSpot虚拟机对象探秘

3.1.对象的创建

Java是一门面向对象的编程语言,在程序运行过程中不断有对象被创建出来。在语言层面上,创建对象的动作仅仅是一个new关键字而已,而在虚拟机中的创建是一个什么样的过程呢?

  1. 类加载检查:当Java虚拟机遇到了一个字节码new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,并且检查这个符号引用锁代表的类是否已经被加载、解析、初始化过。如果没有,那必须先执行相应的类加载过程。

  2. 内存分配:在类加载检查通过后,虚拟机会向新生对象分配内存。为对象分配内存空间的任务实际上等同于把一块确定大小的内存块从Java堆中划分出来。

    1. 分配方式,指针碰撞:假设Java堆是一块规整的内存空间,所有被使用过的空间放在一遍,空闲的空间放在另一边,中间放着一个指针,作用为指示分界点(分界点指示器),那分配内存后,指针会向空闲空间挪动一段与分配空间大小相同的距离。
    2. 分配方式,空闲列表:如果Java对的内存不是规整的,使用过的和空闲的内存交错在一起,那么虚拟机就必须维护一个列表,这个列表记录了哪一块内存被使用了,哪一块内存是空闲的,在分配的时候找到一块足够大的内存进行分配,并更新空闲列表。

    总的来说,分配方式是由Java堆空间是否规整来决定,而Java对是否规整是又由所采用的垃圾收集器是否带有空间压缩整理的能力决定。

  3. 线程安全:由于Java是支持多线程的语言,且new对象是一个频繁的行为,那么在并发情况下new对象是不安全的,可能会出现正在给对象A分配内存,指针还没有修改,对象B又同时使用了原来的指针来分配内存的情况。

    1. 分配动作同步处理:虚拟机上采用的CAS配上失败重试的方式保证更新操作的原子性;
    2. 线程划分内存空间:把内存分配的动作按照线程划分到不同的空间之中进行,即每个线程在Java堆中预先分配一块内存,称为本地线程分配缓冲(TLAB)。虚拟机是否使用TLAB可以通过-XX:+/UseTLAB参数进行设定
  4. 内存空间初始化:内存分配完成后虚拟机必须将分配到的内存空间(不包括对象头)都初始化为零值,这不操作保证了实例对象字段在Java代码中不用赋初值就可以直接使用,使程序能访问到这些字段的数据类型所对应的零值。

  5. 对象必要设置:接下来Java虚拟机需要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希值(实际上对象的哈希值会延后到真正调用hashCode()方法时进行计算)、对象的GC分代年龄等信息。这些信息存放在对象头中。

  6. 构造函数:上述工作完成后,从虚拟机角度来看,一个新的对象产生了。但是从Java角度来说,对象的创建才刚刚开始—构造函数,这是所有的字段都还是零值,对象需要的资源,状态信息还没有构造好。一般来说new指令之后会执行init()方法,这时一个真正可用的对象才构造出来。

3.2.对象的内存布局

在HotSpot虚拟机内,对象在堆内存中的存储布局可以划分为三个部分:对象头,实例数据,对齐填充。

对象头

虚拟机对象的对象头部包括两类信息,第一类用于存储对象自身运行时数据,如:哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

实例数据

实例数据是在对象真正存储的有效信息,即在我们程序代码中所定义的各种类型字段的内容。

对齐填充

不是必然存在的也没有特殊的含义,仅仅起到占位符的作用。

由于虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象头的已经被精心设计为8字节的整数倍,但是实例数据部分可能没有8字节的整数倍,所以需要对齐填充来进行补全。

3.3.对象的访问定位

创建对象后要适用对象,Java程序会通过Java虚拟机栈上的reference数据来操作堆上的具体对象。但是对象的访问方式是由虚拟机实现而定的,主流的实现方式有一下两种:

  1. 句柄访问:Java堆中划分出来一块内存作为句柄池,reference存储的是对象的句柄地址,句柄中包含了对象的实例数据与类型数据各自具体的地址信息。
  2. 直接指针访问:若使用直接指针访问,Java堆中对象就需要考虑如何放置访问类型数据的相关信息,reference中存储的就是对象的地址,如果只是访问对象本身的话就不需要多一次的访问开销。

这两种访问方式各有优势:

  • 使用句柄访问最大的好处就是reference中存储的时稳定句柄地址,再对象移动时只会改变句柄中的实例数据指针,而reference本身不需要更改。
  • 使用直接指针访问最大的好处就是速度更快,节省了一次指针定位的开销,拥有更高的效率。

在《深入理解Java虚拟机》一书中主要描述的HotSpot虚拟机而言,主要是使用第二种方式进行访问。

对象访问示例图:

句柄访问:

JVM虚拟机-探究Java内存区域与对象创建过程_第2张图片

直接访问:

JVM虚拟机-探究Java内存区域与对象创建过程_第3张图片

4.本文知识点思维导图(原图可私信)

JVM虚拟机-探究Java内存区域与对象创建过程_第4张图片

你可能感兴趣的:(JVM)