第3章 垃圾收集器与内存分配策略
经过半个世纪的发展,目前内存的动态分配与内存回收技术已经相当成熟了 ,一切看起来已经进入“自动化”时代,那么学习垃圾收集器(GC)和内存分配的意义何在?
- 面临排查内存溢出,内存泄露
- 垃圾收集成为系统达到更高并发量的瓶颈
当面临上述两种情况时,就需要对垃圾收集和内存分配进行必要的监控和调节。
Java内存运行时各个区域内存分配和回收的情况
程序计数器,虚拟机栈,本地方法栈:确定的内存分配、回收
由于这三个区域随线程而生,随线程而灭
,栈中的栈帧随着方法的进入和退出而出栈和入栈,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,在方法或者线程结束时,内存自然完成回收。
java堆,方法区:动态的内存分配、回收
一个接口中多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也可能不一样,只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配与回收都是动态的。
如何确定对象的“存活”与“死亡”
垃圾收集器在对堆进行会收前,第一件事就是要判断哪些对象还“存活”,哪些已经“死亡”。
* 引用计数算法
定义:在对象中增加一个引用计数器
,每当有一个地方引用它时,计数器加1,当引用失效时,计数器减1,减为0时,表示该对象不可能再被使用。
- 优点:实现简单,判定效率高
- 缺点:难以解决对象间循环引用的问题
*可达性分析算法
定义:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链
,当一个对象到GC Roots没有任何引用链相连时,则证明此对对象是不可用的。
在java语言中,可作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(一般说的Native方法)引用的对象;
再谈引用
狭隘定义:reference类型的数据中存储的数值是另外一块内存的起始地址
,则称这块内存代表着一个引用。
- 一个对象的状态:
被引用
和没有被引用
。
在jdk1.2以后,对引用的概念进行了扩充,将引用分为强引用
,软引用
,弱引用
,虚引用
4种。引用强度依次减弱。
-
强引用:程序代码中
普遍存在
的,“Object obj = new Object()”
,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
。 -
软引用:描述一些
还有用但非必需
的对象,对于软引用关联的对象,在内存溢出异常前
,被纳入垃圾收集器的范围内进行第二次回收,jdk1.2以后,SoftReference实现软引用。 -
软引用:描述
非必需
的对象,但强度比软引用更弱,只能存活于下一次垃圾收集发生之前
,无论当前内存是否足够,都会回收掉被弱引用关联的对象。jdk1.2以后,WeakReference实现软引用。 -
虚引用:最弱的一种引用关系。一个对象是否有虚引用,完全不会对其生存时间产生影响,并且不能通过虚引用创建一个实例,存在的唯一目的是在
垃圾收集时得到一个系统通知
。jdk1.2以后,PhantomReference实现软引用。
对象:生存还是死亡
当对一个对象进行可达性分析后,如果该对象不存在一条连接GC Roots的引用链,那么该对象可能存在一次自救的机会,来确定是否被回收。
-
如果该对象没有覆盖finalize()方法,或者虚拟机已经调用过finalize()方法。没有自救机会,直接确定被回收。
-
如果该对象覆盖finalize()方法,且虚拟机没有调用过finalize()方法。此时, 可以执行fianlize()方法自救。该对象会被放在一个
F-Queue
的队列中,由低优先级的Finalizer线程
执行(虚拟机只会触发finalize(),而不等待方法执行结束),如果该对象可以重新与引用链上的对象建立连接(把自己赋值给某个类变量,成员变量),那么,该对象就会移除“即将回收”集合。否则,该对象就真的确定会被回收。
回收方法区
在堆中,尤其时新生代中,进行一次垃圾收集一般可以回收70%-95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集:废弃常量,无用的类
- 废弃常量:没有引用指向该常量;
- 无用的类:该类所有的实例都被回收了;加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
* 标记-清除算法:最基础的算法
首先标记出所有需要被回收的对象,在标记完成后统一回收被标记的对象。
- 不足:
效率太低
:标记和清除的效率不高;
空间问题
:标记清除会产生大量不连续的内存碎片,后续在给较大对象分配内存时,无法找到足够连续内存而不得不触发另一次垃圾收集。
* 复制算法:适用于存活率较低的对象(新生代)
内存按容量分为大小相等的两块,当一块的内存使用完,将还存活的对象按内存顺序复制到另一块的上,然后清理掉已经使用过的内存。
- 优点:实现简单,运行高效
- 缺点:内存缩减一半
主要应用在java堆中新生代
的垃圾收集上。
新生代:内存分为一块较大的Eden空间和两块较小的Survivor空间(From Survivor和To Survivor),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另一块Survivor上,清理掉刚才使用过的空间。当Survivor空间不够时,对象直接进入老年代(大对象可以直接进入老年代)。
*标记-整理算法:适用于存活率较高的对象(老年代)
不需要进行较多的复制操作,更关键的是没有浪费50%的空间,标记过程与“标记-清除”算法一样,但后续操作不是对可回收对象进行清理,而是让所有存活的对象向一端移动,然后清理掉边界意外的内存。
*分代收集算法
根据对象的存活时间的不同,对新生代
采取“复制”
算法,老年代
采取”标记-清除“
或者“标记-整理”
进行回收。
HotSpot的算法实现
- 枚举根节点
- 安全点
- 安全区域
垃圾收集器(暂时跳过)
- 新生代:Serial,ParNew,Parallel Scavenge
- 老年代:CMS,Serial Old(MSC),Parallel Old
二者皆有:G1
内存分配与回收策略
对象主要分配在Eden区上,少数情况直接分配到老年代。分配的细节取决于使用哪一种垃圾收集器组合和虚拟机中与内存相关的设置。
*对象优先在Eden中分配
大多数情况下,对象在新生代Eden中分配,当Eden区中没有足够空间进行分配时,虚拟机将发起一次Minor GC
- Minor GC :发生在新生代的垃圾收集动作,新生代对象朝生夕灭,发生频繁,回收速度块
- Major GC: 发生在老年代的垃圾收集动作,经常会伴随至少一次的Minor GC(Parallel Scavenge除外),速度比Minor GC慢10倍以上。
*大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象(长字符串、数组)
*长期存活的对象将进入老年代
虚拟机给每个对象定义一个年龄计数器,对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15)时,就会被晋升到老年代。
*动态对象年龄判断:为了适应不同程序的内存状况
虚拟机并不要求对象的年龄达到了MaxTenuringThreshold才能晋升老年代,当Survivor空间中相同年龄对象的总和大于Survivor空间一半,大于或者等于该年龄的对象可以直接进入老年代。
*空间分配担保
在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,Minor GC可以确保安全,否则,虚拟机会查看HandlePromotionFailor设置值是否允许担保失败。如果允许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC(取平均值进行比较仍是一种比较冒险的手段,可能某次Minor GC后存活的对象特别多,远远高于平均值,依然会导致担保失败,仍需重新发起一次Full GC),如果小于,或者HandlePromotionFailor设置不允许,则这时要改为进行一次Full GC。