目录
引言
Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。
1.内存分配
JVM规范中指定运行时数据去分为以下几块,方法区、堆、程序计数器、虚拟机栈、本地方法栈。
其中方法区、堆是所有线程共享的,而程序计数器、虚拟机栈、本地方法栈是线程私有的。
线程共享? | jvm规范 | hotspot实现 | 存放内容 | 可能异常 | hotspot选项控制 |
---|---|---|---|---|---|
共享 | 方法区 | 永久代 1.8 元空间 | 存放加载的类 类信息包含常量池、静态变量 | OutOfMemoryError: PermGen space | -XX:MaxPermSize=64m |
堆 | 堆-年轻代 堆-老年代 | 对象&数组, 最大的一块内存 gc管理的主要区域 | OutOfMemoryError: Java heap space | -Xms128m -Xmx700m -XX:NewRatio=2 | |
私有 | PC程序计数器 | PC程序计数器 | 下一条字节码指令地址 | 不会发生内存溢出 | |
虚拟机栈 | 栈 | 执行java字节码的栈 | StackOverflowError | -Xss | |
本地方法栈 | JNI调用的执行机器码栈帧 |
1.0 创建一个对象涉及的内存
包含栈中引用、堆中对象、方法区-类信息,如下如,以上的3中区域内存通过如下方式串联
- 方法区加载对应的类(当前类及其父类、依赖类)
- 堆中分配内存存放对象实例,对象头中引用指向方法区类型信息
- 栈中分配局部变量引用到对象实例
1.1 方法区
JVM规范中的方法区,在hotspot(jdk1.8以前)安排在堆中永久代,JDK1.8以后成为元空间
包含类信息和运行时常量池,
运行时常量池是每个类或者接口的类文件中的常量池的运行时内存存储区域,
常量池保存代码中的字面量、类名等,如一行String str = "123";"123"和"java.lang.String"等等常量。
1.2 堆
这是被所有线程共享的一块内存区域,其中存放的是对象实例本身以及数组对象。也是GC最频繁的一块区域
HotSpot虚拟机为了把GC分代收集扩展至方法区,堆进一步划分为,
当JVM启动参数:-Xms20M -Xmx20M -Xmn20M
1.2.1 对象(堆里的存储单元)
堆的存储单元是一个对象,一个对象包含对象头和实例数据,对象头又包含Mark Word和元数据指针。
对象内存分配策略:
- 对象优先在Eden分配
- 大对象直接进入老年代
- 大 阈值 -XX:PretenureSizeThreshold=3145728(3M)
- 长期存活的对象将进入老年代
- 每熬过一次Minor GC,年龄就加1岁,-XX:MaxTenuringThreshold设置 default 15 ,CMS default=6
2.1.3 虚拟机栈
虚拟机栈主要包含局部变量表、操作数栈、动态链接和方法出口,局部变量表中存储着指向对象的引用。
2 垃圾回收GC
2.1 目标区域和实际
full? | 解释 | 时机 | |
---|---|---|---|
部分收集(Partial GC) | 新生代收集 (Minor GC/Young GC) | 只是新生代 | 创建新对象在Eden申请空间失败时 |
老年代收集 (Major GC/Old GC) | 只是老年代 只有CMS收集器会有单独收集老年代的行为 请注意“Major GC”这个说法现在有点混淆, 在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。 | ||
混合收集(Mixed GC) | 整个新生代以及部分老年代 只有G1收集器会有这种行为 | ||
整堆收集(Full GC) | 收集整个Java堆和方法区的垃圾收集 | Eden、survivor不足,判断老年代是否充足,不充足触发full gc |
-
堆内存中的不可用的对象
- 方法区中废弃的常量和无用的类进行回收
2.2 垃圾回收算法
2.2.0 标记算法
你是什么垃圾??判断垃圾方法包括引用计数法和可达性分析法
(1)引用计数法
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时表示可以回收。
优缺点:此方法简单,但无法解决对象相互循环引用的问题。因此Java虚拟机未采用该种方法
(2)可达性分析法
CMS、G1、ZGC等收集器使用此标记方法,在遍历GC roots需要
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,即为不可达对象,会被判定为可回收的对象。
GC Roots包括:
- * 虚拟机栈中局部变量
- * 本地方法栈中JNI(即native方法)
- * 方法区中类静态变量
- * 方法区中常量
2.2.1 标记-清除(Mark-Sweep)
大名鼎鼎的CMS就是这类算法啦。
实现原理:首先标记出需要回收的对象,标记完成后,统一回收掉被标记的对象,也可反过来标记存活的对象,统一回收未标记的对象。
标记过程就是对象是否属于垃圾的判定过程,这在前一节讲述垃圾对象标记判定算法时其实已经介绍过了。
优势:算法简单,原地实现
劣势:内存碎片,大量可回收对象时执行效率低
2.2.2 标记-复制
“半区复制”(Semispace Copying),它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优势:实现简单,运行高效
劣势:需要保留空闲内存,空间浪费。
新生代的回收器大部分都是用这个算法,且做了优化,不用浪费那么多,一般浪费1/10。
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。
2.2.3 标记-整理
不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
优势:不浪费空间,没有内存碎片
劣势:对象存活率较高时就要进行较多的移位操作,效率将会降低
2.2.4 分代收集
- 指的是针对不同分代的内存区域,采用不同的垃圾回收算法
- 年轻代一般采用复制算法,老年代则一般采用标记-整理算法
2.3 垃圾收集器
2.3.1 年轻代
采用上节中所说的复制算法
- Serial 复制
- ParNew 复制
- Parallel Scavenge 复制
2.3.2 老年代
- CMS 清除
- Serial Old 整理
- Parallel Old 整理
2.3.3 分代收集器
G1 Garbage First,JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。
基于Region的堆内存布局,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以按需扮演新生代的Eden空间、Survivor空间、老年代空间。Region大小:G1HeapRegionSize设定,取值范围为1MB~32MB。
收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
四个步骤:
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking):从GC Root开始进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。