【JVM学习笔记二】垃圾收集器与内存分配策略

那年仲夏 提交于 2019-12-04 18:26:17

1、 概述
  1) GC的历史比Java久远
  2) GC需要完成的三件事:
    | 哪些内存需要回收
    | 什么时候回收
    | 如何回收
  3) Java内存运行时区域各个部分:
    | Java虚拟机栈、计数器、本地方法栈随线程而生,随线程而灭,不需要考虑太多问题,因为方法的结束或者线程结束时,内存自然就回收了
    | Java堆和方法区只有在运行时才知道需要的内存,分配和回收都是动态的,垃圾收集器所关注的是这部分内存

2、 对象已死吗?
  1) 引用计数算法(Reference Counting)
    | 思路:
      给对象添加引用计数器,一个地方引用+1,引用失效-1,为0不可能再被使用
    | 优点:
      判定效率高,大部分情况下都是一个不错的算法
    | 缺点:
      很难解决对象间相互循环引用的问题 [主流的Java虚拟机里面没有选用引用计数算法来管理内存]
  2) 可达性分析算法(Reachability Analysis)
    | 思路:
      通过一系列的“GC Roots”的对象作为起始点,往下搜索,走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用
    | 可作为GC Roots的对象:
      虚拟机栈(局部变量表)中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI(即一般说的Native方法)引用的对象
  3) 引用
    JDK1.2之后,引用分为
      强引用:Object obj = new Object(),只要存在,不会回收
      软引用:描述一些还有用但非必需的对象。发生内存溢出之前,将会把这些对象二次回收,还没有足够的内存才会抛出内存溢出。提供SoftReference类来实现软引用
      弱引用:描述非必需对象,强度比软引用更弱,被弱引用关联的对象只能生存到下次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。提供WeakReference类来实现
      虚引用:最弱的一种引用关系,不会影响对象生存时间,也无法通过虚引用取得对象。唯一目的就是能在这个对象呗收集器回收时收到一个系统通知。提供PhantomReference类来实现
  4) 自救
    | 对象确认死亡需要两次标记:可达性性分析算法不可达;没有重写finalize()方法或finalize()方法以执行完毕
    | 可以通过重写finalize()方法,关联其他GC Roots对象。
    | finalize()只会执行一次;方法不一定会被虚拟机正确的执行完毕;尽量避免使用
  5) 回收方法区
    | 永久代的垃圾收集主要回收两部分:废弃常量和无用的类
    | 无用的类满足条件:
      所有实例已被回收,堆中不存在该类的任何实例
      加载该类的ClassLoader已被回收
      该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法
    | HotSpot虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class(Product版的虚拟机中使用) 以及 -XX:+TraceClassLoading(需要FastDebug版虚拟机支持) 查看类加载和卸载信息
    | 在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出

3、 垃圾收集算法
  1) 标记-清除算法(Mark-Sweep)
    | 分为“标记”和“清除”两个部分:首先标记需要回收的对象,完成后统一回收被标记的对象
    | 不足:两个过程效率都不高;产生大量不连续的内存碎片,导致需要分配大对象时无法找到足够内存触发另一次GC
  2) 复制算法(Coping)
    | 将内存按容量划分成大小相等的两块,每次只用其中一块。这块用完了就把存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
    | 优点:不用考虑内存碎片等复杂情况,只要堆顶指针,按顺序分配即可,实现简单,运行高效
    | 缺点:内存缩小为原来的一半,代价太高;在存活率高时就要进行较多的复制,效率将会变低;需要额外空间担保
    | 改进:
      研究表明:新生代中的对象98%是朝生夕死,所以并不用按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor
      HotSpot虚拟机默认Eden和Survivor的大小比例是8:1
      当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保
  3) 标记-整理算法(Mark-Compact)
    | 标记过程和标记清除相同,但后续是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存
  4) 分代手记算法(Generational Collection)
    | 把Java堆分为新生代和老年代,根据各个年代的特点采用适当的收集算法
    | 年轻代:复制算法
    | 老年代:标记-清除算法或标记-整理算法

4、 HotSpot的算法实现
  1) 枚举根节点
    | 准确式GC:能知道哪个地方上放置的是对象的引用,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来
    | JIT编译(动态编译,一边运行一边把字节码编译为机器指令)中,也会在特定的位置记录下栈和寄存器中的哪些位置是引用
    | HotSpot中以上的内容都存放在OopMap的数据结构中
  2) 安全点(Safepoint)
    | OopMap内容变化的指令非常多,如果为每一个条指令都生成对应的OopMap,将需要大量的额外空间。HotSpot没有为每条指令都生成OoMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点
    | 总的来说,安全点就是指,当线程运行到这类位置时(方法调用、循环跳转、异常跳转等),堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等
    | GC时所有线程要跑到安全点上再停顿下来:抢先式中断(虚拟机GC时把所有线程中断,恢复不在安全点的线程,让它进入安全点)和主动式中断(虚拟机GC设置标识,线程到达安全点时轮询,为真时自己中断挂起)
  3) 安全区域(Safe Region)
    | Safepoint无法解决一些问题:当程序“不执行”的时候,就是指程序为分配CPU时间(如Sleep和Blocked状态),这时候线程无法响应JVM的中断请求,这时就需要安全区域
    | 安全区域是指在一段代码中引用关系不会发生变化,在这个区域中的任何地方开始GC都是安全的。
    | 在线程执行到Safe Region时标识自己,JVM要发起GC时就不用管此线程

5、 垃圾收集器
  1) Serial收集器
    | 新生代收集器;复制算法;“单线程”:只会用一条线程和一个CPU进行垃圾收集,收集时必须暂停所有用户线程(Stop The World)
    | 优点:简单高效,单个CPU环境无线程交互,获得更高的收集效率;桌面应用场景中,分配的内存较小时,停顿时间可以接受;对于运行在Client模式下的虚拟机是很好的选择,也是默认的收集器
    | 缺点:CPU多时单线程处理效率低下;管理内存较大时,停顿时间长,用户体验差;
    | 可搭配的老年代收集器:Serial Old,CMS
  2) ParNew收集器
    | 新生代收集器;复制算法;Serial收集器的多线程版本(暂停用户线程、Stop The World)
    | 优点:多核CPU资源利用好;可以配合CMS老年代收集器,是Server模式下默认新生代收集器;
    | 缺点:仍存在STW;CPU数量较少时收集效率不一定比Serial好
    | 可搭配的老年代收集器:Serial Old,CMS
  3) Parallel Scavenge收集器
    | 新生代收集器;复制算法;并行多线程
    | 和ParNew的不同:目标是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),拥有自适应的内存管理调节(-XX:+UseAdaptiveSizePolicy)
    | 可搭配的老年代收集器:Parallel Old,Serial Old
  4) Serial Old收集器
    | 老年代收集器;标记-整理算法;单线程收集器,Serial收集器的老年代版
    | 优点:Client模式下的虚拟机使用;JDK 1.5之前与Parallel Scavenge收集器搭配使用;作为CMS收集器的后备预案;
    | 缺点:单线程;Stop The World
    | 可搭配的新生代收集器:Serial,ParNew,Parallel Scavenge
  5) Parallel Old收集器
    | 老年代收集器;标记-整理算法;多线程收集,Parallel Scavenge的老年代版
    | 优点:JDK 1.6之后才出现,和Parallel搭配组合,适用注重吞吐量和CPU资源敏感的场合
    | 可搭配的新生代收集器:Parallel Scavenge
  6) CMS收集器(Concurrent Mark Sweep)
    | 老年代收集器;标记-清除算法;以获取最短回收停顿时间为目标的收集器
    | 运行步骤:
      初始标记:标记GC Roots能直接关联到的对象,单线程,速度很快;Stop The World;
      并发标记:进行GC Roots Tracing(对象追踪标记)的过程
      重新标记:修正并发标记期间因用户程序继续运行而导致标记变动的对象的标记记录;Stop The World,多线程,停顿时间会比初始标记长;
      并发清除:清除可回收对象
    | 优点:系统停顿时间短,适合B/S系统服务端;Server模式默认老年代收集器
    | 缺点:对CPU资源非常敏感(默认启动回收线程数是:CPU数量+3/4);无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC产生(由于并发收集,垃圾收集时,还会产生垃圾,);收集结束时会产生大量空间碎片(标记-清除算法通病)
    | 可搭配的新生代收集器:ParNew,Serial
  7) G1收集器(Garbage-First)
    | 老年代、新生代皆可回收;当今收集器技术发展的最前沿成果之一;
    | 优点:
      并行与并发(充分利用多核优势缩短Stop The World时间,可以通过并发方式让Java程序继续执行)
      分代收集(能独立管理堆,采用不同方式去处理新创建的对象和旧对象以获取更好的收集效果)
      空间整合(整体看是基于“标记-整理”算法实现,从两个Region之间来看是基于“复制算法实现)
      可预测的停顿(追求低停顿,还能建立可预测的停顿时间模型,能让使用者指定在一个长度为M毫秒的时间片段内,垃圾收集时间消耗不超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了)
    | 设计:
      它将整个Java堆分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离的;
      之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集:G1跟踪各个Region里的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First名称的来由);
      G1中Region之间对象引用和其他收集器中新生代与老年代之间的引用,虚拟机都是使用Remembered Set来避免全堆扫描,每当对Reference类型数据写入时,检查引用是否位于不同Region,
      通过CardTable记录到引用所在Region的Remembered Set;
    | 步骤:
      初始标记:标记GC Roots能直接关联到的对象,单线程,速度很快;Stop The World;
      并发标记:进行GC Roots Tracing(对象追踪标记)的过程
      最终标记:修正并发标记期间因用户程序继续运行而导致标记变动的对象的标记记录;通过Remembered Set Log实现;Stop The World,多线程,停顿时间会比初始标记长;
      筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定计划;可以做到和用户程序并发执行,但是停顿用户线程将大幅提高收集效率;
  8) 理解GC日志(-XX:+PrintGCDetails -XX:+PrintGCDateStamps)
    例子:2019-11-17T08:52:55.952-0800: [GC (System.gc()) [PSYoungGen: 41304K->21754K(305664K)] 41304K->21762K(1005056K), 0.0199646 secs] [Times: user=0.20 sys=0.02, real=0.02 secs]
    说明:时间(系统运行时间)、[GC类型(GC发生原因)[GC发生区域(不同收集器区域名称不同):回收前使用区域->回收后使用区域(该区域总大小)]回收前堆使用大小->回收后堆使用大小(堆总大小),GC时间] [时间详情:用户态时间(多核会累加) CPU系统时间(多核会累加) 真实时间]
    GC类型:
      | Minor GC(新生代发生的GC,由于大部分对象都是朝生夕死,所以速度很快)
      | Major GC/Full GC(老年代发生的GC,且至少伴随一次Minor GC,速度很慢)
  9) 垃圾收集器参数总结

       参数                     适用收集器            说明                                                                         使用
            UseSerialGC                -                    Client模式默认值,开启Serial+Serial Old收集器组合                                -XX:+UseSerailGC
            UseParNewGC             -                    开启ParNew+Serial Old收集器组合                                                -XX:+UseParNewGC
            UseConcMarkSweepGC        -                    Server模式默认值(1.7之前),开启ParNew+CWS+Serial Old收集器组合                    -XX:+UseConcMarkSwapGC
            UseParallelGC            -                    Server模式默认值(1.8之后),开启Parallel Scavenge+Serial Old收集器组合            -XX:+UseParallelGC
            UseParallelOldGC        -                    开启Parallel Scavenge+Parallel Old收集器组合                                    -XX:+UseParallelOldGC
            SurvivorRatio            -                    新生代中Eden和Survivor比例,默认为8,即Eden:Survivor=8:1                        -XX:SurvivorRatio=8
            PretenureSizeThreshold    ParNew、Serial        直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配        -XX:PretenureSizeThreshold=1024000
            MaxTenuringThreshold    -                    晋升到老年代对象需要的年龄,即坚持过Minor GC的次数,默认15                            -XX:MaxTenuringThreshold=15
            UseAdaptiveSizePolicy    Parallel Scavenge     动态调整Java堆中各个区域的大小以及进入老年代的年龄,不会调优是很适合的选择            -XX:+UseAdaptiveSizePolicy
            HandlePromotionFailure     -                    是否允许担保失败,即老年代的剩余空间不足以应付新生代所有对象存活的情况(下见详情说明)    -XX:+HandlePromotionFailure
            ParallelThreads            -                     设置并行GC时进行内存回收的线程数    (默认CPU数量)                                        -XX:ParallelThreads=2
            GCTimeRatio                 Parallel Scavenge     GC时间占总时间的比率,默认99                                                     -XX:GCTimeRatio=99
            MaxGCPauseMillis        Parallel Scavenge     设置GC的最大停顿时间,单位毫秒                                                    -XX:MaxGCPauseMillis=200
            CMSInitialtionOccupancyFraction    CMS         设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认68%,太高容易引起”CMF“(上文)    -XX:CMSInitialtionOccupancyFraction=90%
            UseCMSCompactAtFullCollection    CMS         设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理                                -XX:-UseCMSCompactAtFullCollection
            CMSFullGCsBeforeCompaction        CMS         设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理                            -XX:CMSFullGCsBeforeCompaction=3

     其他说明:
      PretenureSizeThreshold默认值为0,表示所有对象会先尝试在新生代中分配内存,失败后再去老年代分配;主要目的是为了防止大对象(如没有任何对象引用的数组、长字符串)在Eden和Survivor区域间复制

6、 内存分配与回收策略
  1) 对象优先在Eden分配
    | 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
  2) 大对象直接进入老年代
    | 大对象是指需要大量连续内存空间的Java对象,最典型的就是很长的字符串和数组(byte[]),应当避免”短命大对象“;经常出现大对象容易导致内存还有不少空间时就触发垃圾收集以获取连续内存
    | 大于-XX:PretenureSizeThreshold设置值得对象直接在老年代分配;目的是为了避免大对象在Eden和Survivor之间的复制
  3) 长期存活的对象将进入老年代
    | 虚拟机为了分代管理对象,给每个对象定义了一个对象年龄(Age)计数器,初始0,每经过一次Minor GC年龄增加1
    | -XX:MaxTenuringThreshold可设置对象晋升老年代的年龄
  4) 动态年龄判断
    | 虚拟机并不是什么时候都要求对象达到MaxTenuringThreshold时候才可以晋升到老年代
    | 当Survivor区域中相同年龄所有对象大小的总和大于Survivor区域的一半时候,大于等于该年龄的对象可以直接进入老年代
  5) 空间分配担保
    | Minor GC之前,虚拟机先回检查老年代的最大连续空间是否大于新生代对象总和,否的话代表此次GC有风险。
    | 当Minor GC存在风险时,会先检查HandlePromotionFailure(允许担保失败)是否开启,如果开启会检查老年代最大连续空间是否大于新生代历次晋升到老年代对象的平均大小,如果大于,则进行Minor GC
    | 如果HandlePromotionFailure未开启,或者小于,则会先进行一次Full GC
    | 如果出现了HandlePromotionFailure失败,失败后会重新发起一次Full GC

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!