深入学习JVM 01
Java内存区域与对象的创建
运行时数据区域
程序计数器
- 程序计数器
(Program Counter Register)
是一块较小的内存空间,是线程私有的内存,可以看做是线程所执行的字节码的行号指示器。字节码指示器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要这个计数器来完成。 - 为线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。因此各条线程之间计数器互不影响,独立存储。
- 如果线程正在执行的是一个
Java
方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native
方法,这个计数器的值则为空(undefined)
。这片内存区域也是唯一一个在Java
虚拟机规范中没有规定任何OutOfMemeryError
的区域。
Java虚拟机栈
-
Java
虚拟机栈(Java Virtual Machine Stacks)
也是线程私有的,它的生命周期与线程相同。 - 虚拟机栈描述的是
Java
方法执行的内存模型:每个方法在执行的同时会常见一个栈帧(Stack Frame)
用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。 - 局部变量表存放了编译期可知的各种基本数据类型、对象引用
(reference类型)
和returnAddress
类型(指向一条字节码指令的地址)
。其中long
和double
类型的数据会占用2
个局部变量空间(Slot)
,其余的数据类型只占用1
个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 - 这个区域会产生两种异常状况:1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
*Error
异常;如果扩展时无法申请到足够的内存,将会抛出OutOfMemoryError
异常。
本地方法栈
- 本地方法栈
(Native Method Stack)
与虚拟机栈所发挥的作用十分相似,区别只是虚拟机栈为执行Java
方法服务,而本地方法栈为使用到的Native
方法服务。所以,它也是线程私有的。
Java堆
-
Java
堆(Java Heap)
是Java
虚拟机所管理的内存的最大的一块,Java
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象实例及数组都要在堆上分配。 - 由于现在收集器基本都采用分代收集算法,所以
Java
堆还可以分为:新生代和老生代。再细致点可以分为:Eden
空间、From Survivor
空间、To Survivor
空间等。 -
Java
堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是客货站的,主流的虚拟机都是可扩展实现的。 - 如果在堆中没有内存完成实力分配,并且堆也无法再扩展时,将会抛出
OutOfMemoryError
异常。
方法区
- 方法区
(Method Area)
与Java
堆一样,是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - 方法区除了和
Java
堆一样不需要连续的内存和可以选择固定大小或者可扩展之外,还将可以选择不实现垃圾收集。但这并不意味着数据进入了方法区就不有GC
回收的必要了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。 - 当方法区无法满足内存分配需求时,将抛出
OutOfMemeoryError
异常。
运行时常量池
- 运行时常量池
(Runtime Constant Pool)
是方法区的一部分。Class
文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)
,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。 - 运行时常量池相对于
Class
文件常量池的另外一个重要特征是具备动态性,Java
语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,常见的就是String
的intern()
方法。
直接内存
- 直接内存
(Direct Memory)
并不是虚拟机运行时数据区的一部分,也不是Java
虚拟机栈中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError
异常出现。 - 本地直接内存的分配不会受到
Java
堆大小的限制。但是,肯定会受到本机总内存(RAM)
大小以及处理器寻址空间的限制。 - 如果忽略直接内存,使得各个内存区域总和大于物理内存限制
(包括物理的和操作系统级的限制)
,从而导致动态扩展时出现OutOfMemoryError
异常。
对象的创建
- 虚拟机遇到一条
new
指令时:- 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用;
- 并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。
- 如果没有,那么必须执行相应的类加载过程。
- 虚拟机为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从
Java
堆中划分出来。虚拟机分配内存的方式有两种:-
指针碰撞
(Bump the Pointer)
:假设Java
堆中的内存是绝对规整的,所有已用的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针想空闲空间那边挪动一段与对象大小相等的距离。 -
空闲列表
(Free List)
:如果Java
堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那么就不能使用上述的方式了。这时,只需要让虚拟机维护一个列表,记录哪些内存块是可以用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
-
指针碰撞
- 由上可以看出,选择哪种分配方式是由
Java
堆是否规整决定,而Java
堆是否规整又由所采用的的垃圾收集器是否带有压缩整理功能决定的。 -
在并发情况下,如何保证创建对象时的线程安全性问题呢?有两种解决方案:
- 对分配内存空间的动作进行同步处理,采用的是
CAS
配上失败重试的方式保证更新操作的原子性; - 把分配的动作按照线程划分在不同的空间中进行,及每个线程在
Java
堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Loacl Allocation Buffer, TLAB)
。只有在这个缓冲用完并且分配新的TLAB
时,才需要同步锁定。
- 对分配内存空间的动作进行同步处理,采用的是
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。然后,虚拟机需要对对象进行必要的设置,如对象属于哪个类的实例、它的元数据信息如何找到、对象的
hash
值、GC
分代年龄等信息。这些信息都存放在对象的对象头中(Object Header)
之中。
对象的内存布局
不同的虚拟机对象的内存布局是不一样的,这里以HotSpot
虚拟机为例。
- 对象在内存中存储的布局可以分为
3
块区域:对象头(Header)
、实例数据(Instance Data)
和对齐填充(Padding)
。 - 对象头包括两部分信息:
- 用于存储对象自身的运行时数据,如哈希值
(HashCode)
、GC
分代年龄、锁状态标志、线程持有的锁、偏向线程ID
、偏向时间戳等,这部分数据官方称之为Mark Word
。 - 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。但是,查找对象的元数据信息并不一定要经过对象本身。
- 用于存储对象自身的运行时数据,如哈希值
- 实例数据部分,是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。
- 各种类型的字段内容的存储是有顺序的,相同宽度的字段总是被分配到一起。在此前提下,在父类中定义的变量会出现在子类之前。
- 对齐填充并不是必然存在的,也没有什么特殊含义,仅仅是起着占位符的作用。由于
HotSpot VM
的自动内存管理系统要求对象起始地址必须是8
字节的整数倍,也就是说对象的大小必须是8
字节的整数倍。而对象头部分刚好是8
字节的倍数(1或者2倍)
。所以,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
-
Java
程序需要通过栈上的reference
数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。-
使用句柄访问的话,那么
Java
堆中将会划分出一块内存来作为句柄池,reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 -
如果使用直接指针访问,那么
Java
堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference
中存储的直接就是对象地址。
-
-
两种对象访问方式各有优势:
- 使用句柄的最大好处就是
reference
中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference
本身不需要被修改。 - 使用直接指针访问方式的最大好处就是速度更快,因为它节省了一次指针定位的时间开销。
- 使用句柄的最大好处就是
总结:
- 主要讲述了
JVM
中内存的结构,细致的描述了每个结构的组成、作用和可能产生的异常。 - 介绍了
JVM
是如何执行new
指令的,对象是如何被创建的,对象的组成和各组成存储的信息和其作用等。