JVM知识回顾

一、JMM内存模型

    主要分为六块:程序计数器、本地方法栈、虚拟机栈、堆、方法区以及堆外内存。

  1. 程序计数器:存储当前线程所执行的字节码指令地址;
  2. 本地方法栈:存储本地native方法状态;
  3. 虚拟机栈:主要是栈帧,局部变量表、方法出口顺序出栈压栈这些,其实就是JVM给每一个线程开辟的工作内存,默认1M;
  4. 堆:作为JVM管理的最大一块内存,主要存储几乎所有的对象示例,但并非绝对,因为可以OSR栈上替换,比如简单的VO拆解成标量栈上分配;
  5. 方法区:主要是常量池、类信息,比如编译期的Class常量池、加载后的运行时常量池。方法区在Java8之后被拆分,类信息作为元数据空间被移到了机器内存中,常量池则留在了堆中,主要是为了避免触碰VM内存上限;
  6. 堆外内存:比如内存映射,通过Native函数直接分配堆外内存,作零拷贝;

    线程安全的就是计数器、本地方法栈、虚拟机栈,线程隔离、同生共死,而堆和方法区是线程共享的,比如我在堆里分配一块内存要加锁要线程同步。所以就有TLAB这种线程专用的内存区域来提高分配效率,也有PLAB作为线程专用的对象晋升的内存区域。

这里解释两个概念:

    本地线程缓冲区(TLAB):因为堆是线程共享的,每次对象分配都需要进行线程同步,拉低了分配效率。JVM利用一个线程专用的内存分配区域来避免这种冲突,以提升效率。每个线程使用自己的TLAB,并内部维护一个动态阈值(refill_waste值,表示TLAB的使用度,默认为64)。TLAB缺省值很小,仅占Eden空间的1%,所以无法对大对象进行担保分配。

    栈上替换(OSR):即栈上分配和标量替换,是指经过JIT实时编译后对象被拆散为标量类型分配在栈上,或将对象的属性视为独立局部变量在栈上分配

关于方法区中的字符串常量池(String Constant Pool),它在HotSpot VM里的实现是一个StringTable类,一个Hash表,默认长度是1009,在每个HotSpot VM的实例中只有一份,被所有的类共享。

字符串构建,都会使用到String.intern()这个Native方法:
1、在Java7之前,若常量池中已经存在这个String,则返回该字符串的引用;若没有,则将其加入常量池中,并返回该字符串的引用;
2、在Java7之后,若常量池中没有该字符串时,则将其加入堆中并在常量池中存储该字符串的引用,然后返回该引用;

 

1.  JVM的GC

    在Java7之前,Heap是这样的:新生代(Young)和老年代(Old),新生代可分为Eden空间和Survivor空间,Survivor又分为两个独立的From Survivor和To Survivor。

    

    JVM中,怎样为一个普通对象分配内存呢?
    1、JVM首先为新生对象在Eden区中申请一块内存区域;
    2、当Eden区空间不够时,触发一次 Young GC,在此期间JVM将Eden区所有存活的对象移动到其中一块Survivor区;
    3、如果Survivor区放不下,那么将剩下的存活了一定GC次数的对象提升到老年代;
    4、当老年代空间不够时,触发一次Full GC;
    5、Full GC后,如果Survivor区及老年代仍然无法存放从Eden区复制过来的对象,则意味着JVM无法为新生对象分配内存,即OOM;

    JVM一般,频繁收集生命周期短的Young,较少收集生命周期较长的Old,很少或基本不收集的PermGen。JVM在进行内存分配时有三个原则:

  1. 对象优先在Eden区分配;
  2. 大对象通过老年代担保,直接进入老年代,所以要避免临时大对象产生;
  3. 长期存活的对象进入老年代;

常见的Full GC原因:
    显式的System.gc()方法调用;
    Allocation Failure引起的Full GC;
    ClassLoader很多而永久代/元数据空间很小引发的Full GC;
    对象晋升失败,比如新生代提升速度过快而Survivor和老年代空间不够,触发Full GC;
    并发模式失败,比如CMS由于浮动垃圾需要预留空间进行内存分配但内存碎片严重,放不下晋升过来的大对象,从而引发Full GC;

 

 

二、垃圾收集器

    大体上是这样的:

    

    下面,我们来说说我们最常用到的是CMS和G1。

 

1.  CMS收集器

    CMS的回收算法分为这么几个阶段:

  1. 初始标记(STW)
  2. 并发标记
  3. 重新标记(STW)
  4. 并发清除

CMS通过三色标记法,进行垃圾收集:

    白:该对象未被标记;
    灰:该对象被标记了,但其引用的对象还没有被标记完;
    黑:该对象被标记了,并且其引用的对象也都被标记完成;

三色标记法的基本逻辑是:

  • 起初所有的对象都是白色;
  • 扫描出所有可达对象标记为灰色,放入待处理队列;
  • 拿出待处理队列中的灰色对象,将其引用的对象标记为灰色放入队列,并将其自身标记为黑色;
  • 通过写屏障事件捕获所有白色对象的内存修改,重新标色并放回队列。

    CMS的缺点:

  1. 对CPU资源敏感:
    默认启动的垃圾收集线程数 =(CPU核数+3)/ 4,也就是当CPU在4个以上时,垃圾收集线程占用不少于25%的CPU资源,且随着CPU数量的增加而下降。但当CPU不足4个时,CMS对应用程序的影响就变得非常大。如果本来CPU负载就比较大,还分出一半的资源去执行GC,很可能导致应用程序的执行速度忽然降低了50%,这让人无法接受;
  2. 浮动垃圾问题:因为清理阶段与应用程序是并发运行的,这段时间产生的新的垃圾只能在下个GC周期才能清除;
  3. 内存碎片问题:CMS因为使用标记-清除算法,产生大量不连续的内存碎片,遇大对象分配时不得不提前触发Young GC;

 

2.  G1收集器

    G1是Java8之后的主流垃圾收集器,主要是因为它提供了GC的并发性(部分GC过程与应用程序交替执行)和并行性(回收期间,多个GC线程同时工作,有效利用多核CPU)。而且会基于GC停顿时间来自适应性调整新生代和老年代比例,以及对象晋升年龄。

    Java7之后,Heap空间被整合为一个个可以不连续的Region,同时在Region内部,同样支持使用分代收集的方法,回收粒度更细了。Region在用途上也分为Eden Regions(新生代Eden区)、Survivor Regions(新生代Survivor区)、Old Regions(老年代区)、Humongous Regions(大对象区),而每个Region内部都由一个个Card组成。

Card标识Region最小可用粒度,所有Region的Card都会记录在全局Card Table表中:
1、分配的对象会占用物理上连续的若干个Card,被称为这个Card脏化了(dirty card)。当查找分区内对象的引用时便可通过记录Card来查找该引用对象,即points-out,我引用了谁的关系。每次对内存的回收,也是对指定Region的Card进行处理。
2、Card Table数据结构是一个字节数组,用单字节映射着每一个Card。

除此之外,还存在全局的待收集集合(CSet),CSet内部存在着已记忆集合(RSet),记录引用老年代对象的引用对象,即points-in,谁引用了我的关系。G1 GC时每次都会对新生代进行收集,因此引用新生代的对象,就不需要在RSet中记录。实际上,在GC时,只需对old->young和old->old的跨对象引用,才需要扫描对应的CSet中的RSet。

RSet在内部使用per_region_table(PRT)变量来记录Region的引用情况。由于RSet的记录要占用Region的空间,如果一个Region非常Hot,那么RSet占用的空间上升、分区的可用空间下降。所以,G1通过改变RSet的密度的方式来解决这个问题:

  • 稀少:直接记录引用对象的Card索引;
  • 细粒度:记录引用对象的Region索引;
  • 粗粒度:只记录引用情况,每个Region对应一个比特位;

    G1在整体上使用标记-整理算法,局部上(两个Region之间)则复制算法。那么JVM是如何进行GC的呢?分为这么几个阶段:

  1. 初始标记(STW)
  2. 根分区扫描
  3. 并发标记
  4. 重新标记(STW)
  5. 并发清除(Java10之后,是并行清除)

G1通过SATB(Snapshot at the beginning 起始快照)算法,进行垃圾收集。简单而言,就是在Region内比较一前一后两个PTAMS和NTAMS来找到变更的老年代引用关系,这可能会触发几次Young GC。基本的收集流程是:

    1.根扫描:静态和本地对象被扫描;
    2.更新RSet:处理dirty card队列;
    3.处理RSet:检测从年轻代指向老年代的对象;
    4.对象拷贝:拷贝存活的对象到survivor/old区域;
    5.处理引用队列:软引用、弱引用、虚引用处理;

 

3.  GC收集器失败情况

    一般有两种:晋升失败,或并发模式失败。

  1. 晋升失败(Promotion Failed):
    a、新生代提升过快,可能是新生代晋升阈值可能过小、Eden/Survivor空间过小,导致晋升速率提高;
    b、Survivor或老年代空间不够,可能是内存回收周期过长、老年代回收阈值过大或者总的堆容量过小,导致老年代中短期存活对象的变多,在GC之前老年代被填满,从而触发Full GC;
    c、老年代内存碎片严重,放不下大对象晋升
  2. 并发模式失败(Concurrent Mode Failure):
    e、若CMS在运行期间预留的内存无法满足内存分配,则会出现“并发模式失败”,导致JVM启动后备预案:临时启用Serial Old串行收集器来重新进行老年代收集,使得停顿时间更长;
    f、JVM参数 -XX:CMSInitiatingOccupancyFraction老年代回收阈值设置得太高,CMS触发的太晚,很容易导致大量的“并发模式失败”失败,性能反而降低。

 

 

转载于:https://my.oschina.net/duofuge/blog/3011688

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值