0
点赞
收藏
分享

微信扫一扫

JVM 相关知识点记录

sin信仰 03-21 20:00 阅读 2

文章目录


前言

JVM包含内容:

  • 类装载子系统(Class Load SubSystem)
  • 运行时数据区(Run-Time Data Areas)
      • 局部变量表
      • 操作数栈
      • 动态链接
      • 方法返回地址
    • 程序计数器
    • 方法区
  • 本地方法接口(Native Method Stack)
  • PC寄存器(Programe Counter Register)
  • 执行引擎(Execution Engine)
    • 字节码解释器
      对字节码采用逐行解释的方式执行
    • JIT(Just In Time)编译器

静态变量和常量池的变化:

在这里插入图片描述

静态成员的存储位置变化:
在JDK1.8以前,静态成员存储在方法区(永久代)中,此时方法区的实现叫做永久代,而永久代在堆中。
在JDK1.8以后,永久代被移除,此时方法区的实现更改为元空间,但由于元空间主要用于存储字节码文件且用的是堆外内存,因此静态成员的存储位置从方法区更改到了堆内存中。

常量池的存储位置变化:
在JDK1.6及以前,常量池存储在方法区(永久代)中。
在JDK1.7中,方法区(永久代)被整合到堆内存中,常量池存储在堆内存中。
在JDK1.8后,方法区(元空间)从堆内存中独立出来,常量池依然存储在堆内存中。

JDK1.8:在这里插入图片描述
Java变量分为类的成员变量、静态成员变量和方法中的局部变量。

先说局部变量,基本类型的局部变量变量名和值都存放在虚拟机栈中,引用类型的局部变量变量名存放在栈中,而变量指向的对象存放在堆中。
再说类的成员变量,不论基本类型还是引用类型,变量名和值都随着类的实例(对象)存放在堆中。
最后说说静态变量,它比较特殊,是属于类的变量,在jdk7及之前的版本,随类存放在方法区中。在jdk8之后,由于虚拟机内存结构的变化,静态变量和常量池一起被迁移到了堆中。

总结如下:

  1. Java中对象的存储位置

String aa = new String();

new创建的对象存储在堆内存中;

aa这个局部变量存储在栈内存中;

  1. Java中常量的存储位置

常量存放在常量池中,而常量池在堆内存中

  1. Java中局部变量的存储位置

局部变量存放在栈内存中

  1. Java中全局变量的存储位置

存放在全局数据区内存中-堆中

  1. Java中Static常量的存储位置

存放在全局数据区内存中-堆中

  1. java中static修饰的成员变量及参数存放位置

1.7存放在方法区 1.8存放在堆中

JVM中的常量池变化

JIT(Just In Time)编译器

  • 方法调用计数器:统计方法调用次数
    统计方法调用的次数。默认阈值时Client模式下1500次,在Server模式下是10000次。超过这个阈值就会触发JIT编译。这个阈值可以通过-XX:CompileThreshold设定
  • 回边计数器:统计循环体执行的循环次数

jvm内存分配

  • 栈上分配与TLAB/内存分配的两种方法

jdk1.8默认垃圾回收器
JDK1.8中,Parallel Scavenge 被设置为年轻代(Young Generation)的默认垃圾回收器,而 Parallel Old 是用于老年代(Tenured Generation)的垃圾回收器

Class文件数据

Class文件内容

分配对象方式
Java中对象地址操作主要使用了Unsafe调用了C的allocate和free两个方法,分配方法有两种:

  • 空闲链表(free list):通过额外的存储记录空闲的地址,将随机IO变成顺序IO,但带来了额外的空间消耗。
  • 碰撞指针(bump pointer):通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。

Class字节码

在这里插入图片描述

上图可以看出,Class文件中包括:

  • 魔数:它的唯一作用是确定这个文件是否可以被JVM接受。很多文件储存标准中都使用魔数来进行身份识别的,其占用这个文件的前四个字节。

  • 版本号:第5和第6个字节是副版本号,第7个和第8 个是主版本号。

  • 常量池计数器:也就是常量池的入口,代表常量池的容量计数器。

  • 常量池:常量池中主要存放两类常量:字面量和符号引用。字面量比较接近Java语言层面的常量概念。就是我们提到的常量。而符号引用则属于编译原理的方面的概念。包括以下三类常量:

    • 类和接口的全限定名

    • 字段的名称和描述符

    • 方法的名称和描述符

哪些内存需要回收

所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。
寻找回收对象的两种方式。

  • 引用计数法
    给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。
  • 可达性分析法
    通过一系列称为GC Roots的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

可以作为GCRoots的对象包括下面几种:

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

OopMap

当所有线程停下来的时候,并不需要一个不漏的检查完所有执行上下文和全局引用位置,虚拟机应该是有办法直接知道哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到目的的。

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

知乎博客

JVM之OopMap、卡表,安全点,安全区

方法区的垃圾回收

方法区的垃圾回收主要回收两部分内容:

  1. 废弃常量。
    以字面量回收为例,如果一个字符串“abc”已经进入常量池,但是当前系统没有任何一个String对象引用了叫做“abc”的字面量,那么,如果发生垃圾回收并且有必要时,“abc”就会被系统移出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
  2. 无用的类。既然进行垃圾回收,就需要判断哪些是废弃常量,哪些是无用的类,需要满足以下三个条件:
    • 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

  • 标记-清除(Mark-Sweep)算法
  • 复制(Copying)算法
  • 标记-整理(Mark-Compact)算法
  • 分代收集算法

垃圾收集器

  • Serial收集器
    需要STW(Stop The World),停顿时间长。单线程收集器,新时代,采用复制算法。
    简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。
  • Serial Old收集器
    Serial收集器的老年代版本,单线程收集器,老年代,采用标记整理算法
  • ParNew收集器
    ParNew收集器其实就是Serial收集器的多线程版本,多线程收集器,新生代、采用复制算法
  • Parallel Scavenge收集器
    多线程收集器,老年代,采用标记整理算法
  • Parallel Old收集器
  • CMS收集器
    多线程收集器,老年代,采用标记—清除算法。收集包括四个步骤:初始标记(触发STW)、并发标记(不触发STW)、重新标记(触发STW)、并发清理(不触发STW)
  • G1收集器
    G1 垃圾收集器将堆内存划分为若干个 Region,每个 Region 分区只能是一种角色,Eden区、S区、老年代区的其中一个,空白区域代表的是未分配的内存,最后还有个特殊的区域H(Humongous),专门用于存放巨型对象,如果一个对象的大小超过Region容量的50%以上,G1 就认为这是个巨型对象。

什么是STW?
STW是Stop-The-World缩写: 是在垃圾回收算法执⾏过程当中,将JVM内存冻结丶应用程序停顿的⼀种状态。

1、在STW 状态下,JAVA的所有线程都是停⽌执⾏的 -> GC线程除外
2、一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。
3、STW是不可避免的,垃圾回收算法执⾏一定会出现STW,我们要做的只是减少停顿的时间
GC各种算法优化的重点,就是减少STW(暂停),同时这也是JVM调优的重点。

什么时候进入STW状态?
可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿,进入STW状态

为什么一定要STW停顿的原因?
1、分析工作必须在一个能确保一致性的快照中进行
2、一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
3、如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
4、被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉卡顿
5、所以我们要减少STW的发生,也就相当于要想办法降低GC垃圾回收的频率
6、STW状态和采用哪款GC收集器无关,所有的GC收集器都有这个状态,因为要保证一致性。
7、但是好的GC收集器可以减少停顿的时间、减少STW(暂停)和降低GC垃圾回收的频率是调优的重点

CMS收集器

收集过程有四个阶段:1、初始标记 2、并发标记 3、重新标记 4、并发清除
在这里插入图片描述
四个阶段中初始标记和重新标记仍需要暂停所有的用户线程(Stop The World),但为什么说这个收集器也暂停了所有的线程,为什么还能做到停顿时间短呢。因为初始标记阶段只是标记GC Roots能直接关联的对象,这个过程很快。而并发标记时才进行沿GC Roots遍历所有对象,这个工作量说不小的,但这个过程并没有停顿用户线程,而是与其并发执行,如果再过程出现对象引用关系改变,则使用增量更新的方法将其标记。待重新标记阶段就是为了解决这个并发过程中因为改变而被标记的对象。这个阶段是要暂停用户线程的,但这部分的工作量也不大。最后全部标记玩就进入了并发清除的阶段了。这部分也是与用户线程并发进行的。

从整体上看来耗时长的并发标记和并发清除都没有暂停用户线程,所有可以说:从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

G1收集器

G1是一种“停顿时间模型”的收集器,它能指定时间N,确保消耗再垃圾收集上时间大概率不超过N毫秒的目的。G1收集器一改之前的分区收集思想,开创了面对局部收集的设计思路。它将java堆划分为多个大小相等的独立区域Region。它可以面对堆内存任何部分组成回收集进行回收。这个模型回收哪块的衡量标准是哪块Region垃圾最多,再N毫秒内回收收益最大。这就是G1收集器的Mixed GC模式。

收集过程:

  • 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,需要暂停用户线程,但耗时很短。
  • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。 ·
  • 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。 ·
  • 筛选回收:负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。

在这里插入图片描述

垃圾收集器及参数讲解

垃圾收集器讲解

垃圾收集器讲解2

三色标记算法

三色标记算法是一种垃圾回收的标记算法。它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。JVM中的CMS、G1垃圾回收器 所使用垃圾回收算法即为三色标记法。

黑色:代表该对象以及该对象下的属性全部被标记过了。(程序需要用到的对象,不应该被回收)
灰色:对象被标记了,但是该对象下的属性未被完全标记。(需要在该对象中寻找垃圾)
白色:对象未被标记(需要被清除的垃圾)

漏标产生的两个必要条件:

  • 至少有一个黑色对象在自己被标记后指向了白色对象
  • 删除了灰色对象到白色对象的直接或间接引用。
  • 为了避免漏标,只要打破这两个必要条件之一即可。

CMS采用增量更新的方式打破第一个条件(黑色指向白色对象):当A引用指向其他对象时,将A重新标记为灰色,下次扫描时,重新扫描A的成员遍历。

在这里插入图片描述

G1采用STAB(snapshot at the begining原始快照) 打破第二个条件(灰色指向白色对象的引用消失):当B-> D的引用消失时,将D推送到GC堆栈,保证还能被GC扫描到。

在这里插入图片描述
STAB快照使用写前屏障将即将被修改的白色D对象信息保存到stab_mark_queue队列中,下次并发处理的时候会重新处理队列中对象信息。因为G1自身有一个RSet表存储了其他Region到改Region的引用信息,不需要扫描整个堆内存,可以快速通过RSet表找到存活的引用。

标记流程图示

图解

三色标记法与读写屏障

Remembered Set(记忆集)

现代JVM,堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,就需要单独跟踪从老年代到新生代的所有引用,从而避免每次YGC时扫描整个老年代,减少开销。

GC Roots是垃圾收集器寻找可达对象的起点,通过这些起始引用,可以快速的遍历出存活对象。GC Roots最常见的是静态引用和堆栈的局部引用变量。
为解决扫描GC ROOT时遇到对象跨代引用所带来的问题,收集器在新生代上建立一个全局的称为记忆集(Remembered Set)的数据结构。这个结构把老年代划分为若干个小块,标识出老年代哪一块内存会存在跨代引用。当发生 Minor GC 时,只有包含了跨代引用的小块内存中的老年代对象才会加入到 GC Roots 扫描中,避免整个老年代加入到 GC Roots 中。

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本。

下面列举了一些可供选择的记录精度,由高到低依次为:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。

卡表(Card Table)

一种称为"卡表"(caed table)的方式实现记忆集,是目前最常用的一种实现方式,卡表与记忆集(Remembered Set)的关系,类似于Java语言中HashMap与Map的关系。

基于卡表(Card Table)的设计,通常将堆空间划分为一系列2次幂大小的卡页(Card Page),用于标记卡页的状态,每个卡表项对应一个卡页。

HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每个标记项为1个字节。当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。

当进行Minor GC时,我们出于清理新生代区对象的目的,在进行可达性分析过程中,需要判断该对象是否被老年代对象所引用。我们都知道老年代区域比新生代大,如果扫描整个老年代,这是件很消耗性能的事情,这就是对象跨代引用所带来的问题。

为了解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。事实上并不是只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集行为的垃圾收集器,典型如G1,ZGC收集器,都会面临相同的问题,因此我们有必要进一步理清记忆集的原理和实现方式。

Remembered Set解析

卡表与写屏障

Card Table简单描述

图解

年轻代进入老年代条件

  • 躲过15次gc,达到15岁高龄之后进入老年代;
  • 动态年龄判定规则,如果Survivor区域内年龄1+年龄2+年龄3+年龄n的对象总和大于Survivor区的50%,此时年龄n以上的对象会进入老年代,不一定要达到15岁
  • 如果一次Young GC后存活对象太多无法放入Survivor区,此时直接计入老年代
  • 大对象直接进入老年代

内存担保机制

  1. 什么是老年代空间担保机制?担保的过程是什么?
    JVM有这么一个参数:-XX:-HandlePromotionFailure(1.8默认设置),年轻代每次GC前都,JVM都会计算老年代剩余可用空间,如果这个剩余空间小于年轻代里所有对象大小之和(包括垃圾对象),那么JVM就会看是否设置前面这个参数。如果设置这个参数,且老年代剩余空间是否小于之前每一次MInorGC后进入老年代对象的平均大小。
      如果没设置参数,或者小于平均大小,会先触发一次FullGC,将老年代和年轻代的垃圾对象一起回收掉,如果回收后还是没有空间存放对象,则会发生OOM。

在这里插入图片描述

  1. 老年代空间担保机制是谁给谁担保?
    我理解的是老年代给新生代的S区做担保。
  2. 为什么要有老年代空间担保机制?或者说空间担保机制的目的是什么?
    目的:避免频繁的进行FullGC。
  3. 如果没有老年代空间担保机制会有什么不好?
    如果没有这个担保机制,就会直接执行Full GC,这样对性能的影响频次会增加。

FullGC 触发时机

Full GC(Full Garbage Collection)是指对整个Java堆进行垃圾回收,包括新生代和老年代。触发Full GC的情况有以下几种:

  • 老年代空间不足:当老年代中没有足够的空间来分配一个大对象时,会先尝试进行Minor GC,如果仍然无法获得足够的空间,则会触发Full GC。

  • 调用System.gc()方法:虽然使用System.gc()方法不能保证立即进行垃圾回收,但是这个方法可以提示JVM进行垃圾回收。如果此时需要更多的内存空间,那么就可能会触发Full GC。

  • Perm区空间不足:Perm区是存放类信息和常量池等元数据的区域,如果Perm区没有足够的空间来存放这些信息,就会触发Full GC。

  • CMS GC出现Concurrent Mode Failure:CMS(Concurrent Mark Sweep)是一种以最小化停顿时间为目标的垃圾收集器,在CMS执行过程中,如果应用程序产生了大量更新,导致CMS回收速度跟不上对象生成速度,那么就可能会出现Concurrent Mode Failure,此时会启动Full GC来清理整个堆空间。

  • 分配担保失败:在Minor GC后,如果survivor区无法容纳所有幸存对象,那么就要将部分幸存对象转移到老年代。如果老年代剩余空间不足以容纳这些对象,就需要进行Full GC。

需要注意的是,Full GC通常比Minor GC和CMS GC的停顿时间长,同时对于大型应用程序,Full GC可能会影响性能,因此应该尽量避免Full GC的发生。

GC日志解析

GC日志内容

日志内容解析及GC案例

不同垃圾收集器的不同日志打印示例
G1垃圾收集器日志解析

日志参数

  • -XX:+PrintGC: 输出GC日志。类似:java -verbose:gc
  • -XX:+PrintGCDetails : 输出GC的详细日志
  • -XX:+PrintGCTimestamps : 输出GC的时间戳(以基准时间的形式)
  • -XX:+PrintGCDatestamps : 输出GcC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC: 在进行GC的前后打印出堆的信息
  • -Xloggc:./logs/gc.log: 日志文件的输出路径

-XX:+PrintGC :

这个只会显示总的GC堆的变化,如下:

[GC (Allocation Failure) 80832K->19298K(227840K),0.0084018 secs]
[GC (Metadata GC Threshold) 109499K->21465K(228352K),0.0184066 secs]
[Full GC (Metadata GC Threshold) 21465K->16716K(201728K),0.0619261 secs]

参数解析:

GCFull GCGC的类型,GC只在新生代上进行,Full GC包括永生代,新生代,老年代。
Allocation FailureGC发生的原因。
80832K->19298K:堆在GC前的大小和GC后的大小。
228840k:现在的堆大小。
0.0084018 secs:GC持续的时间。

-XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen:70640K->10116K(141312K)] 80541K->20017K(227328K),0.0172573 secs] [Times:user=0.03 sys=0.00,real=0.02 secs]
[GC (Metadata GC Threshold) [PSYoungGen:98859K->8154K(142336K)] 108760K->21261K(228352K),0.0151573 secs] [Times:user=0.00 sys=0.01,real=0.02 secs]
[Full GC (Metadata GC Threshold)[PSYoungGen:8154K->0K(142336K)]
[ParOldGen:13107K->16809K(62464K)] 21261K->16809K(204800K),[Metaspace:20599K->20599K(1067008K)],0.0639732 secs]
[Times:user=0.14 sys=0.00,real=0.06 secs]

参数解析:

GCFull FC:同样是GC的类型
Allocation FailureGC原因
PSYoungGen:使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的变化
ParOldGen:使用了Parallel Old并行垃圾收集器的老年代GC前后大小的变化
Metaspace: 元数据区GC前后大小的变化,JDK1.8中引入了元数据区以替代永久代
xxx secs:指GC花费的时间
Times:
	user:指的是垃圾收集器花费的所有CPU时间
	sys:花费在等待系统调用或系统事件的时间
	real:GC从开始到结束的时间,包括其他进程占用时间片的实际时间。

-XX:+PrintGCTimestamps & -XX:+PrintGCDatestamps

带上日期:

2019-09-24T22:15:24.518+0800: 3.287: [GC (Allocation Failure) [PSYoungGen:136162K->5113K(136192K)] 141425K->17632K(222208K),0.0248249 secs] [Times:user=0.05 sys=0.00,real=0.03 secs]

2019-09-24T22:15:25.559+0800: 4.329: [GC (Metadata GC Threshold) [PSYoungGen:97578K->10068K(274944K)] 110096K->22658K(360960K),0.0094071 secs] [Times: user=0.00 sys=0.00,real=0.01 secs]
2019-09-24T22:15:25.569+0800: 4.338: [Full GC (Metadata GC Threshold) [PSYoungGen:10068K->0K(274944K)]

[ParoldGen:12590K->13564K(56320K)] 22658K->13564K(331264K),[Metaspace:20590K->20590K(1067008K)],0.0494875 secs] [Times: user=0.17 sys=0.02,real=0.05 secs]

总结 :

[GC[Full GC说明了这次垃圾收集的停顿类型,如果有Full则说明GC发生了"Stop The World"

不同的垃圾收集器在日志中的名称:

  • 使用Serial收集器在新生代的名字是Default New Generation,因此显示的是[DefNew
  • 使用ParNew收集器在新生代的名字会变成[ParNew,意思是Parallel New Generation
  • 使用Parallel Scavenge收集器在新生代的名字是[PSYoungGen
  • 使用Parallel Old收集器收集器在老年代显示[ParoldGen
  • 使用G1收集器的话,会显示为garbage-first heap

Allocation Failure:表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
Metadata GCThreshold:Metaspace区不够用了
FErgonomics:JVM自适应调整导致的GC
System:调用了System.gc()方法

一般日志格式:

GC日志格式的规律一般都是:GC前内存占用->GC后内存占用(该区域内存总大小)

[PSYoungGen:5986K->696K(8704K) ] 5986K->704K(9216K)

  • 中括号内:GC回收前年轻代大小,回收后大小,(年轻代总大小)
  • 括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)

GC日志中有三个时间:user,sys和real

  • user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示GC线程执行所使用的 CPU 总时间。
  • sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的CPU 时间
  • real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。

日志分析原文

GC通用日志解读及相关博客

volitale

volatile关键字的作用主要有以下几点:

  • 确保内存可见性:当一个线程修改了一个volatile变量的值,其他线程会立即看到这个改变。这确保了所有线程看到的是一致的内存映像。
  • 防止指令重排序:JVM会在指令级别对程序进行重排序,以便更好地优化执行效率。但在某些情况下,这可能导致变量读取/写入操作被误排序,从而无法正确地反映出程序的意图。volatile关键字可以防止这种重排序的发生。
  • 禁止共享变量缓存:大多数现代处理器都有一种名为“缓存”的技术,这种技术会缓存一部分主内存中的数据,以提高程序的运行效率。但是,如果一个变量被声明为volatile,那么处理器就会知道这个变量是用于同步的,因此不能被缓存,从而确保所有线程都能看到最新的值。

内存屏障是什么

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:

  • 阻止屏障两侧的指令重排序;
  • 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载新数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。

下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障。

  • 在每个volatile读操作的前面插入一个LoadLoad屏障。

  • 在每个volatile读操作的后面插入一个LoadStore屏障。

volatile的实现原理-内存屏障

缓存一致性协议(MESI)

MESI协议只能保证并发编程中的可见性,并未解决原子性和有序性的问题,所以只靠MESI协议是无法完全解决多线程中的所有问题。

伪共享问题

Cpu缓存行读取数据至Cache中(一级、二级、三级)中,每次是按缓存行读取的,一个缓存行有64字节块。这就导致一个缓存行里有不同的数据,只要缓存行一个数据被修改过,根据MESI协议整改缓存行都会变成脏数据。

Cache Line伪共享处理方案
处理伪共享的两种方式:

增大数组元素的间隔使得不同线程存取的元素位于不同的cache line上。典型的空间换时间。(Linux cache机制与之相关)
在每个线程中创建全局数组各个元素的本地拷贝,然后结束后再写回全局数组。

伪共享及解决

举报

相关推荐

0 条评论