内存区域
由所有线程共享的数据区域:Java堆、方法区
线程隔离的数据区域:Java虚拟机栈、本地方法栈、程序计数器
程序计数器
- 1、程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 2、字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 3、它是程序控制流的指示器,如分支、循环、跳转、线程恢复都是依赖该计数器来完成的。
- 4、该区域是线程私有的。
- 5、如果线程执行的是一个Java方法,那么计数器记录的就是正在执行的虚拟机字节码地址。
- 6、如果线程执行的是一个Native方法,那么计数器记录就是空(
Undefined
)。- 7、该区域是唯一一个不会抛出
OutOfMemoryError
情况的区域。
线程私有的原因
为了线程切换后能恢复到正确的执行位置,保证各线程之前互不影响,因此每个线程都需要一个独立的程序计数器。
Java虚拟机栈
- 1、Java虚拟机栈也是线程私有的,和线程生命周期相同。
- 2、虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机栈都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、和出口等。每个方法被调用直至完成的过程就对应着一个栈帧从虚拟栈中入栈到出栈的过程。
- 3、局部变量表存放了编译器可知的各种java基本数据类型(八大基本数据类型)、对象引用(
reference
类型、returnAddress
类型(指向了一条字节码指令的地址)。- 4、3中的数据类型在局部变量的存储空间是以局部变量槽来表示的,其中
double
和long
类型的数据会占用两个槽,其他数据类型只占一个。
异常情况
- 1、如果栈的请求深度大于虚拟机栈允许的深度,就会抛出
StackOverFlowError
错误。- 2、如果栈支持动态扩展,当栈扩展后还是无法申请到足够的内存,就会抛出
OutOfMemoryError
错误。- 3、对于Hotspot而言,是不支持动态扩展的,除非在创建线程申请内存时就因无法获得足够内存而出现
OutOfMemoryError
异常,否则在线程运行时是不会因为扩展而导致内存溢出的。- 4、无论是由于栈帧过大还是虚拟机栈容量过小,当新的栈帧内存无法分配的时候,Hotspot虚拟机就会抛出
StackOverflowError
异常。
本地方法栈
- 1、本地方法栈也是线程私有的
- 2、本地方法栈和Java虚拟机栈的作用比较类似,方法栈是执行的本地方法
- 3、本地方法栈和Java虚拟机栈的异常情况是一致的。
Java堆
- 1、Java堆是虚拟机所管理的内存中最大的一块,是所有线程共享的一块区域。
- 2、虚拟机启动后,该内存区域的目的就是存放对象实例,但是随着逃逸分析技术以及栈上分配、标量替换,堆的作用有所改变。
- 3、Java堆是垃圾收集管理的主要区域,也被称为GC堆。
- 4、从分配内存的角度,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(TLAB),从而提升对象分配时的效率。
- 5、无论哪个区域,存储的都是对象的实例,对Java堆进行细分只是为了更好的回收内存、更快的分配内存。
- 6、Java堆可以处于物理上不连续的空间,但是在逻辑上要是连续的。
- 7、对于大对象,多数虚拟机会要求连续的内存空间。
异常情况
如果Java堆中没有完成实例分配,并且堆也无法在进行扩展,Java虚拟机就会抛出
OutOfMemoryError
异常。
方法区
- 1、方法区也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等数据。
- 2、这块区域的内存回收主要是针对常量池的回收和对类型的卸载,回收效率降低。
- 3、如果方法区无法满足新的内存分配需求时,将抛出
OutOfMemoryError
异常。
运行时常量池
- 1、该区域是方法区的一部分。
- 2、Class文件中有类的版本、字段、方法、接口等描述信息外,还有常量池表,用于存放编译期生成的各种字面量和符号引用。这部分内容在类加载后存放到方法区的运行时常量池中。
直接内存
- 1、直接内存不是运行时数据区域的一部分,也不是《Java虚拟机规范》中的定义的内存区域,但是也可能引发
OutOfMemoryError
异常。- 2、在JDK1.4中引入NIO,引入了一种基于通道(
Channel
)和缓冲(Buffer
)的I/O方式,可以使用Native函数分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer
对象作为这块内存的引用进行操作,从而提高性能,避免Java堆和Native堆中来回复制数据。- 3、直接内存的容量大小可通过
-XX:MaxDirectMemorySize
参数来指定,如果不去指定,则默认与Java堆最大值一致。- 4、有直接内存的溢出的明显特征就是在Heap Dump文件中不会看见什么明显的异常情况。
内存溢出
对象的创建
类加载检查
当Java虚拟机遇到一条new指令时,首先将检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,就需要先执行对应的类加载过程。
分配内存
在类加载检查通过后,接下来虚拟机就为对象分配内存空间,所需的内存空间在类加载期间已经确定。
内存分配方式
- 1、指针碰撞:假设Java堆中内存是绝对规整的,所有使用过的内存放在一边,没有使用过的内存放在另一边,中间放着一个指针作为分界的指示器,分配内存时只需要把指针向空闲一边移动和需要分配的对象大小的空间即可。
- 2、空闲列表:假设Java堆中内存是不规整的,已被使用的内存和未使用的内存时相互交错在一起的,就需要虚拟机维护一个空闲列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间分配给对象实例,并更新列表的记录。
具体使用哪种内存分配方式是由Java堆中内存是否规整而决定的,而Java堆中内存是否规整又是由具体的使用垃圾收集器是否具有压缩整理功能是决定的。如使用Serial、ParNew带有压缩整理功能的收集器就使用指针碰撞,而CMS这种基于清除算法的收集器就应采用空闲列表。
分配内存时的线程安全问题
对象的创建并不是线程安全的,解决办法有两种:
- 1、对分配内存空间的动作进行同步处理——使用CAS加上失败重试的方法来保证更新操作的原子性。
- 2、把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预分配一块内存(称为本地线程分配缓冲TLAB),那个线程需要分配内存,就在对应线程的内存中分配,当TLAB用完后,对分配缓冲区的动作进行同步操作。----这样可以提高分配效率。
虚拟机是否使用TLAB可以通过
-XX:+/-UseTLAB
参数来设置。
初始化对象
内存分配后,虚拟机需要对分配的内存空间进行初始化零值,保证对象的实例在Java代码中即使不赋初始值也可以直接使用,使程序可以访问到对应数据类型的零值。如果使用TLAB的方式分配内存,也可以在TLAB中完成对象的初始化。
接下来Java虚拟机还需要对对象进行一些必要的设置,如这个对象是哪个类的实例、如何找到类的元数据信息、对象哈希码等。这些信息都是放在对象头中的。
执行<init>()方法
在虚拟机的角度来说,对象的创建已经完成,但是在Java程序中,还需把对象按照程序员的意愿进行初始化,即还需要执行构造函数(Class文件中<init>()方法),<init>()方法执行完成后一个对象才算完全创建完成。
一般new指令后面跟随了
invokespecial
指令,new指令后就会接着执行<init>()方法。
对象的内存布局
对象头
Mark Word
1、用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。
2、是一个有着动态定义的数据结构。
类型指针
- 1、表示对象指向它的类型元数据的指针。
数组长度(可选)
- 1、虚拟机需要通过数组的长度来确定数组的大小
实例数据
- 1、存储了对象真正有效的信息,即在程序代码里面所定义的各种类型的字段、包括从父类继承下来的字段。
- 2、这部分数据的存储顺序会受到虚拟机分配策略参数(
-XX:FieldsAllocationStyle
)的影响以及代码中字段的定义顺序。
对齐填充
- 1、该部分是可选,主要取决于实例数据的的字节数。
- 2、HotSpot虚拟机要求的自动管理内存管理要求对象的起始地址必须是8的整数倍,即任何对象的大小都必须是8的整数倍,而对象头已经设计为8的倍数,因此实例数据和对齐填充的和要是8的倍数。
对象的访问定位
- 1、Java程序使用栈上的
reference
数据来操作堆上的具体对象。- 2、主流的访问方式主要使用句柄和直接指针两种
句柄访问
- 1、Java堆中将可能会划分出一块内存来作为句柄池,而
reference
引用中存放的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自具体的地址信息。- 2、在对象被移动时只会改变句柄中的实例数据指针,而
reference
数据本身不需要被修改。
直接指针访问
- 1、Java堆中对象的内存布局就需要考虑如何放置访问类型数据的相关信息,不需要多一次的间接访问的开销。
- 2、访问速度比句柄访问较快,节省了一次指针定位的开销。
- 3、Hotspot主要使用第二种方式进行对象访问。