是个面试官都会问的问题--讲一讲java的GC

Java的垃圾回收主要是考虑三件事情

  • 那些内存需要回收
  • 什么时候进行回收
  • 如何回收

首先说一下哪些内存进行回收(回收谁)

判断哪些内存需要回收,主要是判断哪些对象还活着,那些对象已经死了。判断对象的死亡主要有两种方法,一种是引用计数法,一种是可达性分析。

引用计数法

引用计数法的判定非常的简单,在对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就增加1;当引用失效时,引用计数器的值就减一。计数器的值为0就表示对象已经死了。这种算法有一个非常大的问题,就是很那解决循环引用的问题。假如有两个对象A和B,A对象引用B对象,B对象引用A对象。如果AB没有任何别的引用了,那么其实AB本身是可以进行回收的了,他们已经不可以被访问了。但是他们的引用计数这时候还是1.

可达性分析

因为引用计数法有上面的问题,所以现在主流的实现都是使用可行性分析来确认哪些内存要进行回收。可达性分析算法的主要思想是通过一系列成为“GC root”的对象作为起始点来进行搜索,如果对象对于root不可达,也就是没有引用链相连的话,就证明对象是不可用的。
GC Root的对象主要包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

然后是垃圾回收的方法(怎么回收)

主要是一些垃圾回收算法

标记-清除(Mark-sweep)算法

也是最基础的垃圾收集算法,标记清除算法主要分为两个步骤,就是标记和清除。标记是就是标记处所有要回收的对象,清除就是清楚掉之前标记的对象。
这个算法有一些缺点,首先是标记算法和清除算法的效率都不高,然后就是会产生碎片,感觉有点像操作系统内存分配的首次适应算法,缺点是会产生大量的碎片,运行一段时间之后可能大内存就没有足够大的连续空间可以分配了。

复制(copying)算法

复制算法把内存分为大小相等的两块,每次只使用其中的一块,当着一块内存用完时,就把这块中的对象复制到另一块上去,然后把已使用过的空间一次清理掉。(分配担保机制也可以说一下)

标记-清理(Mark-Compact)

复制收集算法在对象存活率高的时候要进行大量复制操作,效率是非常低的。所以一般的老年代的回收不会使用这种算法。根据老年代对象存活率高的特点,有人提出了标记整理算法,标记的时候和标记清除算法一样,但是后面不直接清理标记的对象,而是让存活的对象都向一端移动,然后直接清理掉边界以外的内存。

分代收集算法(Generation Collection)

现在商业虚拟机的垃圾收集都使用分代收集算法。分代收集算法和前面几种不能算作是一种独立的垃圾收集算法,而是一种根据对象存活周期的不同把对象分代来进行收集,一般是分为新生代与老年代。新生代中的对象存活率低,老年代的对象存活率高。根据这个特点来对新生代与老年代使用不同的垃圾收集算法进行收集。一般来说,新生代中的对象存活率比较低使用复制算法比较合适。老年代存活率高,比较适合使用标记清理法或者标记整理法。

最后是主流的垃圾收集器

现在比较主流的垃圾收集器是CMS收集器和G1收集器

CMS(Concurrent Mark Sweep)收集器

CMS收集器的目标是收集的停顿时间最短,停顿时间就是垃圾收集线程执行的时候要占用cpu资源造成用户程序执行上的停顿。
CMS收集器是基于标记清除算法实现的,他的主要步骤有四步:

  • 初始标记(只标记GC Root直接关联的对象)
  • 并发标记(可达性分析,找出存活的对象)
  • 重新标记(并发标记时时变动的部分)
  • 并发清除

整个过程中最耗费时间的是并发标记和并发清除的过程,在CMS收集器的垃圾清理过程中,整两个操作是和用户线程并发执行的。
但是CMS也有一些缺点,首先是对CPU的要求会比较高。垃圾收集的时候会造成程序响应变慢。然后就是不能收集浮动垃圾,浮动垃圾就是因为CMS
的垃圾回收是并发的(主要是并发清理的过程,重新标记时停止了并发清理),在垃圾回收的过程中可能会产生新的垃圾。还有就是标记清理算法的碎片问题

G1收集器

使用G1收集器的时候,Java堆的内存布局就和其他的收集器有很大的差别,G1将Java堆分为很多的Region,虽然还存在着老年代与新生代的概念,但是它们都是一部分Region的合集。
而且G1收集器的时间停顿是可以预测的(使用者可以指定M秒的时间段内消耗在垃圾收集上的时间少于N秒),可以有计划的避免在Java堆中进行全区域的垃圾收集。G1跟踪各个Region中垃圾堆的价值的大小和回收的代价,在后台维护一个优先列表,每次根据允许的收集时间,优先来回收价值最大的Region。这种方法可以有很高的垃圾回收效率。
主要步骤有:

  • 初始标记(仅标记与GC Root直接关联的对象)
  • 并发标记(可达性分析,标记活着的对象)
  • 最终标记(修正并发标记时用户线程继续执行产生变动的部分)虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记只要将其数据合并到Remember Set中。(这阶段需要线程停顿,但是可以并行执行)
  • 筛选回收(筛选回收就是维护前面说的那个优先队列,然后根据用户期望的GC停顿时间来进行回收计划)

内存分配与回收策略(什么时候回收)

对象优先在Eden区分配

对象有现在新生代Eden区分配,如果Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
-XX:SurvivorRatio = 8 决定新生代中Eden与Survivor的比例

大对象直接进入老年代

大对象是指需要大量连续空间的Java对象(很长的字符串,数组),大对象直接分配到老年代的目的是避免在Eden区和Survivor区之间大量复制内存(新生代采用复制算法收集内存)
经常出现大对象容易导致内存还很多的时候就提前触发GC来放置这些大对象。
-XX:PretensureSizeThreshold参数,大于参数值得直接分配到老年代。

长期存活的对象进入老年代

虚拟机会给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过一次Minor GC后仍然存活,如果Survivor空间还够的话,就移动到Survivor空间,以后每经过一次Minor Gc年龄就加一,年龄大于一定程度(默认15),就会晋升到老年代。
-XX:MaxTenuringThreshold=15 来设置晋升的阈值

动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小总和超过Survivor空间的一半,年龄大于等于该年龄的对象就直接进入老你那嗲,不需要等到MaxTenuringThreshold阈值的年龄

分配担保机制

在发生Minor GC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象的总空间,大的话可以保证安全,因为就算所有都进老年代也能容纳。老年代没有新生代可用空间大的话,虚拟机查看HandlePromotionFailure设置值是否允许担保失败。如果允许,继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,大的话就执行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,就要改为进行一次FullGC。
冒险冒的什么风险? 新生代使用复制算法,但是为了内存利用率,只使用其中一个Survivor空间作为轮换备份。因此在MinorGC之后仍有大量对象存活,就要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值