一、总览
任何语言都需要对内存进行管理,就是对不再使用的内存进行回收处理。要回收一个被使用过的内存我们要知道一下三点:
1、哪些内存需要回收?
2、什么时候回收?
3、如何回收?
其实如今内存分配与回收的的机制已经相当成熟,可是实现“自动化”,不需要像C++一样需要自己对内存进行回收,此处仍要研究java内存回收机制的原因:一是为了在遇到如内存溢出、内存泄露等问题时方便排查;二是这是实习或校招面试的热点问题。
二、哪些内存需要回收(对象)
要判断要回收哪些内存首先要知道哪些对象不再使用。下面是两种判断是否存活的算法:
1、引用计数法(java虚拟机未采用)
思想:在对象中添加一个引用计数器,每当有一个地方引用此对象时,计数器值加一;当引用用失效时,计数器值减一。计数器值为零的对象就是不可用的。
主流java虚拟机不采用的原因:有很多例外情况要考虑,要配合大量额外处理才能保证正确工作。eg:简单的引用计数很难解决对象之间相互循环引用的问题。
2、可达性分析算法(被采用,java、C#)
思路简介:通过一系列“GC roots”的根对象作为起始节点集合,根据引用关系构建对象树,若某个对象不存在在任何GC roots的节点图中,则说明此对象是不可能再使用的。所以此算法首先要确定哪些对象可以作为节点图的root节点。
固定作为GC roots的对象包括以下几种:
-
虚拟机栈中引用的对象,eg:各个线程被调用方法堆栈中使用到的参数、局部变量、临时变量
-
在方法区中类静态属性引用的对象,eg:java类引的引用类型静态变量
-
在方法区中常量引用的对象,rg:字符串常量池(String)
-
本地方法栈中JNI(通常所说的Native方法)引用的对象
-
java虚拟机的内部引用,eg:基本数据类型对应的class对象,一些常驻的异常对象,系统类加载器
-
所有被同步锁(synchronized关键字)持有的对象
-
反应java虚拟机内部情况的JMXBean、本地缓存等
除了上述之外还可能根据用户所选的垃圾收集器以及当前回收的内存区域不同而引入一些其他对象“临时性”的加入。 eg:分代回收和局部回收,区域之间不是封闭孤立的,需要将关联不同区域的对象也加入GC roots集合中。
3、引用
上面两个判断对象是否生存的算法都是基于引用的关系。 JDK1.2之前的引用是传统的定义:引用类型变量存储的是一个对象的起始地址,就说该变量为此对象的引用
java的四种类型的引用:
- 强引用:传统引用第一,指的是我们平时普遍使用的引用,Object obj=new Object() 这种引用关系。任何情况下,只要强引用关系还存在,垃圾回收期就永远不会回收掉被引用的对象,仅仅当对象和引用不关联时才会回收。
- 软引用:用来描述一些有用、但非必须的对象,只被软引用关联的对象,在系统即将发生溢出异常前,进行对软引用关联对象的回收,回收后内存依旧不够才会抛出内存溢出异常。使用SoftReference类来实现软引用。eg:在缓存中使用软引用
- 弱引用:描述非必须的对象,但关系更弱,软引用关联的对象在下一次垃圾收集发生时便一定会被回收。使用WeakReference类来实现弱引用。
- 虚引用(幽灵引用、幻影引用):最弱的引用,一个对象是否有虚引用,不会影响它的生存时间,也无法通过虚引用获取一个对象的实例。 为对象设置虚引用的目的只是为了能在对象被收集器回收时收到一个系统通知。使用PhantomReference类来实现虚引用。
4、对象是否生存?
实际中并不会因为在可达性分析算法判定为不可达对象便直接进行回收,只是此对象处于“缓刑”阶段,要真正对一个对象进行回收要进行至少两次标记过程:
1、可达性分析判定为不可达时进行第一次标记。
2、标记过程是根据对象的finalize()方法,若对象重写了finalize()方法,且未被虚拟机调用则此对象便会被放入一个名为F-Queue的队列之中。否则直接进行第二个标记(对象被回收)。被放入F-Queue队列中的对象还有一次拯救自己的机会,可以在finalize()方法中将给此对象设置一个引用,这样对象便不会在这次回收中被回收,但finalize()方法仅会执行一次。
5、总结
上面的内容回答了哪些内存需要回收,也就是那些“已死亡”的对象所占用的内存需要进行回收,判断对象是否死亡采用可达性分析算法+至少两次标记‘’。
三、什么时候回收?
1、程序员手动调用gc
2、当内存区域即将要溢出时
四、怎么回收?
1、分代收集
当前的商业虚拟机的垃圾回收器,大多数遵循“分代收集”。分代收集有三个假说:
1.弱分代假说:绝大多数对象都是朝生夕灭的
2.强分代假说:熬过越多次垃圾收集过程的对象就难以消亡
3.跨代引用假说:跨代引用相对于同代引用仅占小数
新生代:对象大都是朝生夕灭,难以熬过垃圾收集过程。
老年代:经历多次垃圾收集都未消亡的对象,集中到老年代。(以较低的频率回收)
因为存在跨代 引用不能简单的进行划分为新生代与老年代,使用不同的收集算法单独对这两个分代回收内存。但新生代的对象完全有可能被老生代所引用,反之也存在。为了避免对新生代GC时要额外遍历老年代,要在新生代额外建立一个全局数据结构(被称为“记忆集”),这个结构把老年嗲划分为若干小块,当GC新生代时只对包含跨代引用的老年代块进行GC Roots扫描。
2、收集算法
2.1标记-清除算法
首先标记处所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来标记存活对象,统计回收未被标记的对象。是其他垃圾回收算法的基础。
缺点:
效率不稳定,若java堆中包含大量对象,且其中大部分需要回收,需要进行大量的标记和清除操作,效率随数量增长逐渐降低
内存空间碎片化。
2.2标记-复制算法(用于新生代)
将内存划分为两部分,每次给对象分配内存只使用其中一部分,当这一块内存块用完了,把少量存活的对象复制到另一部分内存,然后对前一内存块一次性清除。
实际优化的复制策略:Appel式回收(基于新生代每次有98%的对象熬不过第一轮收集):把新生代划分为一个较大的Eden空间和两个较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生GC时把Eden和Survivor中仍存活的对象复制到另一块Survivor,然后直接清理掉之前的Eden和Survivor。HotSpot虚拟机默认的Eden:Survivor=8:1.
最坏情况处理:当Survivor不足以容纳一次GC之后存活的对象,需要依赖其他内存区域(大多是老生代)进行分配担保。
2.3标记-整理算法(用于老生代)
思想:把仍存活的对象移动到一边界之内,把边界外的对象一次性清除。对象移动操作需要全程暂停用户应用程序才能进行,被描述为“stop the word”。
优点:内存不存在碎片化
缺点:移动对象需要停顿
从整体来看此种方式会使总的吞吐量提升。
“和稀泥式”方案:让虚拟机大多数时间采用标记-清楚算法,暂时容忍内存碎片的存在,知道内存碎片化程度大到影响对象分配时,采用一次标记-整理算法收集一次。