五. 没有包治百病的通用内存回收算法
现代面向对象编程的方式,向程序员屏蔽了背后内存分配和回收的细节。程序员一半不会考虑内存效率问题,而把中心放在业务逻辑上。这种方式,决定了程序员的使用内存的习惯:用的时候分配,不用的时候赋Null值。所以很多对象,尤其是临时对象,寿命都很短,在使用完成后,就可以被回收了。不过,也有很多对象,生命周期较长。上文中提到,GC的回收算法,是和当前对象的数目成线性关系,对象越多,效率约低下。
因此,对生命周期不一样的所有对象,均一视同仁,势必效率低下,不是很好的做法。
六. 引入次代概念
一切都因为效率。
实际上,GC的设计者,根据对象的活跃程度,把对象分为了:青年代和老年代。针对青年代和老年代使用不同算法,在不同的时机收集,来提升整体效率。如下图
- Eden区是分配对象的区域,这里是对象的出生地。
- Survivor(S0和S1)是minor gc后,存储存活对象的区域。
- Eden以及Survivor(上图中的S0和S1)共同组成New Generatiton(或者叫Young space,青年代)
- Old Generationd(老年代)区域存储长时间存活的对象。
七 . 内存回收算法
GC的回收流程简述如下:
- 通过GCRoot和可达链,标记出堆中活着的对象;
- 定期调整活对象的位置:把青年代里活的很长的对象移动到老年代;
- 释放死对象占用的资源:青年代条件满足随时收集,老年代满了才去收集;
- 在青年代区域(Young space)里回收,叫做minor GC;
- 在老年代区域(Old Generationd)里回收,叫做Major GC;
- 全部区域回收,叫Full GC。
青年代的GC算法有以下三种:
- Serial:简单高效的收集器;
- ParallelScavenge:采用Copy算法的并行收集器(吞吐量优先的);
- ParNew:Serial收集器的多线程版本。
老年代的GC有以下三种:
- Serial Old:Serial的老年代版本;
- Parallel Old:Parallel Scavenge的老年代版本;
- CMS:并发低停顿收集器(重视相应速度);
下面,已Serial为例,描述一下垃圾回收的过程:
回收前
回收后:
三个要点:
1. Eden区域中,非活动对象被释放,活动对象迁移到Survivor Spaces中,被标记为to的区域;
2. Survivor Spaces区域中,被标记为from区域中非活动对象被释放,活动对象迁移到两个地方:
1)活动对象存活时间足够长,被迁移到Old Generation区域中;
2)活动对象存活时间不足够长,被迁移到Survivor Spaces中,被标记为to的区域;
3. Old Generation区域,在满的时候,才会触发Old Generation GC算法。
*注:
1. Eden是对象诞生的地方,每次GC完成之后,此区域为空;
2. Survivor Space有两个(S0,S1),青年代对象到老年代对象过度区域。每次GC完成之后,有一个区域为空,并且to/from标记交换。
八. 附录
1. Minor / Major GC触发的时机
下面是简单描述,具体触发时机,和两个区域选择的GC实现有关系:
- 对象在Eden Space完成内存分配时触发;
- 当Eden Space满,再创建对象,因为空间不足触发;
- 当Old Space被放满之后,进行Full GC
2. 根治循环引用
GC Root在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。
和引用计数方式不同,一个对象的内存是否释放,只和是否有相对应的Root直接或者间接引用相关(可达),与是否发生了循环引用,没有任何关系。
3. 关于内存碎片
4. 理解引用(句柄 和 指针)
引用有两种方式,句柄和指针:
1) 指针引用
指针非常容易理解,即在变量里保存的是对象的地址。
指针,直接把对象的地址,暴露给外部,相对来说不是特别安全。
如下图:
2) 句柄引用
句柄,实际上是在指针的基础上,引入了一个中间层。
系统维护了一个句柄池,真正的指针保存在句柄池里。
而引用中存放的是句柄(或者句柄地址),通过句柄,间接的到指针,从而对对象进行内存访问。