jvm内存模型与GC知识(深入理解java虚拟机读书笔记-3)

jvm内存模型与GC

内存模型

  • jvm的内存模型分为方法区、堆、虚拟机栈、本地方法栈、程序计数器与直接内存。

    下面列表介绍

    内存区域名称内存区域内容描述
    方法区(元空间)元空间并不在虚拟机中,而是使用本地内存。存储类的信息,方法数据,方法代码等
    方法区(运行时常量池)jvm规范没有对这部分内存做规定,一般是静态常量池(Class文件)中的编译期生成的各种字面量与符号引用。运行时常量池具有动态性,运行期间也可以将新的常量放在池中。
    唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
    虚拟机栈生命周期与线程相同,存放操作数、动态连接、方法出口等信息
    本地方法栈本地方法使用的栈
    程序计数器线程行号指示器
    直接内存不是jvm运行时数据区的内容,但在NIO中使用Native函数库可直接分配堆外内存,就是直接内存

    表格中前3项是线程共享的,后边都是线程私有的。

    • 静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。

      在这里插入图片描述
      上图引用其他博客

    • 本地方法:本地方法是由其它语言编写的,编译成和处理器相关的机器代码。本地方法保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的。JAVA方法是与平台无关的,但是本地方法不是运行中的JAVA方法。调用本地方法时,虚拟机装载包含这个本地方法的动态库的,并调用这个方法。

GC与内存分配策略

GC

GC是针对堆的。

  • 引用计数算法:给每个对象添加一个引用计数器,有引用就+1,无引用就-1,归零就把对象清除。

    优点:实现简单

    缺点:需要考虑很多例外情况,特别是对象间的循环引用问题。

    现实中的jvm实现都没有用引用计数算法。

  • 可达性分析算法:确定一系列对象为GC Roots根对象,沿着根对象进行图搜索,所有无法到达的对象都是需要被回收的。

    • GC Roots对象一般包含:在虚拟机及本地方法栈中引用的对象、方法区中静态属性引用的对象与常量引用的对象(如字符串常量池里的引用)、jvm内部的引用如Class对象常用异常对象、被同步锁持有的对象、反映jvm内部情况的对象。
    • 现有的jvm实现都支持局部回收:只针对堆中某一块区域的回收。
  • java的概念中如果只有有无两种引用状态还不是很灵活,因此现在有4中引用状态。

    引用类型说明
    强引用就是java中new一个对象这种引用,引用还在就不会被回收。
    软引用还有用但非必须,系统发生内存溢出前会被回收,使用SoftReference类实现
    弱引用非必须对象,只能生存到发生下一次回收为止,使用WeakReference类实现
    虚引用存在的唯一目的是被回收时收到一个系统通知,使用PhantomReference类实现。
  • 被判定没有被引用的对象,被判了缓刑->jvm判断是否需要执行对象的finalize()方法,不需要或已经执行过就回收,判定需要执行就执行一次,如果对象成功在finalize中被引用就自救成功,否则失败。finalize的执行优先级很低。不同于C++中的析构函数,java不推荐使用finalize方法。

  • 方法区也是可以被回收的,但性价比比较低,条件比较苛刻,但在大量使用反射、动态代理、CGlib及频繁使用自定义类加载器的场景中应对方法区进行回收。

GC算法

  • 分代收集理论是一种GC收集的思想,绝大多数jvm实现采用这种方式,但它也有缺陷,现在有了新的算法。

  • 思想:绝大多数对象的生命周期很短,熬过多次GC的对象多半还会继续生存

    据此就可以只对新生代的对象进行部分GC,引出另外一条思想:**跨代引用相比与同代引用只占少数,大多数跨代引用的新生代对象最终会进入老年代。**针对跨代引用问题,可将老年代的对象分块,只把存在跨代引用的老年块加入到GC Roots中进行搜索。

  • 标记-清除算法:很简单,就是标记上需要回收的对象并清除,但会造成执行效率不稳定(如果有大量的需要清除的对象会有很大工作量)与内存碎片化。

  • 标记-复制算法:只关乎新生代内存区,在新生代中设计大容量的eden区与2个小容量的sui区,他们空间的比率大约是8:1.每一个GC,将Eden与一块sur区中的存活对象都复制到另一块ser区中,并清除eden区与原来的sur区。这样就只浪费了一个sur区的空间。但有可能某次GC后sur空间存不下存活的所有对象,这就需要把一部分对象直接放到老年代中去。

  • 标记-整理算法:把所有存活的对象都紧凑的放置在一起后清除其余内存内容,主要用于老年代的GC。缺点是GC需要大量的时间,但节省了内存分配的时间,鉴于内存分配与访问情况比GC更多,因此标记-整理算法是合算的。

  • 也可以先采用标记-清除等碎片太多后再使用标记-整理。

HotSpot算法细节

  • 根节点枚举:对GC Roots集合的元素进行枚举,所有的jvm与GC实现都需要在枚举根节点的时候进行全局停顿。对于在栈中的对象等,主流的jvm会维持一个OopMap来保存他们在堆中的引用地址,这样就不用每次都重新寻找了。

  • 安全点:在实践中,不可能没个指令都生成对应的OopMap,这就像游戏存档一样,需要程序执行到安全点,安全点一般选取在方法调用、循环跳转、异常跳转等时机。几乎所有的jvm实现都采用主动中断的思想保证所有线程都在最近的安全点再更新OopMap。

  • 安全区域:其是安全点的延伸,加入线程一直睡眠,就无法轮询主动中断标志,因此定义安全区域,只要线程进入了安全区域就算进入了安全点,它的引用关系就不会发生变化。当线程离开安全区时要询问jvm是否完成了根节点枚举。

  • 考虑之前的跨代引用的情况,多数情况下是老年代的对象引用了新生代的对象,这里就设置了一个记忆集的概念,卡表是记忆集的一种实现,将老年代空间分割成同等大小的块,每个卡表指向一个块,只要块脏了(即存在跨代指针)就把块中所有的对象加入根节点集合中。

  • 卡表的维护需要写屏障,其实就是在引用类型字段赋值的一个around 类型的AOP切面,在执行前后更新卡表。关于卡表的伪共享问题参阅博客:https://www.jianshu.com/p/defb9f9af5d3

  • 并发的可达性分析:找到GC Roots的对象之后,就需要按照引用关系进行深度搜索了,搜索过程时长与用过的java堆的容量呈正比关系,可以使用搜索线程与用户线程并发的方式缩短用户的等待时间。用户线程会在搜索过程中修改对象,会造成两种后果:

    (1)把原本消亡的对象标记为存活(搜索线程判断为某个对象为存活后,用户线程将指向这个对象的引用去掉),这不是好事,但可以容忍。

    (2)把本应存活的对象标记为消亡。这是不能容忍的,会导致程序出错。

    对此,Wilson在理论上证明“对象消失”问题必须同时满足:

    (1)赋值器插入了一条或多条从黑色对象到白色对象的新引用。

    (2)赋值器删除了全部从灰色对象到该白色对象的直接或间接应用。

    对此,有两种解决对象消失的方法:

    (1)增量更新:黑色对象一旦新插入了指向白色对象的引用后,它就会被设为根并再搜索一次。代表GC:CMS

    (2)原始快照:无论引用关系删除与否,都会按照原始对象引用拓扑结构进行扫描。代表GC:G1,Shenandoah。

    这里有一个问题:如果GC在搜索标记过程中用户线程又分配内存新建并引用了一个新对象,这中情况就只满足了条件(1),并没有满足条件(2),但是也会造成对象消失,此问题暂时没有答案。

经典垃圾收集器

  • 各种GC实现的概览:

在这里插入图片描述
图片来源:https://blogs.oracle.com/jonthecollector/our-collectors

黄色部分是新生代GC,灰色是老年代GC,问号处应该是G1.连线表示可以互相配合。

  • Serial收集器

    最基础、最悠久的GC,完全单线程,停止时间长,但额外的内存资源消耗很少,用武之地是应用与内存资源受限的情形,如微服务。老年代版本是Serial Old

  • ParNew收集器

    Serial的多线程版本,唯一能与CMS收集器配合的新生代收集器。现已退出。

  • Parallel Scavenge收集器

    与上述GC目标不同,要尽量保证一个好的吞吐量(运行用户代码时间 divide 包括GC在内的整个时间),老年代版本是Parallel Old。

  • CMS收集器

    基于标记-清除算法。分为4个步骤:

    (1)初始标记:需要stop the world,但停顿时间很短,只是标记一下与GC Roots直接关联的对象。

    (2)并发标记:并发的遍历所有GC Roots引用链上的对象。

    (3)重新标记:基于并发可达性分析中的增量更新算法对并发标记的结果进行修正。

    (4)并发清除:并发的删除掉不可达的对象。

    HotSpot追求低停顿的第一次尝试,缺点如下:

    a. 由于并发进行,故对处理器资源非常敏感。

    b. 由于在阶段(2)(4)并发的进行,用户线程会产生新的无用对象,形成浮动垃圾,当最终空间不足时,只能对所有老年代进行stop the world的GC。

    c. 由于标记-清除算法,会形成大量碎片。

  • G1收集器

    G1是里程碑式的成果。G1将堆内存划分为很多的块,称为Region,G1也是分代的,但每个Region都有可能是新生代的Eden或Survior空间或老年代,每次回收会挑选回收价值最高的一批Region回收。G1分为4个步骤:

    (1)初始标记,需要用户线程停顿,与CMS的初始标记一致。

    (2)并发标记,并发的从GC Roots开始对堆中对象进行可达性分析,完成后处理原始快照记录的对象变动情况。

    (3)最终标记,需要用户线程停顿,处理(2)中仍然遗留的原始快照标记。

    (4)筛选回收,根据Region回收成本、回收价值,用户期望停顿时间确定回收集,复制回收集中的存活对象到空Region中并回收集合中的所有Region,需要用户线程停顿。

    G1缺点:需要额外占用较高内存,机器负载也高。综合G1更适合大内存场景。

低延迟垃圾处理器

  • 衡量GC的3个重要标准:内存占用、吞吐量、延迟。这3者是不可能完美的三角。

    Shenandoah与ZGC隆重出场,他们停顿时间极短且与堆容量、对象数量无关。

  • Shenandoah(简称“S”)

    它只存在于OpenJDK中,不是甲骨文官方开发。S相比于G1,不使用分代概念,不用卡表,而是用连接矩阵表示Region间的引用关系,它的工作流程如下:

    (1)初始标记:与G1一样。

    (2)并发标记:与G1一样。

    (2)最终标记:与G1一样,同事统计回收价值最高的Region组成回收集。需要停顿。

    (4)并发清理:清理连一个存活都没有的Region。

    (5)并发回收:将回收集中存活的对象复制到空Region中,并与用户并发,使用Brooks 指针来解决用户线程的对象访问问题。

    (6)初始引用更新:将被引用的旧地址变成新地址的初始阶段(其实没有任何处理,只是确保起点一致),产生一个极短的停顿。

    (7)并发引用更新:并发进行堆中的引用的更新。

    (8)最终引用更新:独占进行GC Roots中的引用更新,要停顿。

    (9)并发清理:并发清理回收集中的Region。

    图解:

    蓝色代表空闲、绿色:存活、黄色:选定的被回收对象。

    在这里插入图片描述

    (from:https://shipilev.net/talks/devoxx-Nov2017-shenandoah.pdf)

    Brooks 指针就是对象最前边再有一个指向最新对象的指针,平时是指向自己,对象复制后指向新地址。在并发引用更新中如果用户修改了原对象的内容,是灾难性的,所以这里使用了比较并交换操作保证只有一个线程可以访问Brooks指针。

  • ZGC

    JDK 11开始出现,ZGC中的Region不是固定大小,动态创建与销毁,具有小中大不同容量。大型Region容量不固定,用来存储大对象,不会被充分配。

    ZGC的核心问题:并发整理的问题。ZGC给出的方案是使用染色指针,所谓染色指针就是指,在对象被指向的指针本身上做标记,而不是在对象处做标记,这基于:某个对象只有它的引用关系能决定它存活与否,与对象上的属性无关

    染色指针:64位系统上的内存可寻址空降有64EB,但是并不是所有的位数都用来寻址,64位Linux系统高18位不用来寻址,剩余46位寻址空间64TB,ZGC使用了46位中的高4位搭载此指针指向对象的存活信息,因此ZGC最高支持4TB寻址。染色指针带来的好处:

    (1)一旦某个Region的存活对象被移走后,这个Region就能被立即释放。

    (2)大幅减少GC过程中的内存屏障数量。

    (3)可作为一种存储结构用来记录更多对象标记、重定位相关信息。

    然而,染色指针需要操作系统底层支持,x86_64不支持,ZGC使用多重映射技术将多个不同虚拟内存地址映射到同一个物理地址解决。

    ZGC的4个阶段:(全部可以并发)

    (1)并发标记:与G1一样,开头需要短停顿,不过标记是在染色指针上。

    (2)并发预备重分配:统计本次要清理哪些Region,组成重分配集,每次GC都会扫描所有Region。重分配集的存活对象会被复制。

    (3)并发重分配:将重分配集上的存活对象复制到其他Region中,同时建立转发表,记录旧对象到新对象的转发关系,再有对旧对象的访问,就被内存屏障截获并转发给新对象,称为“自愈”。之后老Region就可以释放,但转发关系表要保留。

    (4)并发重映射:由于有自愈存在,指针指向地址的修改不那么急迫,这一阶段就合并到下一轮的并发标记阶段,结束后转发关系表就可以删除了。

    ZGC缺点:不能应对GC过程中新产生的浮动垃圾,除非引入分代概念。

    官方测试结果:“令人震惊的,革命性的ZGC”。

  • Epsilon收集器:不做GC行为,适用于短命程序。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值