GC(垃圾回收)详解

image-20220615103231681

JVM

1.年轻代:年轻代主要存放新创建的对象,垃圾回收会比较频繁。(稍微讲细一点就是即可,年轻代分成Eden Space和Suvivor Space。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space,如果对象仍然存活,则复制到Suvivor Space。) 2.年老代:年老代主要存放JVM认为生命周期比较长的对象(在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。) 3.持久代:持久代主要存放类定义、字节码和常量等很少会变更的信息。

GC算法基础

1、引用计数法:无法解决循环引用的问题,不被java采纳

2、根搜索算法:从根结点扫描,只要这个对象在引用链中,那就是可触及的。

现代虚拟机中的垃圾搜索算法:

标记清除 复制算法(新生代) 标记压缩(老年代) 这三种算法都扩充了根搜索算法。

1、标记清除算法

概念: 标记清除算法是现代垃圾回收算法的思想基础。标记清除将垃圾回收分为两个阶段:标记阶段和清除阶段。

  • 标记阶段:首先通过根结点,标记所有从根节点开始的可达对象。因此未被标记的对象就是未被引用的垃圾对象。
  • 清除阶段:在清除阶段,清除所有未被标记的对象。

实践: 在堆中的有效空间被耗尽时,就会停止整个程序(stop the world),然后进行标记清除。

  • 标记:遍历所有的GC Roots,然后将所有的GC Roots可达的对象标记为存活的对象。
  • 清除:清除的过程将遍历堆中的所有的对象,将没有标记的对象全部清除。

标记清除算法的缺点:

  • 等待时间长(递归与全堆对象遍历),导致stop the world的时间很长。
  • 不能解决内存碎片的问题

2、复制算法

概念: 将原有的内存空间分为两块,每次只使用其中的一块。在垃圾回收的时候,将存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC),和标记清除算法相比解决了内存碎片的问题。

复制算法的缺点:

  • 空间浪费,复制算法每次都只对半区内存进行回收,复制算法要想使用,最起码对象的存活率要非常低才行,而且重要的是要克服50%的内存浪费。
  • 如果要应对所有对象都100%存活的极端情况,就必须要有很大的额外空间担保,所以这种算法不适用很多的对象存活率都很高的时候(老年代)。
  • 新生代:Eden区、Surivor区(From区和to区)

现代商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要1:1的比例来划分内存空间,而是将内存划分为一块比较大的Eden区和两块比较小的Surivor空间(From区和to区),每次使用Eden和其中一块Surivor。当回收的时候,先根据分代收集算法将年龄大的对象放入老年代,然后将Eden和Surivor中还存活着的对象一次性的复制到另一块Surivor空间上,最后清理掉Eden和刚才用过的Surivor空间。HotSpot虚拟机默认Eden和Surivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。

当然,我们没法保证每次回收都只有不超过10%的对象存活,当Surivor空间不够用时,需要依赖于老年代进行分配,所以大对象直接进入老年代。

3、标记-整理算法

概念: 在标记-清除算法的基础上做了一些优化,和标记-清除算法一样,首先需要从根结点开始,对所有可达对象做一次标记。但是之后,将所有存活的对象压缩到内存一端,然后清理边界外所有的空间。

实践:

  • 标记:遍历所有的GC Roots,然后将所有的GC Roots可达的对象标记为存活的对象。
  • 整理:移动所有存活的对象,按照内存地址依次排列,然后将末端内存地址以后的内存全部回收。
  • 标记-压缩算法不仅可以弥补标记-清除算法的内存碎片问题,也消除了复制算法中,额外内存空间的问题。

标记-整理算法的缺点:

  • 效率很低,不仅要标记所有存活的对象,还要整理所有存活对象的引用地址。效率是上面两个算法中最低的。

4、标记-清除算法、复制算法、标记整理算法的总结

  • 效率: 复制算法>标记-整理算法>标记-清除算法

  • 内存整齐度 :复制算法=标记/整理算法>标记/清除算法。

  • 内存利用率: 标记/整理算法=标记/清除算法>复制算法。

5、分代收集算法

概念: 根据对象的存活周期的不同,将堆内存划分为老年代和新生代。存活时间短的对象未新生代,寻获时间长的为老年代。

实践:

  • 少量对象存活,适合复制算法,在新生代中,每次GC都会有大量对象死亡,那么就选择用复制算法。

  • 大量对象存活,适合标记-清理\标记-压缩算法,在老年代中,对象存活率高,就用标记-清理\标记-压缩算法进行GC。

6、Stop-The_World

  • java中一种全局暂停的现象,全局停顿,所有代码停止,native代码可以执行,但不能和JVM交互。多半情况下时由于GC引起的。

危害:

  • 长时间服务停止,没有响应(将用户正常工作的线程全部暂停掉)
  • 比如上面的这主机和备机:现在是主机在工作,此时如果主机正在GC造成长时间停顿,那么备机就会监测到主机没有工作,于是备机开始工作了;但是主机不工作只是暂时的,当GC结束之后,主机又开始工作了,那么这样的话,主机和备机就同时工作了。主机和备机同时工作其实是非常危险的,很有可能会导致应用程序不一致、不能提供正常的服务等,进而影响生产环境。
7、GC时为什么会有全局停顿?
  • 避免无法彻底清理干净

  • GC的工作必须在一个能确保一致性的快照中进行

8、jvm调优

  • 首先打印GC信息:

  • -XX:+PrintGC 根据不同场景设置GC的类型:

    web接口交互的系统:使用ParNew+CMS组合,可以提高响应时间; 服务型的系统:使用parallel scavenge + parallel old,可以提高吞吐量;

  • 设置堆区大小:

    ​ -Xms:初始堆大小 -Xmx:最大堆大小 -XX:SurvivoRatio=n:设置年轻代和年老代的比值。

9、什么时候会触发GC?

  • 当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC
  • 年老代(Tenured)被写满
  • 持久代(Perm)被写满
  • System.gc()被显示调用=
  • 上一次GC之后Heap的各域分配策略动态变化

10、GC的类型

①年轻代垃圾回收器:

  • Serial:

    ​ 单线程GC,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

  • ParNew:

    ​ 就是Serial的多线程版本,除了使用多条线程进行垃圾收集之外,其余都和Serial完全一样。老年代使用CMS时,新生代会默认使用 ParNew,也只有ParNew可以和CMS配合使用。

  • Paraller Scavenge:

    ​ 并行多线程收集器,server模式下默认GC的方式。停顿时间短,良好的响应速度可以提升用户体验。有自适应调节策略。

②老年代垃圾回收器:

  • Serial Old:

    是Servial的老年代版本,同样是一个单线程收集器,每次进行回收都很耗时。

  • Parallel Old:

    Parallel Scavenge收集器的老年代版本,多线程

  • CMS:

    一种获得最短停顿时间为目标的GC,目前用重视响应速度的服务端上(如BS架构)。使用XX:+UseConcMarkSweepGC指定使用。

优点:响应时间短

缺点:对CPU资源很敏感,会导致应用变慢,总吞吐量会降低

  • G1:

    并行与并发:充分利用多CPU的优势,使用多个CPU来缩短Stop-The-World停顿的时间。通过并发的形式让java程序据继续执行。

    分代收集:既可以收集新生代也可以收集老年代

    空间整合:采用标记-整理算法

    可预测停顿:可以建立可预测的停顿时间模型,从而达到降低停顿时间的要求。

文章作者: 郭远
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 郭远的博客空间
Java基础 java基础
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝