<<深入Java虚拟机>>-第三章-垃圾收集器与内存分配策略-学习笔记
垃圾收集
垃圾收集(Garbage Collection,GC),垃圾收集需要完成的三件事情。
- 哪些对象需要回收
- 什么时候回收
- 如何回收
如何确定对象已死(即不可能在被任何途径引用的对象)
-
引用计数算法
给每一个对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;计数器值为0的对象就是表示没有在被使用的。
引用计数算法的实现简单,判定效率也比较高,大部分情况下都是一个不错的算法。
Java语言中没有采用该算法来管理内存,最主要的原因是该算法没法解决对象之间的相互依赖的问题。
-
根搜索算法
算法的基本思路是通过一系列的名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
Java中,可以作为GC Roots的对象有以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
-
再谈引用
JDK1.2之前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种。
- 强引用就是在程序代码中普遍存在的,类似于“Object obj = new Object()”,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
- 软引用用来描述一些还有用,但不是必须的对象。如果内存空间足够,垃圾回收器就不会回收它,内存空间不够了就会回收该对象。
- 弱引用也是用来描述非必须对象的,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集之前。
- 虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用来取得一个对象的实例。设置虚引用的唯一目的就是希望这个对象被垃圾收集器回收时收到一个系统通知。(虚引用作用就是我们可以声明一个虚引用来引用我们感兴趣的对象,在垃圾收集器回收之前,垃圾收集器会把这个对象添加到referenceQueue,这样我们检测到referenceQueue中有我们感兴趣的对象时,就说明gc要回收该对象了,我们可以在gc回收之前做一些事情,比如记录日志等)
-
两次标记
要真正判断一个对象死亡,至少要经过两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相关联的引用链,那么它将被第一次标记并进行一次筛选,筛选的条件是判断对象是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法或者对象的finalize()方法已经被虚拟机执行过了,虚拟机将这两种情况都视为没有“必要执行”。
如果一个对象被判定为有必要执行finalize()方法,那么这个对象会被放置到一个名为F-Queue的队列中,虚拟机会自动创建一个低优先级的Finalizer线程去触发对象的finalize()方法(不承诺会等待它运行结束),如果在finalize方法中对象有了引用链建立连接(比如将this赋值给某个类的成员变量),则对象就会复活。
一个对象的finalize()方法只会被虚拟机执行一次,也就是说一个对象不可能自救两次。
finalize()方法很少使用,finalize()能做的关闭资源工作,使用try-catch-finally或其他方式都可以做得更好、更及时。
-
回收方法区
在方法区进行垃圾收集的“性价比”一般比较低,在堆中,尤其是在新生代中,常规应用进行一次垃圾回收可以回收70%-95%空间,而永久代的垃圾收集效率远低于此。
方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。
- 废弃常量的判定和堆中判定对象不可用类似。以常量池中字面量为例,比如一个字符串“abc”已经在常量池中,但是当前程序没用任何String对象引用常量池中的“abc”变量,这个时候发生垃圾回收,如果有必要的话,“abc”则会被回收掉。
- 无用的类:判定类为无用的类需要同时满足下面三个条件才能算是“无用的类”
- 该类的所有实例已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-
垃圾收集算法
-
标记-清除算法
最基础的收集算法是“标记-清除算法”,算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所被标记的对象。
这种算法的主要缺点有两个:一是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致程序运行过程中需要分配一块较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集工作。
-
复制算法
为了解决效率问题,一种称为“复制”的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将这一块上还存活的对象复制到另一块上面,然后在把已使用过的内存空间一次清理掉。
这样每次都是对其中的一块进行内存回收,内存分配的时候也不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
只是这种算法的代价就是将内存缩小为原来的一半。
现在的商业虚拟机都采用这种方法来回收新生代,IBM的专门研究表明,新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden内存空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor空间。回收时,将Eden和Survivor中还存活的对象一次性地拷贝到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。
Sun HotSpot虚拟机默认Eden和Survivor空间的大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是被“浪费”掉的。
对于98%的对象时朝生夕死,我们也没办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,就需要依赖其他的内存(这里指老年代)进行分配担保(Handle Promotion)。
如果另外一块Survivor空间没有足够的空间存放上一次新生代收集存活下来的对象,这些对象将直接通过分配担保机制进入老年代。
-
标记-整理算法
根据老年代的特点,提出了一种“标记-整理”算法,标记过程和“标记-清除”算法一样,但后续的步骤不是对标记的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。老年代一般采用这种算法。
-
分代收集算法
当前的商业虚拟机垃圾收集都采用“分代收集”算法,并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样可以根据各个年代的特点采用不同的垃圾回收算法。
在新生代中,每次垃圾回收时都有大批量的对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中,由于对象的存活率高,没有多余的空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。
-
内存分配与回收策略
-
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多具有朝生夕死的特性,所以Minor GC非常频繁,回收速度也比较快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的),Major GC的速度一般比Minor GC慢十倍以上。
-
大对象直接进入老年代
大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的内存空间来“安置”它们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝。
-
长期存活的对象将进入老年代
虚拟机采用分代收集的思想来管理内存,就必须识别哪些对象应当放到新生代,哪些对象应放到老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过了一次Minor GC之后任然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加一,当它的年龄增加到一定的程度(默认为15岁)时,就会被晋升到老年代中。
对象晋升到老年代的年龄阀值,可以通过参数-XX:MaxTenuringThreshold来设置。
-
动态对象年龄判定
为了更好的适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入到老年代,无需等到MaxTenuringThreshold中要求的年龄。
-
空间分配担保
在发生Minor GC之前,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。
新生代使用复制收集算法,为了内存利用率,只使用其中的一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC之后仍然存活的情况时,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入到老年代中。
理解GC日志
-
停顿类型
[GC:Minor GC, [Full GC:Full GC
-
GC的位置
[DefNew:Default New Generation Serial收集器新生代
[ParNew:Parallel新生代
[PSYoungGen:Parallel Scanvenge收集器的新生代
[Tenured:老年代
[Perm:永久代