内存管理
一、JVM常用的垃圾回收算法
1.主要因素:
1、分配的效率 2、回收的效率
3、是否产生内存碎片 4、空间利用率
5、是否停顿,停顿时间是否可以接受(归根结底:都是围绕这条)
6、时间复杂度 7、实现的复杂度
2.GC算法:
2.1、引用计数
原理:统计每个对象被引用的次数,如果引用次数为0.就释放该对象。(新的对象加1,老的对象减1,如果引用次数为0,对象被回收)
存在问题:
1、在并发场景下,对引用次数的修改需要和对象指针的修改保证同步,(1、原子对象 2、加锁)或者非常复杂的无锁算法。设计到两个变量
2、会引发连锁式的回收(等待时间不可测)
3、致命问题:无法有效解决循环引用(A引用B,B引用A --AB垃圾对象)
面试问题:
1、引用计数如何解决循环引用?
弱引用就是专门用来解决循环引用问题的:
若 A 强引用了 B,那 B 引用 A 时就需使用弱引用,
当判断是否为无用对象时仅考虑强引用计数是否为0,不关心弱引用计数的数量。
解决了循环引用导致对象无法释放的问题,但这会引发野指针问题当B要通过弱指针访问A时,
A 可能已经被销毁了,那指向 A 的这个弱指针就变成野指针了。
在这种情况下,就表示 A 确实已经不存在了,需要进行重新创建等其他操作。
2、inc_ref和dec_ref是否能换位置?
不能换位置,换了位置就被回收了 (swift\python)
代码如下(示例):
void do_oop_store(Value * obj, Value value) {
inc_ref(&value);
dec_ref(obj);
obj = &value;
}
void inc_ref(Value * ptr) {
ptr->ref_cnt++;
}
void dec_ref(Value * ptr) {
ptr->ref_cnt--;
if (ptr->ref_cnt == 0) {
collect(ptr);
for (Value * ref = ptr->first_ref; ref != null; ref=ref->next)
dec_ref(ref);
}
}
2.2、基于拷贝的算法
原理:程序运行的堆分成大小相同的两半,一半from空间、一半to空间。
利用from空间进行分配,当空间不足以分配新的对象的时候,就会触发GC。
GC会把存活的对象全部复制到to空间。复制from和to空间互换。
特点: 1、分配采用bump the pointer(碰撞指针),每次都把top指针向后移即可。
2、回收是否高效取决于存活的对象比例
3、无内存碎片
4、要浪费一半内存
5、需要停顿(业务线程需要停止、GC正在往零停顿发展——ZGC)
6、实现简单
2.3、Mark-Sweep(标记-清除)
原理:使用链表管理所在的空闲区域。在Mark阶段,将所有的存活对象都识别出来,将不存活的对象所占用的内存交还给链表。
特点: 1、分配和回收都要操作链表
2、有内存碎片(B—D之间就是内存碎片)
3、总体的内存空间利用率较高(整个堆都可以用)
4、可以用很小的代价实现并发标记和清除。(对象的内存没有影响)
二、基于Copy的垃圾回收算法解析
面试问题:
1、如果A和B都引用的C,A和C搬到另一个空间地址,B怎么引用到C呢?B还是原先老的地址
方案一:间接指针:复制简单、分配和回收都难了;每次都需要访问对象两次,性能退化不能接受
方案二:forwarding指针
改进:提高空间利用率
将Eden空间分配成Eden,Survivor0和Survivor1三个区域。这样Survivor空间的浪费可以减少了。
配置Survivor空间的大小是JVM GC调参中的重要参数:-XX:SurvivorRatio=8 代表Eden:S0:S1=8:1:1
to空间:S0
from空间:Eden和S1 浪费的只有一个S0或者S1
启动java进程分析命令:jstat -gcutil 4709(进程值)
年轻代进行GC的时候,to区域不够怎么办?
年轻代撑死、会有老年代、进行Full GC、最后不行OOM,每个机制都有兜底的,最后就崩了。
三、GC中使用的图算分析——遍历
1、广度优先遍历(Breadth First Search,BFS)
概念:连通图的一中遍历策略。因为它的 思想是从一个顶点开始,辐射状地优先遍历与该顶点相连的顶点。
在这个过程中可以看到广度优先遍历的特点是以某个点为中心,一层层地往外扩展。
无锡、湖州是第一层,苏州、杭州第二层,前一层没有遍历完,不会访问到后一层的节点。
所以广度优先遍历适合求解最优化问题。如果B结点是由A结点扩展来的,就称B为A的BFS后序,
记录了这个关系,我们就可以找到包含最少步骤的解。可以类比游戏中的寻路问题。
采用BFS算法,实现上会比较简单,可以借助 to 空间做为算法的辅助队列。
在JDK6.0以前采用的就是广度优先搜索。
2、深度优先遍历(Depth First Search, DFS)
概念:尽可能先对纵深方向进行搜索。可以类比于树的前序遍历。
3、BFS vs DFS
采用深度优先搜索可以让一个对象与它所引用的对象在新的空间里靠得更“近”,由于程序的空间局部性,
这样可以很大地提高访问内存时的缓存命中率。