深入理解Java虚拟机之内存管理

时间:2023-01-02 13:55:44

JVM(Java Virtual Machine)运行时数据区

深入理解Java虚拟机之内存管理

我们先通过这张图来直观的认识一下JVM运行时数据区是如何分布的,下面就由笔者对其进行简单介绍。

1.程序计数器(Program Counter Register)(字节码的行号指示器)

.java文件通过编译器(javac)编译成为.class文件后成为字节码文件。在多线程的情况下,每个线程都需要一个字节码的行号指示器来标识当前线程运行至哪一条字节码指令。
多个线程以轮流切换并分配处理机的方式来实现JVM多线程,由于处理机是一条一条来执行字节码指令的,当我们要确定每个线程当前所执行到的字节码指令的位置时,就需要将程序计数器私有化,即线程私有化。

2.JVM栈

JVM栈由虚拟机栈和本地方法栈两部分组成,根据所关注对象的不同而对栈的划分不同,笔者认为是合情合理的。
虚拟机栈和本地方法栈都是线程私有的,并且有着和线程相同的生命周期,它们的区别仅仅是服务对象的不同。(java方法和native方法)
虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程

3.JAVA堆

JAVA堆是一个线程共享的内存区域,在虚拟机启动时创建。JAVA堆的存在是为了存放对象实例,几乎所有的对象实例都在JAVA堆上分配内存。
JAVA堆也被称做GC堆(Garbage Collected Heap)。划分角度不同,堆划分也会有很多种,在垃圾收集中我们还会再详细的介绍JAVA堆的划分。

4.方法区(Method Area)

方法区是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
HotSpot虚拟机设计团队选择将GC分代收集扩展至方法区,所以方法区也被称作永久代,但这并不是意味着数据进入方法区就“永久”存在了(方法区的内存回收目标主要是针对常量池的回收和对类型的卸载)

5.运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分。class文件除了类的信息、字段、方法、接口等信息外,还有一项信息称作常量池,常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
JAVA虚拟机并没有对运行时常量池进行严格的规定,不同的虚拟机实现自然可以按照需求来实现这个内存区域。

6.直接内存(Direct Memory)

直接内存不存在于虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域。这部分区域由JAVA堆中的DrectBtyeBuffer对象作为这块内存的引用进行操作。

HotSpot虚拟机对象

注意:这里以HotSpot虚拟机和Java堆为例来深入探讨HotSpot虚拟机在Java堆中对象的分配、布局、访问的全过程

1.对象的创建

虚拟机对象的创建分为三个步骤:1.类加载检查 2.为新生对象分配内存 3.将分配到的内存空间初始化
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
为新生对象分配内存由两种方式:指针碰撞和空闲列表
指针碰撞(堆规整):我们将堆分为两部分,一部分是已用内存,一部分是空闲内存,中间放置着一个分界指示器。当进行内存分配的时候,我们只需要将指针向空闲内存的区域挪动即可
另一种方式是空闲列表(堆不规整),如果java堆中的空闲内存和已用内存是相互交错的,当进行内存分配的时候,我们就需要一张表来记录哪些内存已分配,哪些内存未分配,这样我们只需要在进行完内存分配后维护那张表就好了,这就是所谓的空闲列表
选择哪种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
内存分配完成后,虚拟机需要将分配到的内存空间初始化为零值,接着需要对对象头进行一些必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息等等......
至此,从虚拟机的角度来看,一个新的对象已经产生了,但从java程序的角度来看,对象的创建才刚刚开始......

2.对象的内存布局

在HotSpot虚拟机中,对象在内存存储的布局可以分为3块区域;对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

Header(对象头)

对象头由两部分组成,第一部分为用于存储对象自身的运行时数据,如哈希码、gc分代年龄、锁状态标志、线程持有的锁等等;第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Instance Data(实例数据)

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来

Padding(对齐填充)

对齐填充并不是必要的,它的存在仅仅是起着占位符的作用。

3.对象的访问定位

对象的访问方式分为使用句柄和直接指针两种方式
深入理解Java虚拟机之内存管理
上图是通过句柄访问对象的生动描述。使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动后只会改变句柄中的实例数据指针,而reference本身不需要修改
深入理解Java虚拟机之内存管理
上图是通过直接指针方位对象的生动描述,使用直接指针访问对象最大的好处是速度更快,它节省了一次指针定位的时间开销,这是一个积少成多的执行成本。

本文版权归作者0xTsmon和博客园所有,欢迎转载和商用,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.