前言:
- 垃圾回收算法的思想是之前就有学习,但是一直没有进行文字的整理,希望从这篇开始,对所看书的每个章节都能做一个类似于小结的整理。
- 今跟随多年的小6p投坑“了断”了。。。
参考:
- 《深入理解Java虚拟机》第二版——周志明
- JVM调优总结——和你在一起
运行时数据区
谈到JVM的内存管理就必须了解它的运行时数据区,了解下图中各区域的功能和数据存储的类别十分必要。
运行时内存区域程序计数器
一块较小的内存空间,可以看做当前线程执行的字节码的行号指示器。
- 特点:
- 线程私有——每个线程都有独立的程序计数器;
- 运行时数据区中唯一一个没有OutOfMemoryError的区域;
- 功能
- 字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、跳转、线程恢复、异常处理等基础功能都需要依赖这个计数器;
- 上面提到的线程恢复需要依赖程序计数器,是由于Java虚拟机是通过线程轮流切换并分配执行时间的方式来实现的,线程私有的程序计数器可以保证线程切换后能恢复到正确的位置;
- 执行Java方法时,计数器记录的是正在执行的虚拟机字节码指令的地址,但是当执行Native方法时,计数区域为空(Undefined)。
Java虚拟机栈
我们常说的将Java内存区理解为“堆”和“栈”,其中所说的栈就是Java虚拟机栈中的局部变量表部分
- 特点:
- 线程私有,生命周期与线程相同;
- 异常:
- StackOverflowError异常:线程请求的栈深度大于栈所允许的深度;
- OutOfMemmoryError异常:如果虚拟机支持动态扩展,当扩展时也无法申请到足够的内存,会抛出OutOfMemmoryError异常;
- 存储:
- 每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用到结束,也对应着栈帧在Java虚拟机栈中的进栈到出栈;
- 局部变量表:存放了各种编译器可知基本数据类型(boolean、byte、char、int、short、long、float、double)、引用类型(reference,可能是只想对象的地址或者句柄等与对象相关的位置)和returnAdress类型(指向字节码指令的地址);
本地方法区
使用Native方法服务,可以由虚拟机自由实现
- 线程私有
- 本地方发区与Java虚拟机栈作用相似,但是使用Native方法服务,也会有StackOverflowError和OutOfMemmoryError异常;
Java堆
Java虚拟机中所管理的内存最大的一块,几乎所有的对象实例以及数组都在这里分配内存
- 特点:
- 线程共享,唯一目的就是存放对象实例;
- 垃圾回收的主战场(又称"GC堆",垃圾回收部分将在后续展开);
- 可以处在物理上不连续,但是逻辑上连续的内存空间中;
- 支持堆扩展的虚拟机中,堆中没有内存完成实例分配,并且堆也无法扩展时,会有OutOfMemoryError异常;
方法区
(Non-Heap,非堆)
各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、变量、静态变量、即时编译后的代码等数据
-
特点:
- 可以处在物理上不连续,但是逻辑上连续的内存空间中;
- 可以不实现垃圾回收
-
存储的数据:
- 用于存储已被虚拟机加载的类信息、变量、静态变量、即时编译后的代码等数据;
运行时常量池
用于存放编译期生成的各种字面量和符号引用
对象存活判断法
如何判断对象已死?
引用计数法
给对象添加一个引用计数器,每个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象视为不能被使用;
- 缺点——很难解决对象之间相互循环引用的问题,所以主流的Java虚拟机不使用该算法来管理内存;
public class Ref{
private Object instance;
...
}
public static void main(){
Ref a = new Ref();
Ref b = new Ref();
a.instance = b;
b.instance = a;
a = null; // a引用失效,但是堆中的“a实例”仍然在引用b
b = null; // b引用失效,但是堆中的“b实例”仍然在引用a
/**
* 此时执行GC,虽然a、b引用都已经失效;
* 但是却在互相引用,无法完成回收
*/
System.gc();
}
可达性分析算法
- 在主流实现中都是通过可达性分析来判定对象是否存活:
- 以可看做GC Roots的对象作为起点,从这些节点作为起点向下搜索,搜索路径成为引用链,引用链不经过的节点都视为不可达(即该对象可回收);
- GC Roots的对象
- 虚拟机栈中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- Native方法引用的对象
- 在可达性分析中被视为不可达的对象 // TODO
引用的类型
- 强引用:强引用不会被垃圾收集器回收;
- 软引用:用于描述一些有用但不是必需的对象,在发生内存溢出异常之前,会列入回收范围;
- 弱引用:只能存活到下一个垃圾收集发生之前;
- 虚引用:最弱的引用,不会对其对象实例生存时间造成影响,唯一目的是在这个对象被回收时收到一个系统通知;
常见的垃圾回收算法思想
标记—清除算法
标记—清除算法是最基本的收集算法,分为“标记”、“清除”两个阶段。首先,会标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
示意图 // TODO
缺陷:
- 效率问题,标记和清除两个过程效率都不高
- 空间问题,在标记—清除后会产生大量不连续的内存碎片,会导致需要分配较大对象时无法找到足够的连续内存,导致新一轮垃圾回收触发;
复制算法
复制算法的核心在于将内存分成了容量大小相等的两块,每次只使用其中的一块,当这一块的内存用完时,触发回收,将该半区还存活的对象复制到另一半区,并对它进行整理,不会出现大量不连续的内存碎片;
-
示意图 // TODO
-
优点:实现简单,运行高效,不会有大量不连续的内存碎片
-
缺点:将一个回收周期的可用内存减小到了原有的一半,也是一种对内存空间的浪费。
标记—整理算法
正如标记—清除算法,标记—整理算法也分为对应的两个阶段——“标记”和“整理”;
-
示意图 // TODO
-
标记阶段:标记所有需要回收的对象;
-
整理阶段:所有的存活对象都向一端移动,清理掉端边界以外的内存
分代收集算法
目前商业虚拟机的主流收集算法
基于不同的对象的生命周期是不一样的事实,根据对象的存活周期不同,把堆分为新生代和老年代,并根据特点采用不同的收集算法,避免每次回收都会遍历所有存活对象,而导致时间的浪费;
- 新生代:由于新生代中的对象“朝生夕死”,每次垃圾收集时,大部分对象都会死去,只存留少部分存活,采用的是“复制算法”;新生代中存活的对象在一次垃圾收集结束后,会进入到另一半区(若复制算法划分内存数量>)
- 新生代一般会划分为三个区:一个Eden区和两个Survivor区,新的对象生成会在Eden申请空间,当Eden中存活的对象在一次垃圾收集结束后,会进入到Survivor区,当Survivor区满时,该Survivor区中的存活对象会复制到另一个Survivor区;Survivor区不分前后,Survivor-A(A、B仅作标记区分)中会有来自Survivor-B的对象,同样Survivor-B中也会有来自Survivor-A的对象;当对象在所有的Survivor区(Survivor区可能会超过2个)都观光结束后仍然存活时,就可以前往老年代”度假“了;
- 老年代:
- 在新生代经历了N次垃圾回收后仍然存活的对象,就会来到老年代,因此来到老年代的对象一般生命周期都比较长,回收率相对较高;如果仍然采取新生代的“复制算法”显然是不合理的(没有那么多空间进行分配担保),在这里常用的回收算法是“标记-清除”和“标记-整理“算法;
- 持久代:
- 用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置
如何触发垃圾回收
由于对对象进行了分代处理,因此回收的区域、时间都不一样;GC有两种类型:Scavenge GC和Full GC
- Scavenge GC:当新的对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC
- Full GC:对整个堆进行整理,包括新生代(Young)、老年代(Tenured)以及持久代(Perm)。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数;
- 触发原因
- 年老代(Tenured)被写满
- 持久代(Perm)被写满
- System.gc()被显示调用
- 上一次GC之后Heap的各域分配策略动态变化
- 触发原因
网友评论