java gc 回调_[Java]GC和内存分配策略

0x01 判断对象是否已死

GC对堆进行回收之前,要判断堆中的对象实例是否已死。

1)引用计数算法(Reference Counting)

给对象添加一个引用计数器,有一个地方引用这个对象,计数器就加一,引用失效时,计数器减一,计数器为0时代表可回收。

但是主流JVM都没有采用引用计数算法(COM,PYTHON等采用了引用计数算法),因为它很难解决对象之间的循环引用问题。也就是A引用B,B引用A,除此之外没有任何对象引用它们,这时候这两个对象已经不可能再被访问了,但是他们的引用计数都不为0。

优点:效率高

缺点:无法解决循环引用问题。

2)可达性分析算法(Reachability Analysis)

主流JVM都是用这个算法来判定对象是否存活的。

思想是从GC ROOTS的对象作为起点去搜索对象是否不可达(其实就是无向图的遍历)。

3)引用的类型

Java1.2前,引用的定义是,如果reference类型的数据保存的数值代表一块内存的起始地址,就称这块内存为引用。Java1.2后对引用做了扩展,分为以下4种,强度逐渐减弱。

强引用(StrongReference)

只要强引用存在,GC永远不会回收被引用的对象。

软引用(SoftReference)

在快要OOM的时候,会把SoftReference关联的对象回收掉,如果内存还是不够才报OOM异常。

弱引用(WeakReference)

在下一次GC时,无论内存是不是够用,都会回收它关联的对象。

关于弱引用我写过Android的WeakReference,handler中对Activity的引用常常是需要用到弱引用的,因为Activity/Service对象由于被MainLooper->MessageQueue->Message->Handler->Activity/Service这样一个链条持有着(写成内部类的Handler会自动持有外部Activity的引用),Activity就会一直无法被GC回收。

关于GC的时机,我们都知道的是在内存不够时候GC会被回调,另外System.gc()/GC.Collect()不会立即进行GC。GC尽量不要主动调用。

虚引用(Phantom Reference)

精灵引用。。你甚至不能通过它拿到实例,唯一目的就是在被回收掉的时候收到一个系统通知。

3)回收工作开展

垃圾回收器准备释放内存的时候,会先调用finalize()(相当于C++析构函数)。

(1).对象不一定会被回收。

(2).垃圾回收不是析构函数。

(3).垃圾回收只与内存有关。

(4).垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。

书上提到finalize中还可以对对象进行抢救,把对象加到引用链上去,但后来建议我们完全忘记finalize()这个方法,它是Java一开始的一个妥协,不确定性大,代价高。

4)回收方法区

上面说的回收都是回收堆内存中的对象,方法区(Hotspot VM中的永久代(PermGen))也是可以回收的。但是回收性价比比较低,JVM虚拟机实现规范中不要求方法区实现GC。

方法区的回收条件非常苛刻,只有同时满足以下三个条件才会被回收。简单了解下吧。

1、所有实例被回收

2、加载该类的ClassLoader被回收

3、Class对象无法通过任何途径访问(包括反射)

0x02 垃圾收集算法

1)标记-清除算法

分为两个阶段,标记和清除。就是用引用计数和可达性分析进行判断后做标记,再扫描整个空间中被标记的对象,进行清除。

后续所有算法都是基于这个算法做的改进。

适用场景:

在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。碎片太多会导致无法为较大对象找到足够大小的连续空间,从而提前触发下一次GC。

2)复制算法

商业虚拟机采用这种算法GC。

这个看书上的图很直观了,假设把内存分成两个部分,每次只用其中的一半,那GC的时候直接把用到的那一半里面的没有被标记的对象复制到另一半去,再把老的那一半内存全部回收掉就行了。

e61a5b2e65ff

注意,只需要移动堆顶的指针,所以对于对象存活几率较小的新生代,复杂度是O(1)。

我感觉这个做法其实就是用来避免内存碎片的,因为如果想要避免碎片,直接移动每个没被回收的对象的话,这复杂度是O(n)的,而直接赋值到另一半区,复杂度是O(1)。

书上提到98%的新生代都是朝生夕死,每次回收,活下来的对象很少,所以Eden和Survivor空间的比例是8:1就可以了,我不太明白的是为什么要两块survivor,因为我感觉survivor那么小,应该是在清空Eden之后再把survivor复制回Eden存储吧。

20171220Update

上面删除线的部分理解错了。两块Survivor区域(From和To)的原因是,SurvivorFrom区域在每次Minor GC的时候,会跟Eden一起被扫描,存活的放到SurvivorTo区域,同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);然后,清空Eden和ServicorFrom中的对象。然后把From和To的区域互换(其实就是清空To区域给下一次使用)。

(Ref:http://www.cnblogs.com/ygj0930/p/6522828.html)

然后我又考虑了一下,为什么不能只有单个survivor区域,在GC的时候inplace替换survivor区域的内存?因为单个survivor区域的内容会被覆盖。那发散一下,可以先回收单个survivor区域的内存,再回收Eden内存的对象,进行堆顶指针移动就行了,而且这样也不影响对象年龄增加的那一套规则。

这里我们考虑一下

适用场景:

这种算法当控件存活的对象比较少时,极为高效,如果存活得多就不好了。

更关键的是需要一块内存交换空间用于进行对象的移动。

3)标记-整理算法

适用于Old Generation。因为标记整理算法需要移动对象指针,因此需要移动的次数越少效率越高。

刚才讲到复制算法的时候我就考虑到这种算法。。我刚才想的是,这种算法对每块可用内存都要进行一个移动,同时维护一个头指针指向可用内存的位置,时间复杂度是O(n),这个就是常见的brute force..对于我来讲是不可接受的。。结果还真有这种算法啊。前两种算法的时间复杂度都是O(1)的(整体复制、回收)。

4)分代收集

这个就是前三种的一个集合吧,根据上面三种的特点,对于存活率低的新生代,用复制算法,比较高效率;对于老年代,用标记-清除或者标记-整理。

剩下的

剩下的内容就是GC算法的实现,垃圾收集器的介绍了,比如串行并行GC等,似乎没什么好看的,不介绍了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值