文章目录
- JVM 内存模型概述
- 基于分代收集理论设计的垃圾收集器所管理的堆结构
- 方法区的演变
- 内存分配
- 划分内存的方法
- 划分内存时如何解决并发问题
- 对象栈上分配
- 基于分代收集理论的垃圾收集器管理下的内存分配规则
- 对象优先分配在 Eden 区
- 大对象直接进入老年代
- 长期存活的对象将逐步进入老年代
- 对象动态年龄判断机制
- 老年代空间分配担保机制
JVM 内存模型概述
JVM
内存模型,也叫 JVM
运行时数据区。下面是 JVM
内存模型的图解:
可以看出,JVM
内存模型主要分为以下的几块:
- 堆:线程共享,用于存放对象实例,是由垃圾收集器管理的区域,不同的垃圾收集器对于堆会有不同的布局实现
- 方法区:线程共享,用于存储已被加载的所有类的类型信息、静态变量、字段信息、方法信息、字面量、符号引用等数据
- 程序计数器:线程私有,是当前线程所执行的字节码的行号指示器,是唯一一块不会发生
OOM
的区域 Native
栈:线程私有,也叫本地方法栈,当JVM
执行Native
方法时,存储一些必要的数据JVM
栈:线程私有,也叫虚拟机栈,每个方法被执行时,JVM
都会创建一个栈帧用于存放方法的局部变量表、操作数栈、方法返回地址、动态链接等数据
基于分代收集理论设计的垃圾收集器所管理的堆结构
堆是由垃圾收集器管理的内存区域,不同的垃圾收集器对于堆会有不同的布局实现,这里主要介绍基于分代收集理论设计的垃圾收集器所管理的堆结构。
基于分代收集理论,堆内存结构如下所示:
可以看出,堆内存结构主要分为:
Yound Gen
:新生代,约占整个堆大小的1/3
Eden
区:Eden
区,大约占新生代的8/10
Survivor 0
区:S0
区,大约占新生代的1/10
Survivor 1
区:S1
区,大约占新生代的1/10
Old Gen
:老年代,约占整个堆大小的2/3
方法区的演变
方法区,在 JDK8
之前,是用永久代来实现的,在 JDK8
之后,是用元空间来实现的
内存分配
划分内存的方法
JVM
划分内存的方法有两种:
- 指针碰撞:
Bump the Pointer
,当堆中的内存比较整齐,即用过的内存和空闲内存有一条清晰的分界线(分界线处有个指针作为分界点指示器)时,可以使用这种方法- 指针碰撞方法在分配内存时,仅仅需要将分界点指示器向空闲内存方向挪动一段距离,距离取决于所需内存大小
- 空闲列表:
Free List
,当队中的内存不整齐,即用过的内存和空闲内存相互交错、没有清晰的分界线时,就不能使用指针碰撞的方式来分配内存了。JVM
会维护一个列表,记录队中每块内存的使用情况,在分配的时候从可用的内存中分配一块足够大的内存出去,并把这块内存标记为已使用
划分内存时如何解决并发问题
在实际的 JVM
运行过程中,很可能会出现多个线程同时申请内存的情况,此时如果不对划分内存操作进行并发控制操作,大概率会出现并发安全问题(多个内存分配请求被分配到了同一块内存空闲)。
针对上述情况,JVM
采取的并发控制手段有:
CAS
:在划分内存时,采用CAS
+ 自旋操作来保证同一块内存不会同时被分配给多个分配请求TLAB
:Thread Local Allocation Buffer
,即本地线程分配缓冲。核心思想就是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在堆中预先分配一小块内存,这个线程后续的内存分配请求都会先在这块区域上进行,这一小块内存就称为本地线程分配缓冲。可以通过设置参数来开启或关闭此功能。
对象栈上分配
为了减少生命周期较短的对象在堆内分配的次数,JVM
会通过逃逸分析结合标量替换两个功能,把一些引用不会逃逸出方法之外的,可以使用若干个标量来替代本身的对象,将其内存分配在栈上进行。即:
- 应用不会逃逸出方法之外,即这个对象是当前方法栈帧中创建出来的一个局部变量,其生命周期随着方法的结束而结束
- 可以使用若干个标量来替代本身,即这个对象可以被分解成若干个不可再分的标量(如基本数据类型,引用类型等)来替代
满足上述两个条件的对象,将可能会不被创建,而是直接由分解成若干个分配在栈上的标量替代,如:
class User {private int age;private String name;
}public void methodA() {User user = new User();user.age = 10;user.name = "Test";//将 user 数据插入到数据库中//由于 user 对象的引用没有传递到外部,且 user 对象本身可以被一个 int 类型和一个 reference 类型的标量替换掉//那么在开启了逃逸分析+标量替换时,将会用栈上分配的两个标量来替换掉,而不会在堆中创建这个对象......
}
注意,栈上分配必须依赖逃逸分析和标量替换两个功能才能生效。
基于分代收集理论的垃圾收集器管理下的内存分配规则
基于分代收集理论的垃圾收集器,将堆根据存放对象的年龄不同划分成了不同的区域。在不同的区域内,会有不同的分配规则。
对象优先分配在 Eden 区
在大多数情况下,对象将会优先分配在 Eden
区。当 Eden
区没有足够的空间进行分配时,将会尝试进行一次 Young GC
。
大对象直接进入老年代
当对象的大小超过一定的阈值时,对象将会直接被分配到老年代
长期存活的对象将逐步进入老年代
Eden
+ S0
+ S1
共同组成 Young Generation
的设计,称为 Appel
式垃圾收集机制。其主要的特点就是长期存活的对象将逐步进入老年代:
- 每个对象的对象头
Mark Word
中都会记录一个当前对象年龄的计数器 - 在每一次经历
Young GC
后,如果对象依然存活,那么计数器将+1
- 在经历若干次(默认为
15
次)Young GC
还依然存活的对象,也即对象年龄大于晋升老年代年龄阈值(默认为15
)的对象,将会被移动到老年代中
对象动态年龄判断机制
在某一次 Young GC
时,如果此时 Survivor
空间中小于等于某年龄的所有对象大小的总和大于等于 Survivor
空间大小的一半,那么大于或等于该年龄的所有存活对象将直接进入到老年代中(尽管此时对象年龄可能尚未达到晋升老年代年龄阈值)
老年代空间分配担保机制
Appel
式垃圾收集机制,核心思想使用的是复制算法。但是这个复制算法的备用空间(空闲的 Survivor
区)远小于主用空间(Eden
区 + 使用中的 Survivor
区),那么如果在某次 Young GC
时,存活下来的对象比备用空间大怎么办?
Appel
式垃圾收集机制给出的解决方案就是将这些所有存活下来的对象都移动到老年代中。这就是老年代空间分配担保机制的来源以及核心思想。
更细节的:
- 在发生
Young GC
之前,JVM
必须检查当前老年代最大可用的连续空间是否大于新生代当前所有对象的总空间- 如果成立,那么可以安全地执行本次
Young GC
- 如果不成立,那么
JVM
会查看当前是否开启了老年代空间分配担保机制(JDK 8
之后默认开启)- 如果未开启,那么将会执行一次
Full GC
- 如果开启了,那么将会继续检查老年代最大可用的连续空间是否大于历次
Young GC
晋升到老年代的对象的平均总大小- 如果小于,那么将会执行一次
Full GC
- 如果大于,那么将会有风险地执行本次
Young GC
- 如果小于,那么将会执行一次
- 如果未开启,那么将会执行一次
- 如果成立,那么可以安全地执行本次
注意,当有风险地执行 Young GC
时,如果本次 Young GC
存活下来的对象总大小大于老年代最大可用的连续空间(即出现了担保失败的情况),那么将会执行一次 Full GC