JVM之JMM


在上面的Class加载章节中,我们讲解了JVM是如何将Class文件加载进内存的。那当我们对这个Class进行实例化之后,JVM又是如何处理这个实例化对象的呢?我们今天来了解下Java的内存模型JMM

内存分布

当我们在代码中写完 T t = new T(),我们知道这是这是将Class T实例化了一个t对象,那这个t对象,在内存中是如何体现的呢?
主要分为普通对象和数组对象,差别不大,我们先看下普通对象的内存分布情况:
在这里插入图片描述
实例化的普通对象t的组成主要有4块内容:

  • markWord :对象头,里面有GC次数,锁等信息,占8字节
  • Class point:指针,用于将该实例指向内存中某一个具体的Class对象,4字节或者8字节(取决于是否开启指针压缩)
  • 实例数据:Class对象中定义从那些成员变量
  • padding 对齐:部位,为了使整个实例对象的大小为8字节的整数倍

可能大家还没怎么理解,举一个例子说明:

Object obj = new Object()

请问:obj对象有几个字节?
对照上面的讲解,首先markWord,8字节;Class point指针,将obj对象指向Object.Class对象,如果开启指针压缩,则占4个字节,如果没有则占8字节;没有实例数据,占0字节,下面到padding部分了,这个得计算一下了:
如果开启指针压缩,则obj现有字节数为8+4=12,那padding部分需要凑满最小的8字节整数倍为16,所以padding部分占16-12=4字节
如果没有开启指针压缩,则obj现有字节数为8+8=16,已经是8字节的整数倍,所以padding部分占16-16=0字节。

那如果实例对象是个数组,又是个上面情况呢?

T[] t = new T[2]

在这里插入图片描述
我们可以看到,对比普通实例对象,数组的对象会多出一项,用于记录数组的长度,占4个字节,其他都是一样的。

PS:简单介绍下开启指针压缩:在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力。可以通过使用命令:
-xx:+UseComparessedPointers 开启Class pointer的指针压缩
-xx:+UseComparessedOops 开启普通对象引用的指针压缩。

MarkWord

关于MarkWork的定义,是在markOops.hpp文件中,JDK的每个版本之间的定义稍有不同,且markWord里的内容,跟对象当前的状态有关,锁定状态/无锁状态等情况下是不一样的,大体如下:
在这里插入图片描述
解释一下:

  1. 当对象在无锁状态下,且对象没有被复写hashCode()方法,且被调用hashCode方法,JVM根据对象体的内容生成hashCode,也叫identityHashCode,写入markWord的前25位,接着的后4位代表分代年龄,后1位代表是否是偏向锁,再2位代表锁的状态
  2. 如果对象进入轻量级锁状态,前25+4位 记录的是拥有当前轻量级锁的线程ID指针等信息
  3. 如果对象是重量级锁的状态,同上
  4. 如果对象进入GC了,同上
  5. 如果对象进入偏向锁,同上
  6. 32位的JVM和64位的JVM在位数以及代表的内容上稍有不同,不过都是大同小异的,差不多这么理解就可以了。

同时通过上图,我们可以看到分代年龄是占了4位,转为10进制,取值范围位0-15,这也解释了为什么JVM默认将一个对象尝试回收了15之后就转移进入老年代,因为标记的最大值就是15,后面无法再次标记了。

运行时数据区

运行时数据区(run time data area)有下面几个部分组成:
在这里插入图片描述

  • program counter ,简称PC,也叫程序计数器,寄存器,用来存储需要执行的下一条指令地址
  • Direct Memory,直接内存,JVM直接从用户态读取内核态的数据
  • heap,堆,用来存放对象等
  • Method Area:方法区,存储:常量池(Constant poll)、方法数据、方法代、类的信息(Mate Space)等。
  • stack,栈,分有JVM虚拟机栈(JMS)以及本地方法栈(NMS),JMS:每个JMS里面都用来存放栈帧(Frame),而每个方法都会创建一个栈帧;NMS:主要与虚拟机用到的 Native 方法相关,一般情况下, Java 应用程序员并不需要关心这部分的内容。

运行时数据区,这个是JVM规范出来的,是一个逻辑概念,每个JVM厂商之间的具体实现会不一样,JVM规范中内存区域的划分和每家具体实现中jvm内存区域的划分,它们之间存在一个映射关系
如:Hotspot,在JDK1.8之前,Method Area对应是属于老年代,不会被GC,1.8之后定义出Meta Space,其归属于堆,是可以被GC的。

下面简单梳理下他们之间的关系:

  • 每一个线程都有自己的PC,虚拟机栈,本地方法栈,这些都是线程私有的
  • Method Area以及heap,放的都是class,常量等相关信息,属于线程共有

在这里插入图片描述

栈帧Frame

这个栈帧是个什么东西呢?一个Frame的组成有下面几个部分:

  • Local Variable Table :局部变量表
  • Operand Stack:操作数栈
  • Dynamic Linking : 动态链接。举个简单的例子,我们在一个方法A中调用了方法B,当执行到这一行时,该去哪里找B方法的相应信息呢?没错,就是通过Dynamic Linking,去Class的常量池中去找到B方法的信息,然后再执行B方法。
  • return address:返回值地址

Dynamic Linking和return address相信大家现在已经知道是什么意思了,Local Variable Table 和Operand Stack又具体是什么呢?

public class Main {
    public static void main(String[] args) {
        int i = 8;
        i = i++;
        System.out.println(i);
    }
}

首先我们使用javap命令将该方法的class文件进行反编译,
在这里插入图片描述
我们可以看到,在void mian()方法的反编译下面,有这么几块:

  • descriptor: ([Ljava/lang/String;)V , “[Ljava/lang/String”其中"[L"代表是个数组,“String”代表字符串,合起来就是表示,方法入参是个String类型的数组,后面的V代表方法返回值为void
  • flags:public,代表方法是public的
  • code:方法体,后面讲
  • LineNumberTable:行号
  • LocalVariableTable:这变就是局部变量表了

下面我们简单看下这个LocalVariableTable:
有两条记录,看"name"这一栏,有两个值args、i,这不正好是我们main方法里面的入参和局部变量i嘛!到这边相信大家已经很清楚Frame里面的局部变量表存放的是哪些信息了。

目前还剩下 Operand Stack操作数栈了,下面我们具体看下反编译后的code部分,不过在这之前,需要先了解下JVM的几个基本的指令:

指令描述
iconst_1int型常量值1进栈
bipush将一个byte型常量值推送至栈顶
istore_1将栈顶int型数值存入下标为1的局部变量(索引号是从0开始的)
iadd栈顶两int型数值相加,并且结果进栈
iload_1将下标为1的局部变量入栈(索引号是从0开始的)
new创建一个对象,并且其引用进栈
newarray创建一个基本类型数组,并且其引用进栈
invokevirtual调用实例方法
invokespecial调用超类构造方法、实例初始化方法、私有方法
invokestatic调用静态方法
invokeinterface调用接口方法
Code:
      stack=2, locals=2, args_size=1		//栈深度为2,局部变量表有2个值,入参1个值
         0: bipush        8	// 将8压栈,此时操作数栈的栈顶为8
         2: istore_1	//将栈顶数据出栈,也就是将8进行出栈,同时将8赋值给局部变量表下标为1的变量,也就是变量i,下标为0的局部变量为args
         3: iload_1	//将下标为1的变量压入操作数栈,也就是变量i入栈,经过上一步,局部变量i已经被赋值为8,所以入栈之后,栈顶为8
         4: iinc          1, 1	//对下标为1的变量+1
         7: istore_1		//同上,将栈顶的8出栈,同时将8赋值给局部变量表下标为1的变量,也就是变量i
         8: getstatic     #2   // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_1
        12: invokevirtual #3	 // Method java/io/PrintStream.println:(I)V
        15: return

在这里插入图片描述
回到上面的例子,方法A调用方法B,该线程的JVM栈情况是这样的:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值