【<JVM垃圾回收专题>】

  1. 我们首先需要判断垃圾,其中中心思想为判断其是否还有引用。可以使用引用计数法可达性分析
  2. 我们对垃圾进行回收时,需要一些垃圾回收算法进行理论支持。包括:标记-清除复制标记-整理分代收集算法
  3. 如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。见下图,其中CMS和G1重点了解。
  4. 最后我们划分了堆内存(年轻代<一个生成区和两个幸存区>和老年代)和非堆内存(永久代),进行垃圾分类。还有两种针对不同区域实施垃圾回收策略,追求高效率的回收。
    在这里插入图片描述
[1] 垃圾回收的场所及原因?

垃圾回收的主要场所是堆内存,主要对象是实例对象和变量。随着程序的运行它们占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至会因为可用内存不足造成一些不必要的系统异常。

[2] 为什么学习GC和如何学习GC?

当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就需要对这些自动化的技术实施必要的监控和调节。

  • 哪些内存需要回收?->
  • 什么时候回收?
  • 如何回收?
[3] JVM如何判定一个对象是否应该被回收?

判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法。

  1. 引用计数法:是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理对象间循环引用的问题。

  2. 可达性分析:root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。

  1. 什么对象会被认为是root对象?
  • jvm运行时方法区类静态变量(static)引用的对象
  • jvm运行时方法区常量引用的对象
  • jvm当前运行线程中的虚拟机栈变量表引用的对象
  • 本地方法栈中(jni)引用的对象
  • 被同步锁(synchronized)持有的对象
  • 虚拟机内部的引用

总之一句话,GC Root 对象一定是影响程序运行的对象。

  1. 何为循环引用?

link

  1. “引用”是什么意思?

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表一个引用。JDK1.2以后将引用分为强引用,软引用,弱引用和虚引用四种。

  1. 非死不可?

真正宣告一个对象死亡,至少要经历两次被标记的过程,如果没有引用链,就会进行第一次标记,随后进行一次筛选,筛选的条件时对象是对象是否覆盖过finalize方法,或者finalize方法已经被虚拟机调用过,则执行finalize方法。

[4] 在java中为什么不推荐使用finalize

一般来说,finalze方法都是在Java虚拟机发现去除那些已经被执行了finalize的对象之外,没有任何活动的线程能够引用到该对象的时候调用。在finalze方法里,可以做任何事情,甚至让该对象重新可被其他线程引用;但是一般来说,finalize方法的通常目的是在该对象真正被回收之前做一些清理的工作。比如,一个代表输入/输出连接的对象,它的finalize方法可能会在自己被回收之前中断对应的I/O连接。

https://baijiahao.baidu.com/s?id=1655232869611610920&wfr=spider&for=pc

我们都知道一个对象如果没有了任何引用,java虚拟机就认为这个对象没什么用了,就会对其进行垃圾回收,但是如果这个对象包含了finalize函数,性质就不一样了。怎么不一样了呢?

java虚拟机在进行垃圾回收的时候,一看到这个对象类含有finalize函数,就把这个函数交给FinalizerThread处理,而包含了这个finalize的对象就会被添加到FinalizerThread的执行队列,并使用一个链表,把这些包含了finalize的对象串起来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X8fWGGdC-1596595079941)(https://pics5.baidu.com/feed/1e30e924b899a9010d34c78331bb0d7d0208f532.jpeg?token=cc6b19d8b9ecf5e0697042b70da81e1d&s=0D40EC12E18768EA584DA0CE0200D0A1)]

他的影响在于只要finalize没有执行,那么这些对象就会一直存在堆区,不过这里只是4个包含了finalize的对象,影响不是那么大,如果有一万个或者是十万个呢?这就影响大了。

finalize的原理其实很简单,在这里简要的梳理一下:

(1)对象在初始化的过程中会判断是否重写了finalize,方法是判断两个字段标志has_finalizer_flag和RegisterFinalizersAtInit。

(2)如果重写了finalize,那就把当前对象注册到FinalizerThread的ReferenceQueue队列中。注册之后的对象就叫做Finalizer。方法是调用register_finalizer函数。此时java虚拟机一看当前有这个对象的引用,于是就不进行垃圾回收了。

(3)对象开始被调用,FinalizerThread线程负责从ReferenceQueue队列中获取Finalizer对象。开始执行finalize方法,在执行之前,这个对象一直在堆中。

(4)对象执行完毕之后,将这个Finalizer对象从队列中移除,java虚拟机一看对象没有引用了,就进行垃圾回收了。

这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。

为什么要舍弃finalize

第一:调用时机的不确定性

虽然finalize()方法早晚会被调用到,但这种调用时机的不可控性可能会导致资源迟迟不被释放而出现系统级异常。因为计算机的资源有是限的,当明确要释放某些资源时(比如上面例子中reader所掌控的一系列资源),应该使用其它的办法让这些资源立即释放(后面给出例子)!

第二:影响代码的可移植性

因为每种JVM内置的垃圾回收算法都是不同的,所以可能在你的JVM里,你辛辛苦苦编写的使用finalize方法的案例运行的很好,但移植到不同的JVM中时,很有可能会崩溃的一塌糊涂!

第三:成本较高

如果某个类重载了finalize方法且在方法内部实现了一些逻辑,那么JVM在构造或销毁这个类的对象之前,会做很多额外的工作。很明显,如果一个类没有重载finalize方法,那么销毁时只要将堆中的内存处理一下就可以了,而如果重载了finalize方法的话,就要执行finalize方法,万一执行过程中再出现点异常或错误,那消耗的成本就更高了。

第四:异常丢失

第三点中也说过了,万一fianlize方法中抛出了异常,那么finalize会终止运行,而抛出的这个异常也会被舍弃,最终会让对象实例处于一种半销毁半存活的僵尸状态,导致意想不到的后果!

[4] 详细说下四种引用?

强引用:普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。默认情况下,对象采用的均为强引用。
软引用:通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。软引用是Java中提供的一种比较适合于缓存场景的应用.
弱引用:通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
虚引用:也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。

https://www.cnblogs.com/shoshana-kong/p/10575781.html

强引用-FinalReference

介绍:
强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:

String str = new String("str");

这个str就是强引用。
可用场景:
地球人都知道,但是我讲不出来。

软引用-SoftReference

介绍:
软引用在程序内存不足时,会被回收,使用方式:

// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));

可用场景:
创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。PS:图片编辑器,视频编辑器之类的软件可以使用这种思路。
软引用使用例子传送门:https://www.cnblogs.com/mjorcen/p/3968018.html

弱引用-WeakReference

介绍:
弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:

WeakReference<String> wrf = new WeakReference<String>(str);

可用场景:
Java源码中的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
弱引用使用例子传送门:http://www.importnew.com/21206.html

虚引用-PhantomReference

介绍:
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue,使用例子:

PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());

可用场景:
对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效(传送门:http://blog.csdn.net/aitangyong/article/details/39450341),so。

强调

上诉所说的几类引用,都是指对象本身的引用,而不是指Reference<T>的四个子类的引用(SoftReference<T>等)。

[5] 常用的垃圾收集算法有哪些?

参考网站:https://www.cnblogs.com/ghoster/p/7580729.html

  1. 标记-清除算法(Mark-Sweep):算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,之所以说它是最基本的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的主要不足有两个:一是效率问题,标记和清除效率都不高;二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    img

  2. 复制算法(Copying):为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,代价未免太高了一点。

    img

  3. 标记-整理算法(Mark-compact):复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费50%的空间就要使用额外的空间进行分配担保(Handle Promotion当空间不够时,需要依赖其他内存),以应对被使用的内存中所有对象都100%存活的极端情况,对于“标记-整理”算法,标记过程仍与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存

    img

  4. 分代收集算法:当前的商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法

    为什么在新生代使用使用复制算法,在老年代使用标记整理算法?

    在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

什么是内存碎片?如何解决?

由于不同 Java 对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。

[4] 分代收集的理论支撑?

弱分代假说:绝大数对象都是朝生夕灭的;

强分代假说:熬过多次垃圾收集过程的对象越难以消亡。

跨代引用假说:跨代引用相对于同代引用只占少数。

1. 弱分代假说和强分代假说证明了什么?

弱分代假说和强分代假说鉴定了垃圾收集器一致的原则,收集器应该讲java划分为不同区域,然后将回收对象依据年龄(年龄即对象熬过垃圾收集的过程)分配到不同的存储之中存储,使用不同垃圾回收策略。

2. 跨代引用假说解释了什么?

由于对象之间不是孤立的,对象之间会存在跨代引用。所以可以用跨代引用假说证明,存在互相引用关系的两个对象,应该是倾向于同时生存和消亡的。举个例子,如果某新生代的对象存在于老年代难以消亡,那么引用会使新生代的对象存活很久,进而晋升到老年代中,这种跨代引用随即被消除了。

3. 记忆集?

https://segmentfault.com/q/1010000023017473/

依据这条假说,我们不应该为了少量的跨代引用去扫描整个老年代。我们我们可以在新生代中建立一个全局数据结构–记忆集。这个结构把老年代划分为很多小块,并且标记出那一块存在跨代引用。当发生minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入GC roots进行扫描。

[6] 常用的垃圾收集器(内部使用垃圾收集算法)有哪些?

垃圾回收算法是垃圾回收的方法论,垃圾收集器,是内存回收的实践论。link,记忆方法:单线程:serial与serial old,多线程:ParNew与CMS,高吞吐:Parallel Scavenge与Parallel Old,G1

  1. Serial 收集器:新生代与老年代的单线程收集器,标记和清理都是单线程,在垃圾回收时会暂停其他工作线程,直到它收集完,即stop the world。优点是简单粗暴适合单核及少核处理器。
  2. Serial Old 收集器(标记-整理算法)老年代单线程收集器,Serial 收集器的老年代版本。服务端使用时:可以与Parallel Scavenge 收集器配合使用,也可以成为CMS收集器失败后的备选预案。
  3. ParNew 收集器(标记-复制算法)新生代收集器,可以认为是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现。它是激活CMS后默认的新生代收集器。
  4. Parallel Scavenge 收集器(标记-复制算法)并行多线程收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为 99%, 适合后台应用等对交互相应要求不高的场景。吞吐量= 用户线程时间 / (用户线程时间+GC线程时间),吞吐量适合保证高效率的执行有效工作。
  5. Parallel Old 收集器(标记-整理算法)Parallel Old 收集器的老年代版本,并行收集器,吞吐量优先。
  6. **CMS(Concurrent Mark Sweep)**收集器(标记-清除算法)高并发、低停顿,追求最短 GC 回收停顿时间,cpu 占用比较高,响应时间快,停顿时间短,多核 cpu 追求高响应时间的选择。CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
  7. G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First的由来。
[7] 垃圾收集器如何互相配合使用
image-20200715164625585

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OIgrjA16-1596595079949)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715164735100.png)]

[8] CMS说一下?
  • CMS垃圾清除的步骤

link首先停止线程,标记root根遍历直接对象,然后根据其扫瞄,之后停止线程重新扫描,最后删除

  1. 初始标记(stop the world):使用可达性分析记录下直接与 root 相连的对象,暂停所有的其他线程,速度很快;
  2. 并发标记:同时开启 GC 和用户线程,用 CG root 直接关联对象一个去记录可达对象。但在这个阶段结束,这并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  3. 重新标记(stop the world):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录【这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短】;
  4. 并发清除:开启用户线程,同时 GC 线程开始对为标记的区域做清扫。
  • CMS 的优缺点

优点:CMS(concurrent low pause collector)并发收集、低停顿;

缺点:对 CPU 资源敏感、无法处理浮动垃圾、它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

并发消耗CPU资源 其中的并发标记和并发清理是工作线程和垃圾回收线程并发工作,这样在需要STW的时间内不会让整个系统不可用。但是在并发标记阶段,需要根据GC Roots标记出大量的存活对象,而在并发清理阶段,则需要将垃圾对象从各种随机内存位置删掉,这两个阶段都非常消耗性能,所以垃圾回收线程会占用一部分的CPU资源,导致系统的执行效率降低。

CMS默认的回收线程数是 (CPU个数+3)/4,当在CPU核数较多的时候,对系统性能的影响并不是特别大。但是如果是CPU核数较少,例如双核的时候,就会占用一个CPU去处理垃圾回收,系统的CPU资源直接降低50%,这就严重影响了效率。

因为现在CPU的核数越来越多,所以这种场景基本不会对系统造成很大的影响,可以忽略不计。

Concurrent Mode Failure问题 并发清理阶段,工作线程和垃圾回收线程并发工作的时候,此时工作线程会不断产生新的垃圾,但是垃圾回收线程并不会去处理这些新生成的垃圾对象,需要等到下次垃圾回收的时候才会去处理,这些垃圾对象称之为:浮动垃圾 。因为有这些浮动垃圾的存在,所以老年代不能在100%使用的时候才去进行垃圾回收,否则就放不下这些浮动垃圾了。有一个参数是“-XX:CMSInitiatingOccupancyFraction”,这个参数在jdk1.6里面默认是92%,意思是老年代使用了92%的空间就会执行垃圾回收了。但是即使预留了8%的内存去存放浮动垃圾,但是还是有可能放不下,这样就会产生Concurrent Mode Failure问题。一旦产生了Concurrent Mode Failure问题,系统会直接使用Serial Old垃圾回收器取代CMS垃圾回收器,从头开始进行GC Roots追踪对象,并清理垃圾,这样会导致整个垃圾回收的时间变得更长。

解决办法就是根据系统的需求,合理设置“-XX:CMSInitiatingOccupancyFraction”的值,如果过大,则会产生Concurrent Mode Failure问题,如果设置的过小,则会导致老年代更加频繁的垃圾回收。

空间碎片问题 CMS的标记-清理算法会在并发清理的阶段产生大量的内存碎片,如果不整理的话,则会有大量不连续的内存空间存在,无法放入一些进入老年代的大对象,导致老年代频繁垃圾回收。所以CMS存在一个默认的参数 “-XX:+UseCMSCompactAtFullCollection”,意思是在Full GC之后再次STW,停止工作线程,整理内存空间,将存活的对象移到一边。还要一个参数是“-XX:+CMSFullGCsBeforeCompaction”,表示在进行多少次Full GC之后进行内存碎片整理,默认为0,即每次Full GC之后都进行内存碎片整理。

CMS虽然使用并发的方式降低了STW的时间,但是还需要配合一些CMS的参数才能完全发挥出CMS的优势,否则甚至会降低垃圾回收的效率。因此只有掌握了CMS的原理和参数的调试,才能让系统运行的更加流畅。

[9] G1 说一下?

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。G1(Garbage-First)开创了面向局部收集的设计思路和基于region的内存布局形式。JAVA9时取代Parallel Scavenge与Parallel Old组合。G1 在扫描了 region 以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的 region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为 Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

img

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

img

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

img

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记:仅仅标记一下GCroot可以关联到的对象,并且修改TAMS指针的值,让下一个阶段用户线程并发运行时,可以正确在可用的region中分配新对象。这个阶段需要停顿线程但是耗时极短。而且是借用minor GC时候同步完成,所以G1收集器在这个接丢单没有额外的停顿。
  • 并发标记:从已经标记的堆中的对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个过程耗时比较长,
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(暂停用户线程):首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。可以自由选择多个region构成回收集,然后把决定回收的那一部分region的存活对象复制到空的regions中,再清理掉整个旧的region的全部空间。

具备如下特点:

  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
  • G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。 (阿里面试)
[10] G1和CMS?

总体来说,G1跟CMS一样,是一块低延时的收集器,同样牺牲了吞吐量,不过二者之间得到了很好的权衡。

G1与CMS对比有一下不同:

  1. 分代: CMS中,堆被分为PermGen,YoungGen,OldGen;而YoungGen又分了两个survivo区域。在G1中,堆被平均分成几个区域(region),在每个区域中,虽然也保留了新老代的概念,但是收集器是以整个区域为单位收集的。

  2. 算法: 相对于CMS的“标记—清理”算法,G1会使用压缩算法,保证不产生多余的碎片。收集阶段,G1会将某个区域存活的对象拷贝的其他区域,然后将整个区域整个回收。

  3. 停顿时间可控: 为了缩短停顿时间,G1建立可预存停顿模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。

[10] 说下你对垃圾回收策略的理解/垃圾回收时机?

在这里插入图片描述
答:JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代(复制算法)和老年代(标记-整理算法)。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。

  1. Minor / Scavenge GC所有对象创建在新生代的 Eden 区,当 Eden 区满后触发新生代的 Minor GC,将 Eden 区和非空闲 Survivor 区存活的对象复制到另外一个空闲的 Survivor 区中,在这个过程中保证一个 Survivor 区是空的,新生代 Minor GC 就是在两个 Survivor 区之间相互复制存活对象,直到 Survivor 区满为止。我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代。
  2. Full GC:**Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。对整个堆进行整理,包括 Young、Tenured 和 Perm。**Full GC 因为需要对整个堆进行回收,而且会"**STOP THR WORLD"**所以比 Minor GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。

如果生成区存活对象太多,导致幸存者区放不小怎么办?

可以使用逃生门,将对象转移到其他内存区域,如:老年代。

[11] 分区大小比值及其理论依据

1

生成区:s0:s1=8:1:1 老年代:新生代=2: 1

  • 为什么年轻代新增servivor区域?

  • 在年轻代新增Surviver区,有利于减轻老年代的负担,尽可能的让大部分对象在年轻代通过较高效的Yong GC回收掉,不至于老年代里存放的对象过多导致内存不足而进行频繁的Full GC操作。而且这种分区有利于减少内存碎片的产生。

  • 为什么使用 8:1:1,而不使用5:5,或者8:2?

  • 由于IBM的研究表示,新生代中有98%的对象活不过第一轮收集。使用5:5会造成空间的极大浪费。哪为什么使用8:2?因为这样会导致比例为2的空间向空间为8的区域内复制时,发生minor gc速度过快。

  • 老年代:新生代=2: 1?

  • 老年代不宜过小,如果老年代小,会导致转为老年代的时候,老年代撑不下,导致full gc,回收停顿时间过长。

[12] 内存分配策略

大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

[2. 大对象直接进入老年代](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_2-大对象直接进入老年代)

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

[3. 长期存活的对象进入老年代](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_3-长期存活的对象进入老年代)

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

[4. 动态对象年龄判定](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_4-动态对象年龄判定)

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

[5. 空间分配担保](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_5-空间分配担保)

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

[13] Minor GC 和 Full GC触发条件
  • Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

[Full GC 的触发条件](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=full-gc-的触发条件)

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

[1. 调用 System.gc()](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_1-调用-systemgc)

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

[2. 老年代空间不足](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_2-老年代空间不足)

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

[3. 空间分配担保失败](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_3-空间分配担保失败)

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。

[4. JDK 1.7 及以前的永久代空间不足](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_4-jdk-17-及以前的永久代空间不足)

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

[5. Concurrent Mode Failure](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_5-concurrent-mode-failure)

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值