JVM 总结

JVM GC 总结。

周志明大大的《深入明了Java虚拟机》出第三版了,早早的买了这本书,却一直没有花时间看。最近抽闲温习了一下,感受又有了新的收获。这里简朴总结下。

GC的由来

由于的动态性,操作系统将堆交由给了开发者自己治理,手动申请,手动释放。对于C++,则是将这个权限继续交给了开发者,而对于Java,则是将这个历程自动化了。为什么要释放内存呢?最简朴的缘故原由就是操作系统一共给你了4G的内存空间,你需要的时刻,就去借用。有借有还,再借不难,只借不还,最后4G内存空间被用完了,你就无法再申请新的内存了。内存泄露,就是只借不还。

JVM在操作系统与开发者之间又封装了一层,间接的接管了内存的划分。同时也将堆统一治理起来,使得开发者只管借用内存,由JVM卖力接纳,领会JVM的接纳机制,明了它的原理,能让开发者在差别的场景下,定制差别的接纳规则,提高接纳效率。

关于GC的思索

若是让我设计一个能自动接纳垃圾的虚拟机,我会怎么设计呢?

  • 什么时刻最先接纳?
  • 怎么判断这部门内存可以接纳?
  • 怎么接纳这部门的垃圾?

这3个问题,也是JVM开发者一直在思索的问题。之前简朴领会过JVM,就知道JVM会有Stop The World的问题,这对于用户体验来说异常欠好,其根本缘故原由即是由于在接纳垃圾的时刻,用户线程可能会修改这部门内存,若是不暂停用户线程,则可能会导致严重的问题,而若何削减Stop The World的时刻,甚至让其消逝,是各个垃圾接纳器一直追求的目的。

哪些内存可以接纳?

对于一个工具来说,当不存在任何一个引用能够访问到这个工具的时刻,则说明这个工具可以举行接纳。由于没有任何引用指向这个工具,那么这个工具就不能被读或写。

  • 引用计数法

    前面说判断一个工具可以被接纳的尺度就是是否另有引用指向这个工具,以是最容易想到的即是引用计数法,通过判断一个工具的引用数目即可,可是这样无法判断两个循环引用的工具。

  • 可达性剖析

    可达性剖析指的是从现在程序中正在使用的所有引用的工具出发,循环遍历所有能找到的工具。

    作为出发的点的这些工具,被称为GC Roots

    GC Roots主要包罗以下几种:

    • 在虚拟机栈(好比栈帧中的内陆遍历表)中引用的工具
    • 静态属性引用的工具
    • 常量池引用工具(好比
      String Table
    • 内陆方式栈引用的工具
    • Java虚拟机内部的引用对应的工具
    • 所有被同步锁持有的工具

    总体来说,就是当前程序中正在被使用的引用所指向的工具会被作为GC Roots

GC Roots出发,依次查找,就能符号出当前存活的工具。然则符号这个历程,细节上依然存在问题:

  • STW : 符号是通过引用查找工具的,若是在符号历程中,用户修改了引用的工具,那么会导致不能预估的结果,因此一样平常符号历程中,是会STW

  • 跨代符号 : 现在的垃圾接纳器,大多数都是分代,或者分区域接纳的,也就是说,可能举行垃圾接纳的时刻,不是符号所有的垃圾,而是符号一部门,好比老年月或者新生代。此时就存在一个问题,跨代引用。好比一个新生代的工具,仅仅被一个老年月工具引用的话,对于Yong GC来说,是不会扫描老年月工具的,这个时刻就会造成误判。解决这个误判的方式即是影象集(Remembered Set),影象集通过AOP手艺天生写屏障来维护。

    前面说了从GC Roots最先扫面,那分代网络的,怎么知道哪些工具是新生代的,哪些工具是老年月的呢?由于GC Roots是包含了所有引用的。后面想想,实在工具的分代信息是存放在工具头里面的。在扫描GC Roots的时刻,只保留新生代的工具即可。这样基本能保证扫描到的是新生代工具,然后老年月对新生代引用交给影象集实现就行(自己的预测,没有证据)

    JVM书中说道通过AOP天生的写屏障会使得只要有更新操作,无论更新的是不是老年月对新生代工具的引用,都市使卡表变脏,不外这样的价值相对来说是能接受的。

  • GC Roots 需要扫描的引用过多 :随着现在Java应用越做越大,Java堆也越来越大,GC Roots的扫描是需要STW的,若是每次GC都逐个扫描,会异常的浪费时间。解决这个问题的设施就是OopMap,使用OopMap纪录应用程序所存放的引用,每次需要GC的时刻扫描这个OopMap即可天生对应的GC RootsOopMap通过平安点和平安区域来维护,只有在平安点或平安区域的时刻,才更新OopMap和举行垃圾接纳。

  • 并发符号历程可能丢失存活的工具 :从CMSG1,都将从GC Roots出发符号存活工具的历程修改成并发的,这样会需要解决的问题就是符号历程中若是用户修改了工具的引用,可能会导致本应该存活的工具”丢失“(可以通过三色符号剖析),响应的解决方案即是损坏存活工具消逝的必要条件,分别是增量更新(Incremental Upate)和原始快照(Snapshot At The Begin,SATB),增量更新损坏的是第一个条件,每插入一个引用,就都纪录下来,而原始快照损坏的是第二个条件,每删除一个,都将其纪录下来。

    增量更新和并发快照也是通过前面所说的AOP手艺天生写屏障来维护

通过以上剖析以及解决方案,基本明了了怎么符号那些内存可以接纳,接下来需要剖析的就是什么时刻最先接纳

什么时刻最先内存接纳?

对于内存接纳来说,最先也需要有一定的讲求,理论上来说,随时随地都可以最先内存接纳,然则若是接纳时使用的内存过多,会导致GC时间历程,进而STW时间也会很长,若是接纳过于频仍,又会导致吞吐量下降,究竟每次扫描GC Roots都回STW的。

谈谈spring-boot-starter-data-redis序列化

同时,前面还说过,对于用户线程来说,需要将用户线程运行到平安点,更新对应的OopMap,才气最先垃圾接纳。

因此,对应何时GC,有以下几点剖析:

  • 对于新生代来说,一样平常新生代满了(Eden + Survivor1)就会最先举行(Yong/Minor GC

  • 对于老年月来说,一样平常是老年月满了了会最先Full/Major GC

    注重:这里的满了,需要凭据详细的接纳器差别,来权衡真正的满,对于没有并发历程的GC,老年月满一样平常指的是真正到达100%,已经无法分配内存了,对于有并发历程的GC,则需要预留出来空间给用户线程在并发历程中同时申请内存,若是预留内存过小,则会使用非并发垃圾接纳器举行Full GC

    CMS: -XX:CMSInitiatingOccupancyFraction 设置,默认92% (JDK 8),示意当老年月垃圾占用到92%就最先老年月接纳, JDK 9后便无法使用CMS

    G1: -XX:G1ReservePercent设置,默以为10,示意当整个Java堆使用到达90%,就最先接纳。同时配合的参数另有-XX:InitiatingHeapOccupancyPercent=n,默认值为45,示意使用率到达45%就启动符号周期。这里的GCMixed GC

    一样平常来说,只有CMS才有Major GC,其他老年月GC都市接纳整个Java堆,也称为Full GC

  • 统计获得的Minor GC提升到老年月的平均巨细大于老年月剩余的空间。(JDK 6 之后已经删除了担保规则)

  • GC并发失败(concurrent mode failure): 情形如前面说的,并发符号历程中,又泛起了新生代提升的情形,然则此时老年月剩下的内存不足够放下提升的工具的时刻,会生导致Full GC

    这里的Full GC和情形1中说到达预留空间的GC不一样,情形1是正常举行的GC,而这个并发失败却是GC历程中泛起了异常,一样平常需要切换到非并发GC,此时性能会大大下降

  • 方式区区域被使用完毕:JDK 8之后将方式区从Perm Gen替换成了元空间,一样平常来说元空间巨细理论上即是内陆内存巨细,不外元空间有一个默认初始值,到达默认初始值后,会通过Full GC扩大

    注重:G1只有Yong GCMixed GC。没有Full GC的观点,也就是说若是需要接纳方式区的话,只能退化为Serial GC举行Full GC

    CMS可以通过-XX:+CMSClassUnloadingEnabled设置并发接纳方式区

  • 最大延续空间装不下大工具:对于CMS,基于符号-消灭算法来说,纵然空间足够,然则由于内存碎片,装不下分配的大工具时,会举行一次Full GC,对于G1来说,当分配巨型工具的时刻,若是在老年月无法找到延续的Humongous的时刻,会举行Full GC

  • 用户执行System.gc(),可以通过-XX:+DisableExplicitGC屏障

怎么接纳这些内存

最后一步即是怎么接纳这些内存。怎么接纳,书中先容不多,总体来说有以下三种:

  • 符号-消灭(
    Mark-Sweep):最原始的方式,实现简朴,不用移动工具,很容易做到不用
    Stop The Word,然则瑕玷也很致命,容易发生内存碎片。符号消灭的速率一样平常,
    Mark阶段与活工具的数目成正比,
    Sweep阶段与整堆巨细成正比。现在只有
    CMS使用这种接纳方案
  • 符号-复制(
    Mark-Copying):基于符号-消灭修改的垃圾接纳算法,需要移动工具。 前期符号,然后复制活下来的工具到另一个区域,再总体接纳整块区域。符号复制算法对于新生代这种专门放朝生夕死的工具效率异常高,由于存活下来的工具少,以是
    Mark阶段和
    Copying阶段破费的时间都市对照少,险些所有的分代
    GC新生代都是使用的这种算法
  • 符号-整理(
    Mark-Compact): 基于符号-消灭算法修改的垃圾接纳器,需要移动工具。前期符号,然后将所有工具移动到一起,再对剩余的区域举行接纳,速率最慢,然则不会发生内存碎片。

对于新生代使用符号-复制算法,是毋庸置疑的。然则对于老年月,使用符号消灭照样符号整理,需要有一定的考量。由于使用符号-消灭,不用移动工具,速率会相对来说对照快,然则由于存在内存碎片,无法使用指针碰撞的方式分配内存,而不得不使用“分区空闲分配链表”来解决内存分配的问题,这样会对在内存分配带来一定的效率影响,而符号-整理算法需要移动工具,特别是对于老年月这种大工具来说,移动这些工具将是一种极为负重的操作,然则符号-整理不会发生内存碎片。

因此,基于以上思量,对于CMS这种偏重响应速率,致力于削减STW时间的接纳器来说,选择了符号-消灭算法,然则由于内存分配是一个异常频仍的操作,使用”分区空闲分配链表”会降低整个垃圾接纳器的吞吐量,因此,对于Parllel Scavenge这种注重接纳吞吐的垃圾接纳器来说,选择了符号-整理算法。固然,对于G1则是吞吐和响应速率都对照注重,权衡之下,选择了符号-整理(全局)算法。

GC的观点,到这里基本总结完毕,然则,若是仅仅是理论,只是让我们记着一些观点性的器械,接下来,我会连系CMSG1GC日志以及《深入明了JVM》第四章的内容,聊一聊若何剖析以及查看GC历程,简朴先容若是举行GC调优。

小我私家民众号:

JVM 总结

不定期更新一些经典Java书籍总结。

原创文章,作者:admin,如若转载,请注明出处:https://www.2lxm.com/archives/17864.html