这周我投递出了简历,岗位是java后端开发工程师。这周阿里面试官给我进行了面试。面试过程中他问了java垃圾回收机制以及算法,今天结合面试官的三个问题详细讲一讲java的垃圾回收机制。
面试官大佬:如何判断java对象已经被回收
我:(这可难不到我)
为每个对象存储一个计数RC,当有其他引用指向它时,计数RC++;当其他引用与其断开时,RC--;如果有RC=0,则回收它(及其所以指向的object)。
把内存中的每一个对象都看作一个节点,并且定义了一些对象作为根节点“GC Roots”。**以“GC Root”的对象作为起始点,开始向下搜索,搜索所走过的路径称为引用链。**如果一个对象与起始点没有任何引用链,则说明不可用,需要被回收。
图示object6、7、8与起始点没有任何引用链,则说明不可用,需要被回收。
面试官大佬:谈一谈JVM垃圾回收算法的进化
我:(这可难不到我)
Native Stacks | 本地方法栈 |
---|---|
PC | 代码行号指示器,用于跳转下一条需要执行的命令 |
Method area | 用于存储被VM加载的 类信息、常量、静态变量等 |
定义:为每个object设定状态位(live/dead)并记录,即mark阶段;将 标记为dead的对象进行清理,即sweep阶段。
简单来说,首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
标记过程:
清除过程: 存在问题:效率不高:标记和清除过程的效率都不高
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World
首先标记出所有需要回收的对象
在标记完成后让所有存活的对象都向一端移动
最后直接清理掉端边界以外的内存
优点:避免碎片化缺点:时间消耗太长,影响程序本身
该GC策略与标记-整理的区别在于:不是在同一个区域内进行整理,而是将live对象全部复制到另一个区域。
将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象,每次只使用其中的一块。
当一块的内存用完了,将还存活着的对象复制到另外一块上面,然后清理已使用过的内存空间。
flip(){ Fromspace, Tospace = Tospace, Fromspace top_of_space = Tospace + space_size scan = free = Tospace for R in Roots {R = copy(R)} while scan < free { for P in Children(scan) {*P = copy(*P)} scan = scan + size (scan) } } copy(P) { if forwarded(P){return forwarding_address(P)} else { addr = free move(P,free) free = free + size(P) forwarding_address(P) = addr return addr } } 复制代码
优点:
免费压缩空间
所有对象大小的分配都非常便宜:只需增加空闲指针即可分配
仅处理实时数据(通常是堆的一小部分)
固定的空间开销:释放和扫描指针
全面:自然收集的循环垃圾
易于实施并且合理有效
存在问题:
效率问题:在效率存活率较高时,复制次数多,效率低
空间问题:內存缩小了一半;需要额外空间做分配担保(老年代)
Java堆分为新生代、老年代和永久区(Java 8之后改名为Metaspace)。
针对不同的区域,使用不同的GC策略
在新生代中,只有一小部分对象可较长时间 存活,选用复制算法
针对年老代:这里的对象有很高的幸存度,使用“标记-清理”或“标记-整理”算法。
面试官大佬:详细说说一次你JVM调优的经历
我:(这可难不到我)
-Xms1000M -Xmx1800M -Xmn350M -Xss300K -XX:+DisableExplicitGC -XX:SurvivorRatio=4 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled 复制代码
-Xms表示初始化堆内存
-Xmx表示最大堆内存
-Xmn表示新生代内存
-XX:SurvivorRatio=4表示新生代的Eden是4/10,S1和S2各占3/10
因此Eden的内存大小为:0.435010241024字节, 为14010241024**字节
/** * @date : 2020-03-22 09:48 **/ public class JavaHeapTest { public final static int OUTOFMEMORY = 500 * 1024 * 1024; private String oom; StringBuffer tempOOM = new StringBuffer(); public JavaHeapTest(int leng) { int i = 0; while (i < leng) { i++; try { tempOOM.append("a"); } catch (OutOfMemoryError e) { e.printStackTrace(); break; } } this.oom = tempOOM.toString(); } public String getOom() { return oom; } public static void main(String[] args) { for(int i=0;i<50;i++) { JavaHeapTest javaHeapTest = new JavaHeapTest(OUTOFMEMORY); System.out.println(javaHeapTest.getOom().length()); } } } 复制代码
年轻代分为1个Eden和2个Survivor区(一个是from,另一个是to)。新创建的对象一般都会被分配到Eden区,如果经过第一次GC后仍然存活,就会被移到Survivor区。Survivor区中的对象每经过一次Minor GC,年龄+1,当年龄增加到一定程度时,会被移动到年老代。
OUTOFMEMORY = 500 * 1024 * 1024,大于Eden内存的大小。新生代分配内存小,导致YoungGC的频繁触发。
初始化堆内存没有和最大堆内存一致,在每次GC后进行内存可能重新分配。
提升新生代大小
将初始化堆内存设置为最大内存
将SurvivorRatio由4修改为8,让垃圾在新生代时尽可能的多被回收掉
-Xmn350M -> -Xmn800M -XX:SurvivorRatio=4 -> -XX:SurvivorRatio=8 -Xms1000m ->-Xms1800m 复制代码
YoungGC次数明显减少
关于对象从出生到回收的全过程,看到一段很棒的话分享一下。
“我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。”
“有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。
“直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。”
参考链接:blog.csdn.net/wuzhiwei549…