JVM——垃圾收集器与内存分配
1. 概述#
本章讲述的是 JVM 的内存分配和回收
首先我们清楚,Java 的内存区域主要有五个:程序计数器,虚拟机栈,本地方法栈,堆和方法区。然而前三个都是线程私有的,随着线程的创建而创建,消亡而消亡,栈中每一个栈帧分配的大小也是确定的(类加载完成后也就确定),即方法结束或者线程消亡的时候内存也就跟着回收了,不必担心太多。
但是,堆和方法区的分配却具有明显的不确定性,因为你必须在程序运行期间才能知道具体分配多少内存,所以 Java 的垃圾收集器关注的就是堆和方法区!
2. 死亡对象#
ok,既然要做垃圾回收,那么其对象就是那些已经死亡的对象,ok 那我们如何判断对象死亡?
2.1 引用计数法
很简单,就是给每个对象增加一个引用计数器,如果其他地方有引用这个对象,那么就将其计数器加一;引用失效的时候就减一;任何时候如果垃圾收集器发现某对象的引用计数器为 0,就代表这个对象已经死亡,可以回收!优点:简单高效
缺点:计数器占用了一些额外的内存,重点是需要大量的额外处理才能保证正确的工作,譬如说循环引用问题!
既然谈到了引用我们不妨说下 Java 中的引用概念,主要有四种:
- 强引用,普遍存在的,例如最经常的 “Object obj = new Object ();” 代码,即只要强引用关系还在,垃圾回收器就永远不会回收这些被引用的对象
- 软引用,描述一些还有用但非必须的对象,在发生 oom 之前,会对这些软引用的对象进行二次回收,如果还不行只好抛出 oom
- 弱引用,强度更弱, 但凡让垃圾回收器碰到必定回收!
- 虚引用,最弱了,根本不影响对象的生存情况,唯一用来将这个对象回收时会受到一个系统通知
2.2 可达性分析法
可达性分析算法是目前主流的判断对象死亡的算法,此算法主要是以一系列称为 “GC Roots” 的跟对象作为起点,向下遍历搜索走过的路径,这些路径被称为引用链!如果某个对象到 GC Roots 没有任何引用链相连,那么称这个对象不可达!即判定其为死亡!GC Roots
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 本地方法栈中 JNT(即常说的 native 方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
3. 真正死亡?#
难道对象在可达性分析算法中不可达,就说明其一定必须死亡吗?
不一定!不可达对象还处于 “缓刑” 阶段,即要真正判断一个对象死亡,至少要经历两次标记过程!第一次标记:当对象不可达时进行一次标记
筛选:从这些标记中进行筛选,检查是否有必要执行 finalize 方法!如果重写了并且没有被调用过,ok,将放置在一个 F-queue 队列,然后由虚拟机的一条 finalizer 低优先级线程去执行它,而且还不一定执行成功。否则就是不必要执行
第二次标记:从 F-queue 队列中检查,如果对象成功自救,ok 你活了下来,否则真正回收
4. 回收方法区#
垃圾收集器的工作区域大部分都集中在堆,对于方法区的回收也有一定的规范,只不过性价比不高,但还是要强调一下
收集的对象是:废弃的常量和不再使用的类型!!在大量使用反射,动态代理等字节码的框架,通常都需要 Java 虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力!
5. 垃圾收集算法#
5.1 分代收集理论#
- 弱分代假说
- 强分代假说
- 跨代引用假说
5.2 标记 - 清除算法#
简单易实现
- 执行效率不稳定,如果存在大量被回收对象则需要进行长时间的标记
- 空间利用率低下,因为标记清除这种算法会产生大量的内存碎片,导致之后如果有较大对象在新生代分配不到内存而提前触发一次垃圾收集
5.3 标记 - 整理算法#
:空间利用率高,不会产生内存碎片
:移动负担较重,需要更新存活对象引用,甚至或造成长时间用户线程的”Stop The World” 但很短,最关键的是低停顿!
5.4 复制算法#
:实现简单,运行高效
:浪费空间,毕竟另一半 survivor 区是不可用的。
IBM 公司之前做过研究,提出新生代中有 98% 的对象都熬不过第一轮收集的,所以没必要将新生代的内存空间划分为一比一。现在 HotSpot 虚拟机大都也不会采取这种划分,更多的是将 eden 区和 survivor 区划分为 8:1
6. 垃圾收集器#
话不多说先上一副图,如果两个收集器之间有连线,说明他们之间可以搭配使用
6.1 Serial 收集器#
最基础的一款垃圾收集器,是一款单线程的垃圾收集器,他强调在垃圾收集的过程中,必须暂停其他线程的工作,直到收集结束。
—————————————Serial/Serial Old————————-
优点:简单而高效(单线程嘛,没有线程交互的开销),消耗内存少
缺点:”Stop The World” 时间较长,用户体验差!
虽然用户体验恶劣,早期 hotspot 虚拟机设计者表示十分委屈:“你妈妈在给你打扫房间的时候,肯定也会让你老老实实的坐在凳子上,如果她一边打扫,你一边扔纸屑,什么时候能打扫完!!!”
6.2 ParNew 收集器#
其实就是 Serial 收集器的多线程并行版本,在单核处理器下并不比 Serial 收集器有多大优势,但是在多核环境下有着明显的优势
—————————————ParNew/Serial Old——————————–
6.3 Parallel Scavenge 收集器#
和 ParNew 非常相似,但是关注点不同,ParNew 和 CMS 收集器重点关注如何缩短垃圾收集时用户线程的停顿时间,而这款主要关注吞吐量!
用户执行代码的时间
吞吐量 = ——————————————————————————————————
用户执行代码的时间 + 垃圾收集的时间
6.4 CMS 收集器#
CMS 收集器是一款以获取最短停顿时间的垃圾收集器,主要包括四个过程
- 初始标记:很快
- 并发标记 :与用户线程并发执行
- 重新标记 :修正并发标记期间用户线程的不一致性
- 并发清除 :与用户线程并发执行
优点:第一个真正意义上的并发收集器,虽然也需要”Stop The World” 但很短,最关键的是低停顿!!!
缺点:
- CMS 对处理器资源十分敏感
- 无法处理浮动垃圾,有可能出现失败而导致一次完全的 Full GC 出现!
- 内存碎片严重,会给内存分配带来巨大压力!
6.5 Garbage First 收集器#
同样包括四个过程:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
G1 收集器的优势:
并行与并发
分代收集
空间整理 (标记整理算法,复制算法)
可预测的停顿(G1 处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒)
使用 G1 收集器时,Java 堆的内存布局是整个规划为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region 的集合。
G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在真个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的又来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽量可能高的回收效率
本作品采用《CC 协议》,转载必须注明作者和本文链接