JVM:(二)JVM运行时数据区

上一篇:JVM:(一)建立JVM大局观

一、官网对运行时数据区的解释

在这里插入图片描述
翻译:
在程序的运行过程中,为了支持程序的运行JVM中定义了各种不同的运行时区域,有些区域是虚拟机启动时便存在,当虚拟机销毁时退出;有些区域是随着线程的创建而创建,随着线程的结束而销毁。

二、运行时数据区

JVM运行时数据区,是Java虚拟机在运行时对该Java进程占用的内存进行的一种逻辑上的划分,包括方法区、堆内存、虚拟机栈、本地方法栈、程序计数器,这些区域各自负责不同的职责,都有各自的用途。下面会逐个说下我对这些概念的理解。

官方对jdk1.8中jvm的解释:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

先上图:
数据区属于线程共享区域,运算区属于线程私有区域

在这里插入图片描述

1. 程序计数器(The pc Register)

由于JVM同时可以处理多个线程所以就涉及到一些线程调度,当cpu暂停运行线程A把时间片让给线程B的时候我们需要保存线程A被暂停执行前的一些现场状态,需要记录当前执行到那一行字节码了,所以具备保存现场的功能。
每条线程都有自己的程序计数器,在任意时刻虚拟机只会执行一个方法,如果执行的方法不是native方法,程序计数器则保存指向当前执行字节码的指令地址,如果执行的是native方法,程序计数器会保存undefined。

(1)生命周期与线程一致,线程启动而产生,线程结束而消亡。每个线程都有自己的程序计数器,线程之间互不影响。
(2)记录当前JVM的线程执行过程中的执行位置.当出现CPU调度,cpu执行时间片切换的情况.可以做到有据可查.从上次运行的位置继续执行。
(3)运行数据区中唯一不会出现OOM(内存溢出OutOfMemoryError)的区域,没有垃圾回收。

2. Java虚拟机栈(Java Virtual Machine Stacks)

(1)java虚拟机栈中的生命周期与执行的线程一致。
(2)java虚拟机栈以 栈 的数据结构形式存在。虚拟机栈是线程执行的区域,它保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由当前线程挂钩的虚拟机栈来保存。
(3)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
(4)当java虚拟机栈的空间不够使用时,将报出StackOverflowException(栈溢出异常)。

2.1 虚拟机栈与栈帧

虚拟机栈中存储的是栈帧(Frame),可以把栈帧当作java中的一个方法,后面会详细说栈帧的概念。

在这里插入图片描述
可以从下面的例子中形象的理解虚拟机栈和栈帧。

在这里插入图片描述
在这里插入图片描述
执行a方法时会生成一个栈帧压入虚拟机栈中,a调用b,b调用c同理,当c方法执行完后c对应的栈帧就会出栈,其次就是b栈帧出栈,最后a栈帧出栈。

2.2 StackOverflowException

当java虚拟机栈的空间不够使用时,将报出StackOverflowException(栈溢出异常)。

我们用下面的例子去理解这句话。

在这里插入图片描述
由例子可以看出,当调用到第5387个方法的时候就会出现栈溢出异常,即栈的深度可以装下5387个栈帧。栈的内存大小可以用 -Xss 这个命令去控制它,例如下面,我们把栈的内存设置成2M,看下执行结果。

在这里插入图片描述
在这里插入图片描述
大约扩大了一倍左右,所以可以看出,在设置之前虚拟机栈默认的大小是1M左右。

2.3 栈帧(Stack Frame)

2.3.1 概述

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

在这里插入图片描述
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。

2.3.2 局部变量表(Local Variables)
1.局部变量表也被称之为局部变量数组或本地变量表。
2.可以理解成局部变量表就是一个储存数据的数组,数组主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddressleixing。
3.如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存在index为0 处,非静态方法,都会创建this的一个参数,index为0,其余的参数是按照顺序排放的,static 方法不可以使用this是因为static方法中没有放this的index。
4.由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题。
5.局部变量表所需的容量大小是在编译期确定下来的,在方法运行期间是不会改变局部变量表的大小的。
6.方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间。
7.局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

我们用下面的例子去理解3的内容:
下面两个方法对应的局部变量表的数组中各存多少个数据?
在这里插入图片描述
第一个方法对应的局部变量表的数组会存三个数据,分别是op1、op2、result,对应的下标分别是0、1、2。
第二个方法对应的局部变量表的数组会存四个数据,分别是this、op1、op2、result,对应的下标分别是0、1、2、3。

可能this这个概念不太好理解,我们可以自己实验一下:

在这里插入图片描述
可以看出静态方法b是不能用this引用的,但是a方法却可以使用,这就是因为非静态方法对应栈帧的局部变量表存储了this引用,而静态方法没有存储。

2.3.3 操作数栈(Operand Stacks)
概念

操作数栈是属于栈帧中的栈,其实它的全名叫做当前栈帧的初操作数栈。栈,栈帧,操作数栈的关系需要梳理清楚:

  1. 栈:是虚拟机运行时数据区的一个逻辑区域,里面存储了一个个栈帧。
  2. 栈帧:栈帧代表一个方法的整个生命周期,里头存储了局部变量表,操作数栈,动态链接等等
  3. 操作数栈: 刚刚创建时操作数栈是空的。虚拟机提供一些指令从局部变量表把一些常量或者变量值加载到操作数栈,也提供了从操作数栈取走数据的指令。操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)。

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。(如字节码指令bipush操作)
比如:执行复制、交换、求和等操作
代码举例

在这里插入图片描述

操作数栈特点
  1. 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
  2. 操作数栈就是jvm执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
  3. 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的code属性中,为max_stack的值。
  4. 栈中的任何一个元素都是可以任意的java数据类型
    32bit的类型占用一个栈单位深度
    64bit的类型占用两个栈深度单位
  5. 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈push和出栈pop操作来完成一次数据访问
  6. 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器(程序计数器)中下一条需要执行的字节码指令。
  7. 操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类验证阶段的数据流分析阶段要再次验证。
  8. 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
操作数栈代码追踪

下面说的PC寄存器就是程序计数器

结合上图结合下面的图来看一下一个方法(栈帧)的执行过程
①15入栈;②存储15,15进入局部变量表
注意:局部变量表的0号位被构造器占用,这里的15从局部变量表1号开始

在这里插入图片描述
③压入8;④8出栈,存储8进入局部变量表;

在这里插入图片描述
⑤从局部变量表中把索引为1和2的是数据取出来,放到操作数栈;⑥iadd相加操作

在这里插入图片描述
⑦iadd操作结果23出栈⑧将23存储在局部变量表索引为3的位置上istore_3
在这里插入图片描述

指令集架构

个人认为正是因为jvm用的是栈式指令集架构,才会导致有些情况会出现线程不安全的原因。比如 i++ 这个操作就是因为被拆分成了好几个指令,导致它不是原子性操作,所以当cpu切换时间片时,有可能没有执行完i++对应的指令导致最终的数据不正确。

在这里插入图片描述
参考文章:https://www.yuque.com/vpwpw5/wu5tdl/zp2eo4

2.3.4 动态链接(Dynamic Linking)

参考文章:https://www.yuque.com/vpwpw5/wu5tdl/krh1ne

2.3.5 方法返回地址(Return Address)

参考文章:https://www.yuque.com/vpwpw5/wu5tdl/du5kzo

3. 本地方法栈(Native Method Stacks)

本地方法栈和虚拟机栈所发挥的作用是非常相似的,它们之间的区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

4. 方法区(Method Area)

  1. 方法区的生命周期与jvm进程一致。
  2. 储存已被虚拟机加载的类型信息(类、枚举、接口、注解、类的版本、字段等信息),方法信息,域信息,运行时常量,静态变量(不同版本不一样),即时编译器编译后的代码。
  3. 运行时常量池属于方法区的一部分。
  4. 方法区从逻辑上来理解其本身也属于堆(Heap)的一部分,方法区又称之为Non-Heap(非堆),主要目的是为了区分理解。
  5. JDK8 之前,方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。
  6. 方法区内存不足时,将抛出OutOfMemoryError。

理解5是非常重要的,我们要知道方法区只是jvm的一个规范,即:

1.方法区 是 JVM 的规范,所有虚拟机 必须遵守的。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)
2.PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 并且只有 HotSpot 才有 PermGen space。

  而如 JRockit(Oracle)、J9(IBM) 虚拟机有 方法区 ,但是就没有 PermGen space。

  PermGen space 是 JDK7及之前, HotSpot 虚拟机 对 方法区 的一个落地实现。在JDK8被移除。

3.Metaspace(元空间)是 JDK8及之后, HotSpot 虚拟机 对 方法区 的新的实现。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
元空间相比 “永久代” 的优势:

字符串常量池存在 “永久代” 中,容易出现性能问题和 OOM;

类和方法的信息大小难以确定,给 “永久代” 的大小指定带来困难;

永久代会为 GC 带来不必要的复杂,在 “永久代” 中,元数据可能会伴随 full gc 发生而移动;

方便 HotSpot 与其他 JVM 的集成,因为 “永久代” 是 HotSpot 特有的。

5. 堆(Heap)

  1. 堆的生命周期与JVM进程一致。
  2. 堆是java虚拟机运行时数据区共享数据区最大的区域。
  3. "几乎"说有的对象和数组都在堆中进行分配。
  4. 堆是JVM GC工作的重点区域。
  5. 堆内存不足时,将抛出OutOfMemoryError。

5.1 堆的分代模型(先了解下,下节会说垃圾回收

在这里插入图片描述

下面有四个问题,我们从这个四个问题中详细的去了解堆。

1.为什么要分为 Method Area 和 Heap?
Method Area存储的是不经常变动的数据内容,基本不会发生频繁的GC工作,如类信息、方法信息、字段信息 、即时编译的代码;Heap中存储的是运行时产生的数据对象,针对这些内容会频繁进行内存管理工作。所以为了刚好的GC,分为了这两个区域。

2.为什么将堆分为 old(老年代) 和 young(新生代) 两部分?
堆是我们JAVA运行过程中最主要的共享工作内存,JVM机制是共享内存垃圾自动管理的虚拟机。如果我们的每一次对象回收都要扫描整个堆区,将带来很大的工作量和性能损耗。

3.为什么需要将young区分为Eden、s0、s1三个区域?
当young区的内存占用到达一定的量时,如果此时进行直接清理,带来的是空间碎片的问题。另外加上young区的对象一般都是朝生夕死,所以大部分的内容都会被清理。

4.新生代中 Eden:s0:s1 为什么是 8:1:1 ?
IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”。
8:1:1是基于大量实验和数据收集分析统计对比之后的比较合理的比例。

总之堆区和方法区的区域划分都是为了更好的去管理内存。

5.2 对象的一辈子

对象从出生到回收:
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我15岁的时候,爸爸说我该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

在这里插入图片描述
正常情况下我们可以按照上面的说法去理解java对象的一生,但其实这是不准确的。

"几乎"所有的对象和数组都在堆中进行分配。 这句话其实也说明了不是所有的java对象都在堆中。

栈上分配(JVM默认开启)
我们都是有经验的开发者,大家思考一下,在JAVA程序运行过程中,其实有很多的引用类型的对象作用域都不会逃逸到方法之外,即该对象的生命周期是跟方法一致的。对于这样类型的对象,我们是否一定要考虑有些对象可以不在堆空间上分配?如果都在堆空间上分配,在线程结束之后,该对象会成为Heap空间的垃圾,带来GC的性能消,所以针对不会逃逸出方法的对象,JVM允许将对象(聚合量)属性打散后分配在栈(线程私有的,属于栈内存)上这种方式就叫做栈上分配。

要想理解栈上分配就得知道什么是栈上逃逸,我们用下面的例子去了解它:

public class EscapeObject {

   /**
    * obj变量只会被a方法去使用,则不会发生逃逸。
    */
   public static void a() {
       Object obj = new Object();
   }

   public static Object object;
   /**
    * object可能被别处使用,则可能发生逃逸。
    */
   public void globalVariableEscape() {
       object = new Object();
   }

   /**
    * 方法返回值为方法内创建的对象,则该对象发生逃逸。
    */
   public Object methodReturnEscape(){return new Object();}

   /**
    * Object对象由test方法创建,可以看到该对象发生了逃逸。
    */
   public void invokeMethodEscape(){test(new Object());}
   private static void test(Object obj) {
       //......
   }
}

线程本地分配缓冲区(thread-local allocation blocks 简称 TLAB)
前面的内容我们已知常规场景下对象分配在heap-Young-Eden上,而堆是一个全局共享的区域,当多个线程同一时刻操作堆内存分配对象空间时,就需要进行同步或者说是串行的操作,不然一定会带来同一个空间争夺的并发问题。而采用同步(串行)带来对象分配定会让其分配效率变差(尽管JVM使用CAS处理分配失败)。

TLAB就是解决这个问题的设计。在Eden区当一个线程启动时开辟每一个线程私有的很小的缓冲空间,后续线程需要创建对象只要TLAB空间能放下就会在此空间进行创建,避免同步(串行),提升对象分配的效率。

总结
经过上面的总结,我们可以知道一个对象其实是按照下图的流程来分配的:
在这里插入图片描述

三、JVM体系的全局认识

在这里插入图片描述
下一篇:JVM:(三)JVM垃圾回收机制

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值