JVM 相关知识梳理(二) ----运行时数据区、Java内存模型、GC

运行时数据区

这个名词听起来陌生也不陌生…但是它的定义到底是什么呢?

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

大意是:

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。

其中一些数据区域是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会销毁。

其他数据区域是每个线程,每个线程的数据区域在线程创建时创建,在线程退出时销毁。


运行时数据区-区域划分 :

1)方法区:
  1. 方法区是各个线程共享的的内存区域。在虚拟机启动时候创建。

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is created on virtual machine start-up.

  1. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。

Although the method area is logically part of the heap,…

  1. 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

  1. 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

在上一篇文章的类加载篇幅,我们提到了Classs文件除了有类的类版本、字段、方法、接口等描述信息(也含有常量池信息(javaP查看)),用于存放编译期产生的各种字面量和符号引用。

呢么这部分内容将在类加载后保存到方法区的运行时常量池

2)堆:

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
The heap is created on virtual machine start-up.

  1. Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。
  2. Java对象实例以及数组都在堆上分配。

在上一篇文章中,介绍了对象的创建流程,就是这里的对象创建。产生了对象,便可以作为对方法区访问的入口。

但是,随着JIT编译器的发展与逃逸分析技术逐渐成熟,在栈上分配内存、标量替换优化,所有的对象分配在堆上也不绝对。

堆内存区域在细分可以分为 EdenFrom SurvivorTo Survivor ,以及线程私有的分配缓冲区(TLAB)

3)虚拟机栈:
  1. 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread.

  1. 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。

A Java Virtual Machine stack stores frames (§2.6).

  1. 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes.

每个方法在执行的同时都会创建一个栈帧,用于存储局部表量表动态链接方法出口等;
每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中从入到出的过程。

栈帧中的分配布局

局部表量表:

  1. 方法中定义的局部变量以及方法的参数存放在这张表中。
  2. 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。

操作数栈: 以压栈和出栈的方式存储操作数的。

它主要保存计算过程中的结果,同时作为计算过程临时变量的存储空间。
在这里插入图片描述

帧数据区:
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接

在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。

这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。

方法返回地址:当一个方法开始执行后,只有两种方式可以退出。 一种是遇到了方法返回的字节码指令; 一种是遇见异常,并且这个异常没有在方法体内得到处理。

4)程序计数器:

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。

假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。

如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空;

5)本地方法栈:

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用native的方法呢?
在这里插入图片描述

以上5个部分按照线程独享/共享分类

在这里插入图片描述

以上5个结构按照GC分类:

在这里插入图片描述


JVM内存模型

运行时数据区跟JVM内存模型两个概念对于初次接触,是很容易产生懵逼感的一对概念。我们暂时先理解为 JVM运行时数据区是一种规范,而JVM内存模型是对该规范的实现。 在该实现中,我们关注堆与方法区(非堆),其余几块区域我们暂且按住不表。

堆区 与 非堆 在jmm概念中, 分配大致如下:
在这里插入图片描述
对以上区域划分,我们结合GC相关的概念,以FAQ的形式进行讨论,来阐明它这几个区域都是什么意思,以及为什么这么分(你需要知道GC的大概流程)。

case 1 : GC 名词解释

  • Minor GC:新生代
  • Major GC:老年代
  • Full GC:新生代+老年代

case 2 : 为什么需要Survivor区(S0、S1)? 只有Eden不行吗?

如果没有Survivor区,Eden区每一次的minor GC 后,仍旧存活的对象只能投递到老年代里面,这样老年代的空间会很快被打满,进而触发Major GC(因为Major GC 一般伴随着minor GC ,也可以看做触发了Full GC)。

老年代的内存空间是远大于新生代的,触发一次FULL GC 消耗的时间会很长 ,并且FULL GC 的情景下,根据垃圾回收器的不同,会生产STW,暂停业务线程。

诚然,我之前觉得这种STW中断业务线程是差强人意可以接受的,但随着业务量增加,生产触发了一次3S的STW ,百笔交接直接被网关熔至超时,这个月的成功率又达不到相关指标了。。。

所以Survivor区存在的意义,可以理解为一个中间区域。尽可能在这里对对象进行GC,提供老年代入场门槛,最终规避大的FULL GC。

case 3 : 为什么需要两个Survivor区?

这里个人理解是 跟GC算法中的复制算法初衷相似,主要是避免空间碎片化。
eden满后了 -> minor GC -> s0 。下一次s0 与 s1 进行对调。

case 4 : 新生代中Eden:S1:S2为什么是8:1:1 ?

新生代中的可用内存:复制算法用来担保的内存为9:1。
可用内存中 Eden:S1 = 8:1
即新生代中 Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的。

case 5 : 堆内存中都是线程共享的区域吗?

JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。

以上篇幅尝试解释了jmm实现的思路,呢么管对象创建也得管’‘埋’’ , GC 相关内容与行为又是怎么样的呢?


GC

先问是不是在,在问为什么。

GC 这里的知识也同样适合这句话,是不是垃圾?如果是垃圾如何回收?

1)如何判定相关对象可被GC?
  1. 引用计数器法:对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。 但是 A->B 、B->A 这样的指向关系难道不算垃圾? 其实是算的,他们理应称之为 ‘‘一对垃圾’’ 。
  2. 因为有上述问题,现在的垃圾判定往往都是基于可达性分析

可达性分析:通过GC ROOT 对象,开始向下寻找,看某个对象是否可达。

而可以做GC ROOT的对象有 类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

而从跟进行检索的时候,又引入了三色标记的概念。

GC Roots开始遍历,怎么才能高效的标记这些对象,这就是三色标记法的作用了。因为如果堆内的对象越多,那么显然标记产生的停顿时间就越长。

2)我该什么时候进行GC ?

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。

但下述操作有可能会触发GC:

  1. 当Eden区或者S区不够用了
  2. 老年代空间不够用了
  3. 方法区空间不够用了
  4. System.gc()

但GC也不是触发了就会发生,只有stw(stop the world)即用户线程停下来后才会执行,那么如果千万线程如何治理呢?就是此处的安全点安全区域

安全点的选择
安全点的选择很重要, 如果太少可能导致GC 等待的时间太长, 如果太频繁可能导致运行时的性能问题。
大部分指令的执行时间都非常短暂,通常会根据“ 是否具有让程序长时间执行的特征” 为标准。
比如: 选择一些执行时间较长的指令作为safe Point ,如:

  • 循环的末尾
  • 方法临返回前
  • 调用方法之后
  • 抛异常的位置

GC时线程的中断策略

如何在GC生时, 检查所有线程都跑到最近的安全点停顿下来呢?

抢先式中断: ( 目前没有虚拟机采用了)
首先中断所有线程。如果还有线程不在安全点, 就恢复线程, 让线程跑到安全点。
主动式中断
设置一个中断标志, 各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真, 则将自己进行中断挂起。

(客栈门上安装了一个显示器,上面会显示ture,或者false。如果系统需要垃圾回收,就会更新这个状态为true,线程到了客栈后看到为true,就进店别别出来了。)

安全区域Safe Region
SafePoint 机制保证了程序执行时, 在不太长的时间内就会遇到可进入GC的safepoint 。但是, 程序“ 不执行” 的时候呢?
例如线程处于Sleep 状态或Blocked 状态, 这时候线程无法响应JVM的中断请求, “ 走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况, 就需要安全区域(Safe Region) 来解决。

安全区域是指在一段代码片段中, 对象的引用关系不会发生变化, 在这个区域中的任何位置开始GC 都是安全的。我们也可以把Safe Region 看做是被扩展了的Safepoint

安全区域执行流程
实际执行时:
1 、当线程运行到 Safe Region 的代码时, 首先标识己经进入了safe Region ,如果这段时间内发生GC ,JVM会忽略标识为 Safe Region 状态的线程(认为它是安全的):
2 、当线程即将离开时, 会检查JVM是否己经完成GC , 如果完成 , 则继续运行, 否则线程必须等待直到收到可以安全离开Safe Region 的信号为止。

举个生活列子:
一句话,睡觉可以,请先进入酒店再睡觉,并且进去是在屏幕上说一声我进入安全区域了,在睡觉。这样做的目的就是当要GC的时候不至于找不到你,如果看到屏幕上有你这个线程的名字,就知道你是安全的,就会忽略你;
你(线程)睡醒了要出门了。抬头看看大屏幕是不是安全在出去(true),如果不安全(false)就在酒店待着,别出门,等到gc完成后,状态变为false在出门。

R大发表于知乎的解释:https://www.zhihu.com/question/29268019/answer/43762165


3)我该怎么进行GC ?

第一种 :

从堆中所有的对象进行一次扫描,一个个问对象还活着吗?谁该被回收了,打上一个TAG。 这个行为我们称之为 标记

接下来我们对刚打上tag的对象,进行回收,做相应空间的释放。这个行为我们称之为清除

产生出来Plan A : 标记-清除

标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  1. 标记和清除两个过程都比较耗时,效率不高
  2. 会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

第二种 :

我们直接把内存分为两组相等的区域,每次只用一组。当一组空间告罄时,我们将存活的对象复制到另外一块区域,随后将使用的内存空间整体清理掉。

产生出来Plan B : 标记-复制

缺点:

  • 空间利用度低。
  • 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
  • 如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况(==》老年代无法使用这种方式)。

第三种 :

在第一种标记-清除中,如果后续步骤不直接对可回收的对象进行清理,而是让所有存活的对象都移向另一端,随后直接清理掉边界以外的内存。
呢么这种做法 我们可以称之为 标记-整理

第四种 :

Young区:对象在被分配之后,可能生命周期比较短。
Old区:Old区对象存活时间比较长,复制来复制去没必要。

新生代、老年代 各自特点是不一致的,如果可以有针对的选择垃圾回收算法,呢么应该是一种不错的选择。
对于新生代的"朝生夕死“ ,标记-复制 或许是一个不错的选择。
对于老年代的GC成功率,标记-整理或许更为稳妥。

呢么Plan C ,就是 分代收集了。

4)垃圾回收器又都有什么呢 ?

CMS:是一种以获取最短回收停顿时间为目标的收集器。

  • 初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快
  • 并发标记 CMS concurrent mark 进行GC Roots Tracing
  • 重新标记 CMS remark 修改并发标记因用户程序变动的内容
  • 并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾。

由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

优点: 并发收集 ,停顿低。
缺点: 产生大量空间碎片、并发阶段会降低系统吞吐量。

G1

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂
如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中。

设置Region大小:-XX:G1HeapRegionSize=M

所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域:

  1. 分代收集(仍然保留了分代的概念)
  2. 空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
  3. 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)

在这里插入图片描述

CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 MixGC;

G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;

在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值