java 运行内存_Java运行时内存区域

一. 运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为几个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁。

JVM运行时数据区

1. 程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

每个线程都需要一个独立的程序计数器,用来保证线程切换后能恢复到正确的执行位置。因此,程序计数器是线程私有的。

2. Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程相同,它描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型。

64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

3. 本地方法栈

本地方法栈同Java虚拟机栈的作用相似,Java虚拟机栈为Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。

4. Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在堆上分配。

Java堆是垃圾收集器管理的主要区域,也被称作“GC堆”。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就行。

从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,Java堆还可细分为:新生代和老年代;更细致一点可分为Eden空间、From Survivor空间、To Survivor空间等。

从内存分配的角度来看,线程共享的Java堆中可划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

划分的目的是为了更好地回收内存,或者更快地分配内存。

5. 方法区

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

Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它的目的同Java堆是不同的。

方法区内存回收的目标主要是针对常量池的回收和对类型的卸载,回收的条件相当苛刻,但却十分必要。

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

一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,运行期间产生的新的常量也可以放入池中。

二. HotSpot虚拟机对象探秘

1. 对象的创建

(1) 确认对象是否加载

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

(2) 给新对象分配内存

类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配内存的任务等同于把一块确定大小的内存从Java堆中划分出来。

给新对象分配内存的方法有两种:指针碰撞法和空闲列表法。

对象创建在虚拟机中是非常频繁的行为,要保证其在并发下的安全性。

(3)默认初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),默认初始化的操作可以保证对象的实例字段在Java代码中可以不赋初始值就可直接使用,程序能访问到这些字段的数据类型所对应的零值。这也是虚拟机提供的一种最低限度的安全性保证。

(4)对对象进行必要的设置

接着,虚拟机对对象进行必要的设置,如这个新对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

(5)执行构造器中的初始化

执行完上述动作后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,方法还没有执行,所有的字段都还为零。

一般来讲,执行new指令之后对接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。

2. 对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充。

HotSpot虚拟机的对象头包括两部分信息:

一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;

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

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。一般情况,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,而对象头部分正好是8字节的倍数,如果对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3. 对象的访问定位

创建对象的目的是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的对象访问方式有两种:使用句柄和直接指针。

(1)使用句柄:在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

使用句柄访问对象

(2)直接指针:reference中存储的直接就是对象地址。

使用指针访问对象

使用句柄访问好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用指针访问的好处就是速度更快,它节省了一次指针定位的时间开销。

HotSpot采用直接指针的方式进行对象的访问。

网络上看到一张整理的比较好的图片,引用如下:

JVM内存管理

你可能感兴趣的:(java,运行内存)