文章目录

  • 一、什么是OopMap
  • 二、安全点(safe point)
  • 三、安全区(safe region)
  • 四、卡表(card table)
  • 五、写屏障(write barier)
  • 总结

一、什么是OopMap

由于目前几乎所有虚拟机都是用可达性分析算法来判定对象是否存活,即通过选定固定的gc roots作为起始节点,像剥洋葱一样往下溜达,只要存在任意节点从gc roots到该节点不可达,那表示这个对象不被任何对象所引用,这个对象最终就要被当做垃圾回收掉。


问题来了,如何找到这些gc roots呢?
从源代码上看,对象引用不是在类中,就是在方法中,如此,通过扫描所有的对象就可以获取到这些gc roots。但是目前随便一个Java应用相当庞大(低情商叫臃肿),内存中的类,对象,常量数不胜数,每次gc都去扫描一遍,这个性能损耗是不可能接受的,而这只是其一。其二为了保证内存的一致性,获取这些gc roots过程中,必须暂停用户线程。用户线程在这个阶段内不能工作,所以能做的就是尽量要缩短用户线程的停顿时间,也就是要尽快完成gc roots的扫描。惯用套路,既然后期处理遍历耗时,那就前期维护一套数据结构呗,所谓的空间换时间。而OopMap就是这套数据结构,通过OopMap提前记录类、方法的引用信息,查找gc roots时,直接通过OopMap去获取,而不必扫描整个对象。

  1. 首先对于一个类在加载进内存的时候,空间是“确定的”,即结构是确定的,比如定义了哪些变量,哪些引用,而且一定是连续内存,所以对象中的引用是可以通过地址偏移量计算得到的,所以把这个偏移量放在OopMap中,需要的时候OopMap去找就可以了
  2. 一个线程在运行过程中,有自己的栈空间,每一个方法都是一个栈帧,即时编译过程中会在特定位置记录下栈中和寄存器里哪些位置是引用。
public void test()
{Person person = new Person();person.setPhone(new Phone());Dog dog = new Dog();//...
}

一个栈帧可以有多个OopMap,这里假设一个栈帧只有一个,且记录状态是在方法返回之前,如下:

二、安全点(safe point)

虽然OopMap避免了大量扫描内存的消耗,但是内存中对象繁多,对象之间的引用关系也时刻在发生变化,如果每条指令都去记录OopMap,将会消耗大量内存和cpu资源,垃圾回收反而变成了系统的负担,为了解决这个问题,引入了安全点(safe point)的概念。即只在指令流的特定位置记录OopMap,垃圾回收行为发生后,线程如果没有到达安全点,将继续执行,直到到达最近的一个安全点才停下来,等待垃圾回收器完成gc roots的选取。

就像公交车一样,每个乘客到达的地点是不同的,但公交车不会为每一个人去停车,必须等到提前设定的站台才会停下,这个时候乘客才可以下车。

当线程到达安全点后,有两种方式中断线程:

  1. 抢占式中断,在发生垃圾回收时,系统将所有线程中断,如果发现有线程还没有到达安全点,则恢复该线程的执行,等待一会儿继续中断,直到到达安全点上,目前几乎没有虚拟机采用这种方案去停止用户线程
  2. 主动式中断,在特定位置设置一个中断标志位,所有线程在执行过程中不断去轮询这个标志位,如果发现该标志位被置位,就在距离自己最近的安全点主动挂起自己,等待垃圾回收器工作

轮询标志的地方和安全点是重合的,还需要加上所有创建对象和其他需要在对上分配空间的地方,这是为了检查是否要发生gc,避免没有足够的内存分配对象

适合插入安全点的地方:

  • 方法(栈帧)结束前,但并不意味着一个方法只能有一个安全点
  • 非计数循环末尾,避免循环体执行时间太长,导致长时间无法到达安全点
  • 每条Java编译后的字节码边界

根据以上的概括,我们能总结出一个点,安全点既不能太多,也不能太少,如果安全点过多,会对虚拟机资源产生更多的挤压,如果安全点太少,则会导致垃圾回收器等待时间过长,因此,需要在这两者之间取其平衡。

三、安全区(safe region)

安全区域可以理解是对安全点的存在问题的补充,上边说到线程会执行到附近的安全点停下来等待垃圾回收器介入处理,但如果线程没有执行呢,换句话就是说没有获得cpu执行权,比如某一个线程正在sleep或者等待磁盘输入,那么这个线程是不会走到安全点挂起自己的。
这个时候就要引入安全区概念了,顾名思义,安全区就是一段指令域,在这个域中的指令不会对当前内存中的引用造成修改,当线程进入该区域后,会主动将自己的状态标记为“进入安全区”,这个时候如果发生gc,垃圾回收器发现该线程处于安全区域内,认为该线程不会对内存安全造成影响,便会跳过该线程,不会等待该线程到达安全点。
而线程在到达安全区边界时,同样也会检查当前gc是否在工作,如果gc正在工作,这个时候线程便会主动停下来,等待gc动作完成后再继续执行。

四、卡表(card table)

为了解决在垃圾回收算法中,无论哪种算法,都需要先对对象进行标记,然后再进行回收操作。标记过程中,存在跨代引用问题,为了完整的标记对象引用链,将不得不对跨代内存中的对象进行遍历,尤其是老年代对象,对象存活率相对高,遍历的性价比极低。于是就引入了记忆集的概念,即将老年代内存划分为若干个小块,同时在新生代特定位置维护一块数据区域用来标记老年代中的哪个小块内存中存在跨代引用,当发生gc时,只需要检查这块数据区域中哪些老年代内存块中存在跨代引用,然后再对这一小块内存进行遍历。
这个过程相当于将老年代整个内存的搜索粒度降低了,从搜索整个老年代到“只搜索其中的某几小块”。
此外,老年代中内存小块具体设置多大,也需要综合考虑,如果设置太小,虽然粒度更低,精度更高,遍历单个块效率会提高,但分块会变多,同时也需要更多新生代空间去维护这个分块信息;如果划分的太大,则会降低老年代搜索效率。
以下是记忆集可供选择的记录精度:

  • 字长精度:每个记录精确到机器字长,64位机下就是8字节空间,该区域存在跨代指针
  • 对象精度:每个记录精确到一个对象,该区域存在跨代指针
  • 卡精度:每个记录精确到一块内存区域,该区域存在跨代指针

而card table可以理解为是Java虚拟机对记忆集的一种实现,hotspot用一个字节数组完成了卡表的记录,字节数组的每一个元素都标识老年代内存区域中的一段特定长度的区域,这个区域称为卡页。

CARD_TABLE[(this address >> 9)] = 0;
//根据以上的代码可以看出一个卡页的大小是512byte,即2^9 。
//将内存块地址右移9位,即除以512,得到的余数正好对应该内存区域对应的卡表标号


只要卡页中存在跨引用对象,则将对应的卡表标识位置位为1,称之为变脏,发生gc时,只需要检查卡表变脏的元素,就能很快定位到哪一块内存中存在跨代指针。

五、写屏障(write barier)

通过记忆集减少了跨代指针扫描的问题,那卡表示如何维护的呢?答案就是写屏障(write barier),
首先卡表变脏肯定发生在引用赋值那一刻,写屏障可以理解成在指定的操作前后设置一组“保护措施”,可以理解成Spring框架中的AOP,在引用赋值时会产生一个环形通知,供程序执行额外的动作。在赋值前的部分叫做写前屏障,在赋值后的部分叫做写后屏障。除了G1收集器,其他的收集器都只用到了写后屏障。
应用写屏障后,虚拟机就会为所有的赋值操作生成相应的指令,这意味着所有的引用赋值都会增加更新卡表的操作,对产生一定的开销浪费,但是相比扫描整个老年代代价要小得多。

一个伪共享问题:
众所周知,cpu内部存在高速缓存,缓存中的存储单位是以缓存行为单位的,也就是说一个缓存行可以存储多个数据单位,而这些数据可能来自于不同线程,都知道不同线程的变量是相互隔离的,并不会互相影响。但是在并发场景下,缓存中的数据行尽管是来自不同线程的数据,但是由于存储单位是一个缓存行,此时线程不得不进行同步、或者通过cas算法以达到“有序执行”,从而降低了性能。本来数据实际上是相互独立、相互隔离的,但是在这里线程却认为访问的是共享空间,故名“伪共享”。

同样应用写屏障面临这个问题,如果卡表元素被缓存在一个缓存行中,多个线程对该卡表的更新操作就会出现“伪共享”,为了避免该问题,当内存对象引用发生变化需要更新卡表时,先判断一下该对象所对应的卡表是否已经变脏,如果没有变脏,再进行更新,避免多次重复标记导致的线程同步。
JDK7之后增加了新参数 -XX:+UseCondCardMark用来决定是否开启卡表更新的条件判断

总结

看过的东西总是容易忘记… ,就重新对垃圾回收算法其中的一些理解性概念进行了简单的记录和总结,同时也为后续的文章笔记做些一些铺垫,能力有限,如有问题,还望看官们指正~

JVM之OopMap,安全点,安全区相关推荐

  1. 读书笔记——深入理解JVM(JVM自动内存管理)

    简介 本系列为<深入理解Java虚拟机-JVM高级特性与最佳实践>一书的阅读笔记. 本书开头介绍了JVM发展的历史,接着介绍了JVM是如何实现自动内存管理的. 本章节主要介绍: JVM的存 ...

  2. 【对线面试】JVM全网最强面试对答

    文章目录 面试官: 今天我们来聊聊JVM吧!!! 面试官:那你先说一下 JVM 的内存区域有哪些? 面试官:很好,你刚刚提到永久代,那你知道永久代和元空间的区别吗 面试官:很不错,那我再问问,那你知道 ...

  3. JVM内存管理机制线上问题排查

    本文主要基于"深入java虚拟机"这本书总结JVM的内存管理机制,并总结了常见的线上问题分析思路.文章最后面是我对线上故障思考的ppt总结. Java内存区域 虚拟机运行时数据区如 ...

  4. 《深入理解JVM.2nd》笔记(三):垃圾收集器与垃圾回收策略

    文章目录 概述 对象已死吗 引用计数算法 可达性分析算法 再谈引用 finalize():生存还是死亡 回收方法区 垃圾收集算法 标记-清除算法 复制算法 标记-整理算法 分代收集算法 HotSpot ...

  5. Java成神之路——JVM垃圾回收概览

    如何确认对象可以被回收 枚举根节点,来确认, 搜索对象的引用链. 当一个对象的引用不能到达根节点,那么就认为这个对象是垃圾. 根节点可以为: 虚拟机栈中引用的对象,方法区中类静态属性引用的遍历,方法区 ...

  6. 深入理解JVM(第二版读书笔记)

    一  开始前 HotSpot:http://xiaomogui.iteye.com/blog/857821 http://blog.csdn.net/u011521890/article/detail ...

  7. JVM 重点知识点总结

    本篇是学习 JVM 的一些知识点总结,翻阅了大量参考资料,不求多深入到细节,只求把一些重点的内容整理出来,所有参考资料均在文章末尾列出,这些参考资料才是最大的宝藏,文章所引用图片均来自于互联网. 文章 ...

  8. JVM笔记(三):垃圾回收篇

    垃圾回收篇 文章目录 垃圾回收篇 一.垃圾回收算法与引用 1.判断对象已死(标记垃圾算法) `1` 引用计数算法 `2` 可达性分析算法 `3` 真正的死亡 2.垃圾回收算法 `1` 垃圾回收分类 ` ...

  9. 【JVM】浅记JVM

    前言 一.参看视频.网站.书籍 差不多是花了一个月看完黑马的视频,然后查看其它的相关的面试题.差不多花费的时间就是一个半月吧. ① 黑马程序员JVM完整教程,Java虚拟机快速入门, 附上黑马视频自带 ...

最新文章

  1. 清华大学继续教育学院sql注入漏洞
  2. mysql show full processlist;_mysql show full processlist 详解
  3. 雷军反省今天的小米:无法躺赢或猛冲猛打,只能豁出去干
  4. strtok函数取WinMain的参数
  5. yslow客户端性能测试
  6. How to resolve ASSERTION_FAILED error when you register the odata service expose
  7. java 入侵 mysql_Java访问数据库Mysql
  8. egg --- 配置连接mysql 创建模型 插入数据
  9. Java ObjectOutputStream writeFields()方法与示例
  10. C++虚继承的实现原理、内存分布、作用
  11. mybatis mysql merge_Spring Boot + Mybatis 整合Mysql ,SQLServer数据源以及整合druid,动态调整数据源切换。...
  12. LOCK TABLES
  13. linux下写脚本时-gt是什么意思
  14. 解读SOA平台---概念分析
  15. Linux基础命令(管理工具)
  16. 美团和大众点评合并:抱团取暖 1+1能否大于2?---OFweek
  17. SAIL-IMX7D开发板截屏工具gsnap移植
  18. 求助!spyder beautifulsoup4显示错误:AttributeError: 'HTMLParserTreeBuilder' object has no attribute 'initia
  19. 小程序 横向滚动导航栏
  20. Unity3d操作的一些技巧知识点和BUG解决方案

热门文章

  1. 宝塔运行在nginx二级目录代理配置
  2. linux星际译王安装,Linux下星际译王 stardict 的安装
  3. PM进阶之路 | 用户端产品设计
  4. Linux 修改hosts文件并刷新DNS生效hosts文件
  5. GPRS模块的使用Java串口操作(1)
  6. 常见的nlp 自然语言处理模型
  7. 2023武汉纺织大学计算机考研信息汇总
  8. 电大本科计算机考试,电大本科计算机考试
  9. 国内半导体封装测试企业
  10. excel转换txt能否将数据对齐