JVM 相关知识点记录


前言

JVM包含内容:

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

在这里插入图片描述

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

在这里插入图片描述

注意: jdk1.6 静态变量和字符串常量池在方法区, 方法区的实现方式是永久代(采用jvm内存)
注意: jdk1.7 静态变量和字符串常量池在堆中, 方法区的实现方式是永久代(采用jvm内存)
注意: jdk1.8 静态变量和字符串常量池在堆中, 方法区的实现方式是元空间(采用本地内存)

静态成员的存储位置变化:
在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内存分配

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语言层面的常量概念。就是我们提到的常量。而符号引用则属于编译原理的方面的概念。包括以下三类常量:

    • 类和接口的全限定名

    • 字段的名称和描述符

    • 方法的名称和描述符

类加载流程

类加载机制

JVM的类加载过程主要包括加载、链接(验证、准备、解析)和初始化三个阶段。

  • 加载阶段。这个阶段的主要任务是将类的二进制数据读入到内存中,并在堆区创建一个代表这个类的java.lang.Class对象,作为对方法区内这些数据的访问入口。类的二进制数据可以来源于.class文件、网络、其他文件等形式。

  • 链接阶段。链接阶段分为验证、准备和解析三个步骤。

    • 验证是对字节码文件内容的校验,确保其符合JVM规范且不会危害虚拟机安全;
    • 准备阶段是为类变量(静态变量)分配内存并设置初始值,如果类变量是static final的常量,则直接使用字面值,否则会使用0或null作为初始值;
    • 解析阶段是分析和解析类中的符号引用,确保类与类之间的相互引用正确性。
  • 初始化阶段。初始化阶段主要是执行类中的静态代码块和静态变量的赋值操作。如果类的父类尚未初始化,也会先初始化父类。初始化阶段是基于“主动使用”的原则,即只有在类被主动使用时才进行初始化。

类加载

哪些内存需要回收

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

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

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

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的class对象等;
  • 所有被同步锁(synchronized 关键字)持有的对象;
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

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垃圾回收的频率是调优的重点

目前所有垃圾收集器都会进入STW

CMS收集器

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

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

cms回收老年代失效的情况:

  • 并发失效
    并发清理阶段,工作线程和垃圾回收线程并发工作的时候,此时工作线程会不断产生新的垃圾,但是垃圾回收线程并不会去处理这些新生成的垃圾对象,需要等到下次垃圾回收的时候才会去处理,这些垃圾对象称之为:浮动垃圾 。因为有这些浮动垃圾的存在,所以老年代不能在100%使用的时候才去进行垃圾回收,否则就放不下这些浮动垃圾了。有一个参数是“-XX:CMSInitiatingOccupancyFraction”,这个参数在jdk1.6里面默认是92%,意思是老年代使用了92%的空间就会执行垃圾回收了。但是即使预留了8%的内存去存放浮动垃圾,但是还是有可能放不下,这样就会产生Concurrent Mode Failure问题。一旦产生了Concurrent Mode Failure问题,系统会直接使用Serial Old垃圾回收器取代CMS垃圾回收器,从头开始进行GC Roots追踪对象,并清理垃圾,这样会导致整个垃圾回收的时间变得更长。
    解决办法就是根据系统的需求,合理设置-XX:CMSInitiatingOccupancyFraction的值,如果过大,则会产生Concurrent Mode Failure问题,如果设置的过小,则会导致老年代更加频繁的垃圾回收。

  • 晋升失败
    CMS开启新生代垃圾收集的时候,判断老年代似乎有足够空间容纳所有晋升对象。然而晋升的时候才发现老年代的空间竟然都是碎片化的,根本容纳不了一个完整的晋升对象。剩下出路只有内存整理。所有应用运行的线程停止,CMS开始对老年代进行整理和压缩。空间压缩要通过移动里面的对象,令这些对象排列好,所以晋升失败比不需要移动对象的并发失效更加浪费时间。完成清理的堆空间变得规整和空余,继续运行应用。

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 中。

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

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

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

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的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通用日志解读及相关博客

JMM

Java内存模型(Java Memory Model)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范(定义了程序中各个变量的访问方式)。

JMM定义了关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从 工作内存同步回主内存这一类的实现细节,Java内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作,Java虚拟机实 现时必须保证下面提及的每一种操作都是原子的、不可再分的。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分)。JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(Working Memory,可与前面讲的处理器高速缓存类比),用于存储线程私有的数据,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下图。

在这里插入图片描述

Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。

Java内存模型与硬件内存架构的关系

通过对前面的硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注意对于Java内存区域划分也是同样的道理)

在这里插入图片描述

数据同步八大原子操作:

  • lock(锁定): 作用于主内存的变量,把一个变量标记为一条线程独占状态
  • unlock(解锁): 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后 的变量才可以被其他线程锁定
  • read(读取): 作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存 中,以便随后的load动作使用
  • load(载入): 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工 作内存的变量副本中
  • use(使用): 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  • assign(赋值): 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内 存的变量
  • store(存储): 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存 中,以便随后的write的操作
  • write(写入): 作用于工作内存的变量,它把store操作从工作内存中的一个变量的值 传送到主内存的变量中

在这里插入图片描述

如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。注意,Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。也就是说read与load之间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。

JMM解析

volitale

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

  • 确保内存可见性:当一个线程修改了一个volatile变量的值,其他线程会立即看到这个改变。这确保了所有线程看到的是一致的内存映像。
    volatile 内存可见性主要通过 lock 前缀指令实现的,它会锁定当前内存区域的缓存(缓存行),并且立即将当前缓存行数据写入主内存(耗时非常短),回写主内存的时候会通过 MESI 协议使其他线程缓存了该变量的地址失效,从而导致其他线程需要重新去主内存中重新读取数据到其工作线程中。

  • 防止指令重排序:JVM会在指令级别对程序进行重排序,以便更好地优化执行效率。但在某些情况下,这可能导致变量读取/写入操作被误排序,从而无法正确地反映出程序的意图。volatile关键字可以防止这种重排序的发生。
    volatile 的有序性是通过插入内存屏障(Memory Barrier),在内存屏障前后禁止重排序优化,以此实现有序性的。

  • 禁止共享变量缓存:大多数现代处理器都有一种名为“缓存”的技术,这种技术会缓存一部分主内存中的数据,以提高程序的运行效率。但是,如果一个变量被声明为volatile,那么处理器就会知道这个变量是用于同步的,因此不能被缓存,从而确保所有线程都能看到最新的值。

volitale 能保证可见性、有序性,不能保证原子性。

内存屏障是什么
内存屏障(Memory Barrier 或 Memory Fence)是一种硬件级别的同步操作,它强制处理器按照特定顺序执行内存访问操作,确保内存操作的顺序性,阻止编译器和 CPU 对内存操作进行不必要的重排序。内存屏障可以确保跨越屏障的读写操作不会交叉进行,以此维持程序的内存一致性模型。

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

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

在 Java 内存模型(JMM)中,volatile 关键字用于修饰变量时通过内存屏障的插入来实现有序性:

  • 写内存屏障(Store Barrier / Write Barrier):当线程写入 volatile 变量时,JMM 会在写操作前插入 StoreStore 屏障,确保在这次写操作之前的所有普通写操作都已完成。接着在写操作后插入 StoreLoad 屏障,强制所有后来的读写操作都在此次写操作完成之后执行,这就确保了其他线程能立即看到 volatile 变量的最新值。

  • 读内存屏障(Load Barrier / Read Barrier):当线程读取 volatile 变量时,JMM 会在读操作前插入 LoadLoad 屏障,确保在此次读操作之前的所有读操作都已完成。而在读操作后插入 LoadStore 屏障,防止在此次读操作之后的写操作被重排序到读操作之前,这样就确保了对 volatile 变量的读取总是能看到之前对同一变量或其他相关变量的写入结果。

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

如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。下面是基于保守策略的JMM内存屏障插入策略:

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

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

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

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

volatile的实现原理-内存屏障

缓存一致性协议(MESI)

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

MESI 协议定义了高速缓存中数据的四种状态:

  • Modified(M):表示缓存行已经被修改,但还没有被写回主存储器。在这种状态下,只有一个 CPU 能独占这个修改状态。

  • Exclusive(E):表示缓存行与主存储器相同,并且是主存储器的唯一拷贝。这种状态下,只有一个 CPU 能独占这个状态。

  • Shared(S):表示此高速缓存行可能存储在计算机的其他高速缓存中,并且与主存储器匹配。在这种状态下,各个 CPU 可以并发的对这个数据进行读取,但都不能进行写操作。

  • Invalid(I):表示此缓存行无效或已过期,不能使用。

MESI 协议的主要用途是确保在多个 CPU 共享内存时,各个 CPU 的缓存数据能够保持一致性。当某个 CPU 对共享数据进行修改时,它会将这个数据的状态从 S(共享)或 E(独占)状态转变为 M(修改)状态,并等待适当的时机将这个修改写回主存储器。同时,它会向其他 CPU 广播一个“无效消息”,使得其他 CPU 将自己缓存中对应的数据状态转变为I(无效)状态,从而在下次访问这个数据时能够从主存储器或其他 CPU 的缓存中重新获取正确的数据。

这种协议可以确保在多处理器环境中,各个 CPU 的缓存数据能够正确、一致地反映主存储器中的数据状态,从而避免由于缓存不一致导致的数据错误或程序异常。

详解博客

伪共享问题

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

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

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

伪共享及解决


记录博客

JVM内容解析

程序计数器、栈

JVM内存分配机制

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值