本章要探究的问题 :
GC在回收内存时 :
- 怎么判断哪些内存需要回收 ?
- 什么时候回收?
在几个线程私有的运行时区域:
- 虚拟机栈
- 程序计数器
- 本地方法栈
它们的内存分配和回收大多都具有确定性,随着线程的创建而产生,随着线程的停止而被回收。栈帧中的内存大小基本在类的结构确定下来时就已知。
而在线程共有的 Java堆(Heap)
和 方法区(Class(Method) Area)
这两个区域则不同:
比如,一个接口有不同的实现类(类的信息在方法区中),这几个实现类的内存大小肯定不一,没法在运行前就已知需要多大的内存,只有在运行期间才知道创建的对象的大小。
一,哪些内存需要回收?
在知道哪些内存需要回收之前,我们要知道怎么判断一个对象是否还存活,当它不再存活时,就回收它。
而 引用计数算法
就是用来判断对象是否存活的一个算法。
1,引用计数算法(Reference Counting)
算法描述:给对象添加一个引用计数器,当有一个地方引用了它,计数器+1,当引用失效,计数器-1,在任何时刻,计数器为0时此对象将不能再被使用。
引用计数法在大多数情况下表现都不错,也有被很多公司采用的应用案例。但是在JVM中并没有采用这种算法,原因是:无法解决对象之间存在相互引用的问题。
public class Person {
Object instance = null;
public static void main(String[] args) {
Person a = new Person();
Person b = new Person();
a.instance = b;
b.instance = a;
a = null;
b = null;// 正常情况下在这里GC就会把a,b回收掉
}
}
正常情况下在执行11-12行代码时,JVM的GC会把a,b两个对象回收,但是在引用计数算法的情况下:
- 执行
a=null
时,a的引用计数器值为1,因为b对象在引用它。 - 执行
b=null
时,b的引用计数器值为1,因为a对象在引用它。
2,可达性分析(Reachability Analysis)算法
在Java语言中是通过可达性分析来判断对象是否存活。
算法描述 : 通过一系列的 GC Roots
作为起始点,从这些起始点开始向下搜索,能搜索的到的对象说明其可用,不会被GC回收掉,搜索所走过的路径称为 引用链(Reference Chain)
。相反,如果一个对象没有到达GC Roots的路径,则说明它不可用,被判定为可被GC回收的对象。
如图 : 1区域的对象虽然互相关联,但是它们不可到达GC Roots,所以他们会被回收掉,而2区域的对象与GC Roots之间是有可到达路径的,所以它们不会被回收。
什么是GC Roots ?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类的静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
这些都可作为GC Roots.
3,什么是引用(Reference)
我们在上面的 引用计数算法
和 可达性分析
中,都提到了 对象之间的引用
关系。
在Java1.2之前,关于 引用
的定义 :
如果
reference
类型的数据存储的数值代表的是另一块内存的起始地址,就说这块内存代表一个引用。
JDK1,2之后,又引入了 强引用
, 软引用
, 弱引用
, 虚引用
,这四个概念,并且这四种表现的引用关系越来越弱。
- 强引用(Strong Reference) :
例:
Object o = new Object();
只要强引用还在,GC永远不会回收掉被引用的对象。
- 软引用(Soft Reference) :
有用,但非必须,在将要发生内存溢出时,会把 软引用
的对象回收掉,如果内存依然不够用,则抛出OOM异常。
- 弱引用(Weak Reference):
非必需对象,只要GC发生了垃圾回收,不管此时内存是否充足, 弱引用
的对象都会被回收掉。
- 虚引用(Phantom Reference):
- 最弱的引用关系
- 无法通过虚引用构造市实例。
- 唯一的作用就是在虚引用关联的对象被GC回收掉时,可以接受到一个信号。
4,如何判断一个对象可回收(已死)?
一个对象仅仅通过上面说的可达性分析看它没有与GC ROOTS关联来判定这个对象是否可被回收是不够的。
一个对象要经过下面一段判断过程来判断它是否要被回收(建议收藏(^__^) 嘻嘻……):
5,方法区的回收
上面我们说的是存在于Java堆中的对象的回收,但其实在方法区还要回收以下东西:
① 回收废弃常量
假如常量池中有一个字符串 “abc” ,但是系统中没有一个String 对象指向它,也就是这个常量没有被引用,
当GC在回收时会回收此字面量。
② 回收废弃的类(无用的类)
- 该类的实例都已被回收,Java堆中不存在任何该类的实例。
- 加载该类的ClassLoader已经被回收。
- 该类的Java.lang.class对象没有被引用(在反射中会被用到这点)。
③ 方法区的回收策略:
GC在回收方法区时会采用一下2种方式:
- 标记-整理
- 标记-清除
二,如何回收?
GC在回收内存时会采用多种垃圾收集算法,这些算法各有优劣。
1.标记-清除(Mark-Sweep)算法
此算法是最基础也是最古老的垃圾回收算法,该算法主要经过2个过程
① 算法描述
- 标记阶段:经过如何判断一个对象可被回收所述,对可被回收的对象进行标记。
- 清除阶段:将被标记的对象统一回收。
② 算法缺陷
- 效率问题:此种算法标记和清除的效率都不高。
- 标记清除后产生大量不连续的内存空间碎片。
2.复制(Copying)算法
复制算法针对效率问题进行了优化,它将内存区域划分为2块,每次只使用其中一块。
- 活动区域
- 空闲区域
① 算法描述
如图:
- 回收前 : 内存被划分为左右两侧区域,右侧为空闲区域,暂时不使用它
- 回收时 : 将左侧要被回收的部分(黑色) 回收掉,然后将4个存活对象(淡灰色)移动到右侧的空闲区域,并且做了2件事
- 将移动到空闲区域的存活对象按内存地址进行排列。
- 将存活对象指向的旧地址指向新内存地址。
- 回收后 : 原先的右侧空闲区域变为活动区域,左侧的活动区域变为空闲区域。
左右两侧的区域状态在每一次回收后都来回转换…
② 算法缺陷
- 很显然,这种算法浪费了一般内存。
- 当活动区域的100%的对象都还在活跃,那么在回收时需要将全部的对象复制到右侧的空闲区域,此时的效率就很低。
③ 算法应用
IBM公司经研究表明,Java堆新生代种的对象98%是 ‘朝生夕死’ 的对象,比如临时变量等作用域很少的对象。
所以现在的虚拟机并不会按照 1:1的比例划分两个区域。
现在的JVM虚拟机中,将新生代划分为一块 Eden
区域,和2块较小的 Survivor
区域(from ,to区)。
每次使用Eden区和1块Survivor区(from区)最为活动区域,当发生内存回收时,将这2块内存中的存活对象复制到另一块Survivor区(to区)。
在 HotSpot
虚拟机中,Eden区和Survivor的划分是: 8: 1,这样,活动区域占新生代的 (8+1)/10 *100% = 90%,只有10%的内存浪费。
老年代 : 当将存活对象从活动区域(Eden,from) 复制到 to区时,如果to区不够用,则将剩下的存活对象放到老年代。
3.标记-整理算法
标记-整理主要运用于老年代中。
① 算法描述
此算法与标记-清除算法类似,也是经历2个阶段:
- 标记阶段:此阶段于标记-清除中的标记阶段相同,都是标记出要回收的对象。
- 整理阶段:把所有存活的对像,按内存地址排列移动到内存区域的一端,将端边界以外的区域进行回收。
② 算法应用
由于老年代的特点,对象的存活率较高,没有额外的空闲区域,所以 老年代适用 标记-清除和标记-整理算法。
Reference:
深入理解Java虚拟机