三、GC垃圾回收
1、GC是什么?为什么要GC
GC:垃圾收集,GC能帮助我们释放jvm内存,可以一定程度避免OOM问题,但是也无法完全避免。Java的GC是自动工作的,不像C++需要主动调用。
当new对象的时候,GC就开始监控这个对象的地址大小和使用情况了,通过可达性分析算法寻找不可达的对象然后进行标记看看是否需要GC回收掉释放内存。
2、你能保证GC执行吗?
不能,我只能通过手动执行System.gc()
方法通知GC执行,但是他是否执行的未知的。
3、对象的引用类型有哪几种,分别介绍下
强引用
发生GC的时候不会回收强引用所关联的对象。比如new就是强引用。
软引用
有用但非必须的对象,在OOM之前会把这些对象列进回收范围之中进行第二次回收,若第二次回收还没有足够的内存,则会抛出OOM。也就是第一次快要发生OOM的时候不会立马抛出OOM,而是会回收掉这些软引用,然后再看内存是否足够,若还不够才会抛出OOM。
弱引用
有用但非必须的对象,比软引用更弱一些,只要开始GC,不管你内存够不够,都会将 弱引用所关联的对象给回收掉。
虚引用
也叫幽灵引用/幻影引用,无法通过虚引用获得对象,他的意义在于能在这个对象被GC掉时收到一个系统通知,仅此而已。
4、垃圾收集算法有哪些
标记清除
分为两步:标记和清除。
首先需要标记出所有需要回收的对象,然后进行清除回收变为可用内存。
缺点:效率低,会产生垃圾碎片 。
复制算法
将可用堆内存按照容量分为大小相等的两块,每次只用一块,当这块内存快用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存一次清理掉。
年轻代from/to(s1/s2)采取的就是此种算法。老年代一般不会采取此种算法,因为老年代都是大对象且存活的久的,空间压缩一半代价略高。
优点:效率较高、不会产生碎片。
缺点:将内存缩小为原来的一半,代价略高。
标记整理
分为两步:标记和整理。
整理其实也是两步:整理+清除。
整理让所有存活的对象都移动到一端,然后清理掉边界以外的内存。
优点:不会产生碎片问题,适合年老代的大对象存储,不像复制算法那样浪费空间。
缺点:效率赶不上复制算法。
分代算法
并不是新算法,而是根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
5、为什么要分代
因为在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间会相对较长,也有很多对象完全没必要遍历,比如大对象存活的时间更长,遍历下来发现不需要回收,这样更浪费时间。所以才有了分代,分治的思想,进行区域划分,把不同生命周期的对象放在不同的区域,不同的区域采取最适合他的垃圾回收方式进行回收。
6、分代垃圾回收是怎么工作的
分代回收基于这样一个理念:不同的对象的生命周期是不一样的,因此根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。这样来提高回收效率。
新生代执行流程:
- 把 Eden + From Survivor(S1) 存活的对象放入 To Survivor(S2) 区;
- 清空 Eden 和S1 区;
- S1 和 S2 区交换,S1 变 S2,S2变S1。
每次在S1到S2移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老年代。大对象也会直接进入老年代。
老年代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。
7、垃圾回收器有哪些
Serial
采取复制算法,用于新生代,单线程收集器,所以在他工作时会产生StopTheWorld。单线程情况下效率更高,比如用于GUI小程序
ParNew
采取复制算法,用于新生代,是Serial的多线程版本,多个GC线程同时工作,但是也会产生StopTheWorld,因为不能和工作线程并行。
Parallel Scavenge
采取复制算法,用于新生代,和ParNew一样,所以也会产生STW,多线程收集器,他是吞吐量优先的收集器,提供了很多参数来调节吞吐量。
Serial Old
采取标记整理算法,用于老年代,单线程收集器,所以在他工作时会产生StopTheWorld。单线程情况下效率更高,比如用于GUI小程序
Parallel Old
采取标记整理算法,用于老年代,Parallel Scavenge收集器的老年代版本,吞吐量优先。
CMS
采取标记清除算法,老年代并行收集器,号称以最短STW时间为目标的收集器,并发高、停顿低、STW时间短的优点。主流垃圾收集器之一。
G1
采取标记整理算法,并行收集器。对比CMS的好处之一就是不会产生内存碎片,此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。而且他的STW停顿时间是可以手动控制一个长度为M毫秒的时间片段(可以用JVM参数 -XX:MaxGCPauseMillis指定),设置完后垃圾收集的时长不得超过这个(近实时)。
8、详细介绍一下 CMS 垃圾回收器?
采取标记清除算法,老年代并行收集器,号称以最短STW时间为目标的收集器,并发高、停顿低、STW时间短的优点。主流垃圾收集器之一。
主要分为四阶段:
- 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。所以此阶段会STW,但时间很短。
- 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。不会STW。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。STW时间会比第一阶段稍微长点,但是远比并发标记短,效率也很高。
- 并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
所以CMS的优点是:
- 并发高
- 停顿低
- STW时间短。
缺点:
- 对cpu资源非常敏感(并发阶段虽然不会影响用户线程,但是会一起占用CPU资源,竞争激烈的话会导致程序变慢)。
- 无法处理浮动垃圾,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,失败后而导致另一次Full GC的产生,由于CMS并发清除阶段用户线程还在运行,伴随程序的运行自然会有新的垃圾产生,这一部分垃圾是出现在标记过程之后的,CMS无法在本次去处理他们,所以只好留在下一次GC时候将其清理掉。
- 内存碎片问题(因为是标记清除算法)。当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
9、详细介绍一下 G1 垃圾回收器?
采取标记整理算法,并行收集器。
特点:
- 并行与并发执行:利用多CPU的优势来缩短STW时间,在GC工作的时候,用户线程可以并行执行。
- 分代收集:无需其他收集器配合,自己G1会进行分代收集。
- 空间整合:不会像CMS那样产生内存碎片。
- 可预测的停顿:可以手动控制一个长度为M毫秒的时间片段(可以用JVM参数 -XX:MaxGCPauseMillis指定),设置完后垃圾收集的时长不得超过这个(近实时)。
原理:
G1并不是简单的把堆内存分为新生代和老年代两部分,而是把整个堆划分为多个大小相等的独立区域(Region),新生代和老年代也是一部分不需要连续Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
补充:
Region不是孤立的,也就是说一个对象分配在某个Region中,他并非只能被本Region中的其他对象引用,而是整个堆中任意的对象都可以相互引用,那么在【可达性分析法】来判断对象是否存活的时候也无需扫描整个堆,Region之间的对象引用以及其他手机其中新生代和老年代之间的对象引用虚拟机都是使用Remembered Set来避免全堆扫描的。
步骤:
- 初始标记:仅仅标记GCRoots能直接关联到的对象,且修改TAMS的值让下一阶段用户程序并发运行时能正确可用的Region中创建的新对象。速度很快,会STW。
- 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。不会STW。
- 最终标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。STW时间会比第一阶段稍微长点,但是远比并发标记短,效率也很高。
- 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
10、GC日志分析
11、Minor GC与Full GC分别在什么时候发生
新生代内存(Eden区)不够用时候发生Minor GC也叫YGC。
Full GC发生情况:
- 老年代被写满
- 持久代被写满
- System.gc()被显示调用(只是会告诉需要GC,什么时候发生并不知道)
12、新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。标记整理很适合大对象,不会产生空间碎片。
13、栈上分配是什么意思
JVM允许将线程私有的对象分配在栈上,而不是分配在堆上。分配在栈上的好处是栈上分配不需要考虑垃圾回收,因为出栈的时候对象就顺带着一起出去了,没了,而不需要垃圾回收器的介入,从而提高系统性能。
补充1:对象逃逸。
逃逸的目的是判断对象的作用域是否有可能逃出函数体。例如下面的代码就显示了一个逃逸的对象:
private User user; private void hello(){ user = new User(); }
对象实例 user 是类的成员变量,可以被任何线程访问,因此它属于逃逸对象。但如果我们将代码稍微改动一下,该对象就可以线程非逃逸的了。
private void hello(){ User user = new User(); }
可以看到 user 实例作用域只在 hello 函数中,不会被其他线程访问到,也不会访问。所以该 user 实例对象的作用域只在该函数中,因此它并未发生逃逸。对于这样的情况,虚拟机就有可能将其分配在栈上,而不在堆上。
补充2:TLAB,自行Google。
简单点说,就是将本来应该分配在堆中的对象,让其分配在线程私有的栈上。通过这种方式,减少垃圾回收的压力,提高虚拟机的运行效率。
14、简述下对象的分配规则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次YGC。并将还活着的对象放到from/to区,若本次YGC后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次YGC那么对象会进入Survivor区,之后每经过一次YGC那么对象的年龄加1,直到达到阀值对象进入老年区。默认阈值是15。可以通过
-XX:MaxTenuringThreshold
参数来设置。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。无需等到
-XX:MaxTenuringThreshold
参数要求的年龄。
动态年龄判断是有歧义的,要想面试加分,必看这个
- 空间分配担保。每次进行YGC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行YGC,如果false则进行Full GC。