learn and grow up

浅析升级jdk8对jvm/GC(HotSpot)的影响

字数统计: 3.1k阅读时长: 10 min
2019/09/14 Share

jvm内存结构变化

  • 不变点

    1、堆—–堆是所有线程共享的,主要用来存储对象。其中,堆可分为:年轻代和老年代两块区域,

    ​ 老年代与年轻代比例使用NewRatio参数来设定比例。比如: –XX:NewRatio=2表示新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,这也是jdk8的默认比例,也就是老年代 ( Old ) = 2/3 的堆空间大小。

    ​ 对于年轻代,一个Eden区和两个Suvivor区(from,to),使用参数SuvivorRatio来设定大小,比如:

    –XX:SurvivorRatio=8表示Edem : from : to = 8 : 1 : 1,即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。这也是jdk8的默认比例,众所周知:JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。在这里也就是10%是空闲的

    2、Java虚拟机栈—-线程私有的,主要存放局部变量表,操作数栈,动态链接和方法出口等;

    3、本地方法栈—-线程私有的,主要同java虚拟机栈一样,只是保存的是本地方法(Native方法)相关属性;

    4、程序计数器—-同样是线程私有的,记录当前线程的行号指示器,为线程的切换提供保障;

  • 变化点

    8以前:PermGen Space—-线程共享的,主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。方法区理论上来说是堆的逻辑组成部分;运行时常量池——是方法区的一部分,用于存放编译期生成的各种字面量和符号引用;设置方法:

    1
    2
    3
    4
    5
    -XX:PermSize
    方法区初始大小
    -XX:MaxPermSize
    方法区最大大小
    超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

    8以后:移除了PermGen Space,代替它的是元空间(Metaspace),设置方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    -XX:MaxMetaspaceSize
    参数可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。
    -XX:MetaspaceSize
    这个参数是初始化的Metaspace大小,该值越大触发Metaspace GC的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控Metaspace的大小,可能增加上限也可能降低。
    -XX:MinMetaspaceFreeRatio
    当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增长Metaspace的大小。在本机该参数的默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
    -XX:MaxMetasaceFreeRatio=N
    当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。
    -XX:MaxMetaspaceExpansion=N
    Metaspace增长时的最大幅度
    -XX:MinMetaspaceExpansion=N
    Metaspace增长时的最小幅度

    注意:PermGen != 方法区,“PermGen space”是指永久代。方法区和“PermGen space”有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

    ​ 其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

    ​ 这么做可能有以下原因:

    ​ 1、字符串存在永久代中,容易出现性能问题和内存溢出。

    ​ 2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

    ​ 3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

    ​ 4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

对象是否存活方法

jdk8也没有变化

1、引用计数算法(已废弃)

​ 垃圾收集的早期策略,在这中方法中,堆中每个对象都有一个引用计数,每当有一个地方引用他时,引用计数值就+1,当引用失效时,引用计数值就-1,任何时刻引用计数值为0的对象就是可以被回收,当一个对象被垃圾收集时,被它引用 的对象引用计数值就-1,所以在这种方法中一个对象被垃圾收集会导致后续其他对象的垃圾收集行动。

优点:判定效率高;

缺点:不完全准确,当两个对象相互引用的时候就无法回收,导致内存泄漏。

2、可达性分析法

​ 这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。Finalize方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize中成功拯救自己—-只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

​ 可作为GC Roots对象的包括如下几种:

​ a.虚拟机栈(栈桢中的本地变量表)中的引用的对象

b.方法区中的类静态属性引用的对象

c.方法区中的常量引用的对像

​ d.本地方法栈中JNI的引用的对象

gc算法

这个没有变化,主要还是有:标记-清除算法、复制算法(年轻代使用的算法)、标记-整理算法(老年代使用的算法)

垃圾回收器

jdk8默认的垃圾回收器为:ParallelGC

可通过如下命令查看,如下图:

1
2
3
java -XX:+PrintCommandLineFlags -version

java -XX:+PrintGCDetails -version

jdk8默认的垃圾回收器

垃圾回收器分类:

垃圾回收器分类

  • Serial收集器

    一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。
    特点:CPU利用率最高,停顿时间即用户等待时间比较长。
    适用场景:小型应用
    通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

  • Parallel收集器

    采用多线程来通过扫描并压缩堆
    特点:停顿时间短,回收效率高,对吞吐量要求高。
    适用场景:大型应用,科学计算,大规模数据采集等。
    通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

  • CMS收集器
    采用“标记-清除”算法实现,使用多线程的算法去扫描堆,对发现未使用的对象进行回收。
    (1)初始标记
    (2)并发标记
    (3)并发预处理
    (4)重新标记
    (5)并发清除
    (6)并发重置
    特点:响应时间优先,减少垃圾收集停顿时间
    适应场景:服务器、电信领域等。
    通过JVM参数 -XX:+UseConcMarkSweepGC设置

    但是CMS并不完美,它有以下缺点:

    1、由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间;
    2、标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。

  • G1收集器(jdk9默认
    在G1中,堆被划分成 许多个连续的区域(region)。采用G1算法进行回收,吸收了CMS收集器特点。
    特点:支持很大的堆,高吞吐量
    –支持多CPU和垃圾回收线程
    –在主线程暂停的情况下,使用并行收集
    –在主线程运行的情况下,使用并发收集
    实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收
    通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器

    G1垃圾收集器也是以关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是一个非常具有调优潜力的垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很大不同:

    1、G1的设计原则是”首先收集尽可能多的垃圾(Garbage First)”。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
    2、G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
    3、G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
    4、G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

当然,Parallel、CMS、G1各有个的优势,我们需要根据项目实际情况来调整jvm的垃圾回收器的之间的搭配

以下为各个垃圾回收器间的搭配

搭配

包含:1、串行收集器组合 Serial + Serial Old,

2、并行收集器组合 Parallel Scavenge + Parallel Old

3、并发标记清除收集器组合 ParNew + CMS + Serial Old

4、Garbage First (G1)

CATALOG
  1. 1. jvm内存结构变化
  2. 2. 对象是否存活方法
  3. 3. gc算法
  4. 4. 垃圾回收器