阿里面试官这样问Java 垃圾回收,我的回答让他竖起了大拇指!

1,251 阅读6分钟

前言

这周我投递出了简历,岗位是java后端开发工程师。这周阿里面试官给我进行了面试。面试过程中他问了java垃圾回收机制以及算法,今天结合面试官的三个问题详细讲一讲java的垃圾回收机制

java对象

面试官大佬:如何判断java对象已经被回收

我:(这可难不到我)

引用计数

为每个对象存储一个计数RC,当有其他引用指向它时,计数RC++;当其他引用与其断开时,RC--;如果有RC=0,则回收它(及其所以指向的object)。

可达性分析法(根搜索算法)

把内存中的每一个对象都看作一个节点,并且定义了一些对象作为根节点“GC Roots”。**以“GC Root”的对象作为起始点,开始向下搜索,搜索所走过的路径称为引用链。**如果一个对象与起始点没有任何引用链,则说明不可用,需要被回收。

在这里插入图片描述

图示object6、7、8与起始点没有任何引用链,则说明不可用,需要被回收。

面试官大佬:谈一谈JVM垃圾回收算法的进化

我:(这可难不到我)

JVM(java虚拟机)的内存结构

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调优

面试官大佬:详细说说一次你JVM调优的经历

我:(这可难不到我)

问题

在这里插入图片描述
Young 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…