1. 对象已死?
1.1 引用计数法
给对象添加一个引用计数器,被引用时加1,反之减1。存在对象相互循环引用。
public class ReferenceCountingGC { public Object instance = null; private static final int SIZE = 1024 * 1024; private byte[] bigSize = new byte[SIZE]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } }
至少主流的Java虚拟机没有选用引用计数法来管理内存。
1.2 可达性分析
Java语言中GC Roots的对象包括以下几种:
1)虚拟机栈中(栈帧中的本地变量表)的引用变量。栈帧就是Java在每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
2)方法区中类的静态属性引用的对象。方法区用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译其编译后的代码等数据。
3)方法区中常量引用的对象
4)本地方法栈中JNI(即一般说的Native方法)引用的对象
1.3 引用
JDK1.2之后,java对引用的概念进行了扩充,将引用分为了强引用、软引用、弱引用、虚引用四种,引用强度依次逐渐减弱。
1)强引用(Strong Reference):在程序代码中普遍存在的,类似于“Object o = new Object();”这类引用,只要强引用还在,垃圾收集器就不会回收被引用的对象。
2)软引用(Soft Reference):用来描述一些有用但非必需的对象。系统在发生内存溢出异常前,将会把这些对象列进回收范围之中进行的二次回收,SoftReference类。
3)弱引用(Weak Reference):也是用来描述非必需的对象,被引用关联的对象只能生存到下一次垃圾收集发生之前,垃圾回收时,无论内存是否足够,都会回收,WeakReference类。
4)虚引用(Phantom Reference):最弱的之中引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,通过PhantomReference类来实现。
1.4 生存还是死亡
如果由于一个对象被判定为有必要实行finalize()方法,那么对象将会放置在一个F-Queue的队列中,然后虚拟机会自动建立一个优先级低的Finalizer线程去执行,但虚拟机不会等待它运行结束,任何一个对象的finalize方法都只会被系统自动调用一次。
finalize()方法可以逃脱一次GC,但是并不推荐使用,因为它运行代价高,不确定性大,无法保证各个对象的调用顺序。
2.垃圾回收算法
2.1 标记-清除算法(百度搜出来的图片):
思想就是先标记需要回收的,然后回收
缺点就是标记和清除的效率不高,清除后还有很多碎片,分配较大对象时无法找到足够的连续内存而不得不提前出发一次垃圾回收动作。
2.2 复制算法
思想就是把内存分为两块容量一样的内存空间,每次只使用一块,当某块内存使用完了就把还存活的对象复制到另一块,再把原先那块清理掉。
优点是实现简单,运行高效,分配内存时不用考虑内存碎片,只需要移动堆顶指针,按顺序分配即可。
缺点就是内存利用率低,每次就只用一半,所幸通过IBM的研究表明,新生代中对象基本都是’朝生夕死‘,所以不用按照1:1来,通常把新生代分为一个Eden : From Survivor : To Survivor空间,比例默认8:1:1(可以通过-XX:SurvivorRatio=8设置Eden可Survivor空间的比率),每次只是用Eden+一个Survivor,当Survivor空间不够时需要老年代分配担保。
例子:
测试新生代垃圾回收
private static final int _1MB = 1024 * 1024; /** * VM Properties * -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M * -XX:+PrintGCDetails -XX:SurvivorRatio=8 * */ public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; }
设置VM配置我的IDE时IDEA在VM options里输入虚拟机的配置
上述配置的意义:
-XX:+UseSerialGC是指选择了Serial(新生代) + SeialOld(MSC年老代)垃圾收集器组合,具体在后面
-verbose:gc 表示输出虚拟机中GC的详细情况
-Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。
-Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常,所以说上面程序用20M的内存。
-Xmn 是指年轻代的大小
-XX:+PrintGCDetails 是指打印GC细节,直译
-XX:SurvivorRatio=8 是指Eden : Survivor大小为8 : 1
上面程序的结果:
新生代和年老带都是10M,Eden为8M,两个Survivor都为1M,所以新生代总控可用的空间为9M。
按照代码中的分配,先分配了3个2M的空间,然后分配4M空间的时候,由于内存空间不足了,新生代发起了一次Minor GC,要把3个2M的放到1M的Survivor空间里,但是显然不行,然后就通过担保机制放到了年老代中。
通过打印的日志可以看到新生代空间被回收了但是总的使用空间没有太大的变化,是因为那6M的对象还没有’死‘。
这次GC结束后,4M的allocation4被安排到了Eden里,年老代占用6M。
[GC (Allocation Failure) [DefNew: 7976K->643K(9216K), 0.0042953 secs] 7976K->6787K(19456K), 0.0043279 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4905K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0297e8, 0x00000000ff400000)
from space 1024K, 62% used [0x00000000ff500000, 0x00000000ff5a0cb0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
Metaspace used 3137K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,非常频繁,回收速度快。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC(但非绝对的,在Parallel Scavenge收集器收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
2.3 标记-整理算法
上面的复制算法在对象存活率比较高(年老代的对象存活时间都比较长)的时候需要进行多次复制,效率会降低。
如果不想浪费50%的空间,就需要由额外的空间担保,以应对所有对象都存活的极端情况,所以年老代一般不能直接选用复制算法。
根据年老代的特点,有人提出了“标记-整理”(Mark-Compact)算法,标记过程和“标记-清除”一样,在回收过程中是让所有活着的对象都向一段移动,然后清理掉端边界以外的内存。
2.4 分代收集算法
把Java堆分为新生代和老年代,这样就可以根据他们各自的特点来选择合适的收集算法。
3. HotSpot的算法实现
3.1 枚举根节点
weiwan
参考(读书人怎么可以说是抄呢):
《深入理解Java虚拟机》