JVM系列——Java内存区域
文章目录
Java内存区域
运行时数据区域
运行时数据区域主要分为五大部分:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
其中堆和方法区是所有线程共享的区域,其他三个是线程私有的区域。
程序计数器
程序计数器是线程私有的,每个线程都有一个单独的程序计数器,它是一块很小的内存空间,可以将它理解为当前线程正在执行的字节码的行号指示器,也就是说他指向当前程序正在执行的那一行代码。
当执行Java方法时,程序计数器记录的是当前线程正在执行的字节码指令的地址,若执行的是Native方法,则计数器为空(undefined)。
这里的“pc寄存器”是在抽象的JVM层面上的概念——当执行Java方法时,这个抽象的“pc寄存器”存的是Java字节码的地址。实现上可能有两种形式,一种是相对该方法字节码开始处的偏移量,叫做bytecode index,简称bci;另一种是该Java字节码指令在内存里的地址,叫做bytecode pointer,简称bcp。
此内存区域是JVM规范中没有规定任何OutOfMemoryError的区域。
Java虚拟机栈
Java虚拟机栈也是线程私有的,与线程同时创建,它描述了Java方法执行的内存模型,每个Java方法的执行都伴随着一个栈帧的入栈,而方法的结束伴随着栈帧的出栈。
栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈帧有可能在系统的堆(不一定是Java的堆)上分配内存。
局部变量表存放了编译期可知的各种基本类型、对象引用和returnAddress类型。其中long和double类型的数据会占用2个局部变量空间,其余数据类型占用1个。局部变量表在编译期间完成内存分配,方法运行期间不会改变局部变量表的大小。
当线程请求的栈深度大于虚拟机所允许的深度的话会抛出StackOverflowError异常;若虚拟机栈在进行动态内存扩展时无法申请到足够的内存,则会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈和Java虚拟机栈是一样的,只不过区别在于Java虚拟机栈为执行Java方法(字节码),而本地方法栈是执行Native方法。本地方法栈采用C Stack来支持Native方法,HotSpot虚拟机将虚拟机栈和本地方法栈合二为一。
本地方法栈一样会抛出StackOverflowError和OutOfMemoryError异常。
Java堆
Java堆是线程共有的一块内存区域,也是JVM所管理的最大的一块内存区域,同样也是GC的主要内存区域。它在JVM启动时创建,它的唯一目的就是存放Java对象,几乎所有的对象都在这里进行分配内存。几乎所有的Java对象和数组都在此分配空间
Java堆还可以分为新生代、老年代和永久代。新生代还可以细分为Eden空间、From Survivor空间、To Survivor空间。
JDK1.8中移除了永久代,取而代之的是一个叫做元空间的区域
永久代使用的是JVM的堆内存空间,元空间使用的是物理内存,直接受到本机物理内存的限制。
元空间出现的原因:
-
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
这是JRockit和Hotspot融合工作的一部分。 JRockit客户不需要配置永久代(因为JRockit没有永久代),并且习惯于不配置永久代。
-
永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen
线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(这在后面分析Java对昂创建时会提及)。
Java堆在物理上不一定是连续的,只要逻辑上连续即可。
若对象无法在现有的堆空间中完成内存分配,且无法再扩展时会抛出OutOfMemoryError异常。
方法区
方法区也是一个线程共享的内存区域,它主要保存的内容是已被虚拟机加载的类信息、常量、静态变量、即时编译器变异后的代码等数据。方法区是堆的逻辑部分
在JDK1.7之前,方法区位于永久代,在JDK1.8之后,永久代被前面提到的元空间所取代,因此方法区也被移至元空间。
方法区可以不实现垃圾回收,一般回收的内容就是对已加载类的卸载。
当方法去无法分配内存时会抛出OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区的一部分,主要存放的内容是符号引用常量和字面常量。
JDK1.7开始,字符串常量和符号引用被移除永久代
- 符号引用被移至系统对内存
- 字符串字面量被移至Java堆
当常量池无法申请到内存时会抛出OutOfMemoryError异常。
直接内存
直接内存不属于JVM运行时数据区,也不是JVM规范中定义的内存区域。但是也可能导致OutOfMemoryError异常。
JDK1.4之后的NIO可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆的DirectByteBuffer对象作为这块内存的引用进行操作。
HotSpot虚拟机对象
对象的创建
对象创建流程如下:
- 当虚拟机要创建一个对象时,首先检查该类是否已执行类加载过程,若未被加载,则先执行类加载过程。
- 当类加载检查通过之后,虚拟机会为新生对象分配内存,即从Java堆中划分出一块内存,内存分配有两种方式:指针碰撞和空闲列表,到底采用哪种内存方式取决于内存是否规整,换句话说,采用哪种GC方式。
- 内存分配完成之后会初始化零值(不包括对象头),若使用了TLAB(本地线程分配缓冲),也可以提前至TLAB分配时进行。这一步保证了对象的实例字段在被初始化之前也可以被使用。
- 然后会对对象进行设置,即对对象头进行设置。
- 最后会按照程序员的意愿对对象进行初始化,即执行init方法。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储布局分为3块区域:对象头、实例数据和对其填充。
对象头
对象头分为两部分:第一部分用于存储对象自身的运行时数据,第二部分是类型指针。
第一部分为markWord,markWord记录了对象和锁的有关信息,包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
第二部分为对象指向它的类元数据的指针,虚拟机通过该指针来判断该对象属于哪一个类的实例。
若是数组对象的话,还会多出一个数组长度
在32位HotSpot虚拟机中,存放Class指针的空间大小时4字节,MarkWord空间的大小也是4字节,数组的话会多加4字节表示数组的长度。
在64位系统及64位JVM下,开启指针压缩,那么头部存放Class指针的空间大小还是4字节,而MarkWord区域会变成8字节,也就是说对象头至少为12字节。
实例数据
实例数据保存的是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容,即该对象的所有属性,包括自己的以及从父类继承的。
该部分的存储顺序受代码中的先后顺序和虚拟机分配策略参数的影响。HotSpot虚拟机默认的分配策略为相同宽度的字段总被分配到一起,在满足此定义的前提下,从父类继承的属性在子类属性之前。若CompactFields参数值为true(默认为true),那么子类中较窄的变量也可能会插入到父类变量的空隙之中。
对其填充
对其填充只是为了起占位符的作用,HotSpot虚拟机的自动内存管理系统要求对象起始地址必须使8的整数倍,即对象的大小必须使8的整数倍。对象头部分刚好是8字节的整数倍(32位或64位),因此,对齐填充主要是填充对象实例数据部分。
对象的访问方式
目前主流的访问方式有两种:通过句柄访问和直接指针。
直接指针
直接指针是指引用变量直接指向堆中的对象部分,对象部分包括两部分内容:对象实例数据和对象类型数据的指针。
句柄访问
句柄访问是指Java堆中会划分出一块内存来作为句柄池,此时引用变量存储的就是对象的句柄地址,句柄包括两部分内容:对象实例数据的指针和对象类型数据的指针。
通过句柄访问的优势:对象引用中存储的是固定的句柄地址,当对象被移动时只需改变句柄中的对象实例数据指针即可。
直接指针的优势:方法速度快。
参考资料
深入理解JVM