前言
这周我投递出了简历,岗位是java后端开发工程师。这周美团面试官给我进行了面试。面试过程中他问了java垃圾回收机制以及算法,今天结合面试官的三个问题详细讲一讲java的垃圾回收机制。
java对象
面试官大佬:如何判断java对象已经被回收
我:(这可难不到我)
引用计数
为每个对象存储一个计数RC,当有其他引用指向它时,计数RC++;当其他引用与其断开时,RC–;如果有RC=0,则回收它(及其所以指向的object)。
可达性分析法(根搜索算法)
把内存中的每一个对象都看作一个节点,并且定义了一些对象作为根节点“GC Roots”。**以“GC Root”的对象作为起始点,开始向下搜索,搜索所走过的路径称为引用链。**如果一个对象与起始点没有任何引用链,则说明不可用,需要被回收。
图示object6、7、8与起始点没有任何引用链,则说明不可用,需要被回收。
面试官大佬:谈一谈JVM垃圾回收算法的进化
我:(这可难不到我)
JVM(java虚拟机)的内存结构
-
符合内存模型规范
-
屏蔽了各种硬件和操作系统的访问差异
-
保证在各种平台下对内存的访问都能保证效果一致的机制及规范
-
屏蔽了各种硬件和操作系统的访问差异
- 内存管理的基本动作:
-
内存分配
-
垃圾回收
栈
-
存储方法调用以及方法执行中的局部变量。还有方法参数,编译期间已知的数据类型(八大基本类型和对象引用(reference类型), returnAddress类型)。
-
异常
-
StackOverflowError
-
OutOfMemory
本地方法栈
本地方法栈:管理本地方法(用C语言写的),可能底层调用的c或者c++。
PC
用于指示,跳转下一条需要执行的命令
堆
堆可以细分为新生代和老年代。可以通过参数进行扩展
-
所有对象都在堆上创建,即使是局部变量的object,也是在堆上创建
-
堆上创建的对象可被所有线程共享引用
-
可访问对象,就可以访问对象内的成员变量
-
如果两个线程调用同一个对象上的某个方法,它们分别保留该方法的局部变量的拷贝
方法区Method Area
用于存储被VM加载的类信息、常量、静态变量等,如static修饰的变量加载类的时候被加载到方法区中。
-
HotSpot JVM中用Permanent Area (Perm)实现该区域,并作为heap的一部分
-
Java 8之后改名为Metaspace (使用native memory)
-
也是所有线程共享的区域
可通过 -XX: PermSize 和 -XX:MaxPermSize限制方法区大小
垃圾回收基本算法
标记-清除算法
定义:为每个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加一岁),然后被回收。”
参考链接:https://blog.csdn.net/wuzhiwei549/article/details/80563134