JVM第五课:Java运行时数据区和常用指令

Runtime Data Area 运行时数据区

最权威的参考文档:Java Virtual Machine Specification
Java运行时数据区以及JVM指令
在这里插入图片描述
i=i++结果为8
i=++i结果为9

class的生命周期

在这里插入图片描述

运行时数据区的构成

在这里插入图片描述

PC:peogram counter 程序计数器

存放指令位置
虚拟机的执行类似下面的循环:
while(not end){
取pc中的位置,找到对应位置的指令
执行该指令
PC++;
}

在这里插入图片描述

堆heap

所有线程共享同一个堆空间。
堆空间是用来存放所有类实例和数组空间分配的运行时数据区。
重点,后面GC详细学
在这里插入图片描述

栈stacks

在每一个线程创建的时候,线程会有自己独立的JVM栈空间,栈中存放的是栈帧(每个方法对应一个栈帧)
在这里插入图片描述

本地方法栈native method stacks

对应c、c++编写的native方法,实际上和stacks类似

直接内存Direct Memory

从JVM内可以访问OS管理的内存(内核空间),可以提高IO效率(零拷贝),JDK1.4新增的.
比如一个网络请求传过来一个数据到OS内核空间中,1.4以前使用这个数据时需要把内核空间中的数据拷贝到JVM内存中;
1.4之后,通过NIO可以使用直接内存,直接访问内核空间的该数据,不需拷贝.

简单来说就是JVM可以直接访问的内核空间的内存(OS管理的内存)
NIO,提高效率,实现zero copy零拷贝

方法区method area

class结构存放的地方,所有线程共享
方法区中还有块 run-time constant pool 常量池,存放class文件的常量池(constant_pool)

方法区是一个逻辑上的概念
JDK1.8之前,方法区由永久区(Permament Space)实现,字符串常量也在永久区,FGC不会清理;JVM启动时指定永久区空间大小,不能改变
自JDK1.8开始,方法区由Meta Space实现,字符串常量位于堆,会触发FGC 被清理.可以指定大小,如果不指定的话,最大就是物理内存空间.

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

栈帧Frame

A Frame is used to store data and partial results,as well as to perform dynamic linking,return values for methods,and dispatch exceptions.
在这里插入图片描述
每个方法对应着一个栈帧,栈里放着一个个栈帧
主要包括:

  1. 局部变量表(local variable table)
    注意,形参也在局部变量表;如果是成员方法,this也在局部变量表
    变量表从下标0开始,排序优先级为:this > 形参 > 方法中的变量
    形参和方法中的变量按出现的顺序排列
  2. 操作数栈(Operand Stacks)
    里面存的是一个个操作数,比如_load指令会把一个值压栈,_store指令会把栈顶的值弹出.
    对于long的处理(store and load),多数虚拟机的实现都是原子的
    jls 17.7,没必要加volatile
  3. dynamic linking,
    指向运行时常量池
    比如a() -> b(),方法a调用了方法b,class文件解析时把方法放在运行时常量池中了,这个就是用来找到b
    jvms2.6.3
    https://blog.csdn.net/qq_41813060/article/details/88379473
  4. return address
    a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方

Instruction栈的指令集

常见的几个指令:

: 静态语句块
: 构造方法
_store : 出栈,并赋值给一个局部变量
_load : 压栈
invoke_XXX : 调用方法,这个指令很复杂,下面单独拎出来说
dup : 把栈顶的东西复制一份,然后把其压栈,一般是调用实例方法前会dup

面试题理解栈的指令集

  • 理解局部变量表
  • 理解操作数栈
  • 理解一些常用的指令
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;

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

具体原因我们来通过JClassLib查看字节码,一探究竟:
无论i = i++;还是i = ++i;,局部变量表是一样的:
查看的是main方法,静态方法,所以0位置是形参String[] args
1位置是方法中的第一个变量 i.
在这里插入图片描述

  • 当i = i++; 时,用JClassLib查看字节码:
 0 bipush 8 // 把8压栈,用int扩展这个立即数
 2 istore_1 // 出栈,把栈顶的数弹出,并赋给局部变量表下标为1的变量,就是变量i; 到此 int i = 8; 这句完事
 3 iload_1 // 把i压栈,从(下标1)本地变量表中拿值放到栈中(Operand Stack),此时i=8,所以栈中是8
 4 iinc 1 by 1 // 局部变量表1的位置自增1,(第一个1表示局部变量表的下标),此时局部变量表的i值为9
 7 istore_1 // 出栈,把栈顶的值赋给局部变量表下标为1的变量,此时栈中仍是8,所以最后i=8.  到此,i=i++; 这句完事
 // 下main的就是System.out.println(i);了
 8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
  • 当i = ++i; 时,用JClassLib查看字节码:
 0 bipush 8 // 8压栈 
 2 istore_1 // 8出栈, int i=8;完事
 3 iinc 1 by 1 //  i自增1,此时i=9
 6 iload_1 // 把i压栈,此时i已经时9了,栈中也是9
 7 istore_1 // 9出栈,赋值给i,所以此时i=9, i=++i;完事
 // 下main的就是System.out.println(i);了
 8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return

总结:
i++ 是先把i的值压栈,然后把局部变量i自增,代码层面可以理解为先使用i的当前值,用完了让i自增;
++i 是先把局部变量i自增,然后把i的值压栈,代码层面可以理解为先给i+1,然后使用i自增后的值.

设计一台机器的指令集,有两种做法:

  • 基于栈的指令集(JVMStack选择的方式)
  • 基于寄存器的指令集(汇编语言)(HotSpot的局部变量表类似于寄存器)
    比较复杂但是比较快
  • 最终在硬件层面都是基于寄存器的指令集
    单条指令也不一定是原子性的

例子理解

在这里插入图片描述
下面这个i因为超过了127,所以用的是sipush,而不是上图中的bipush
在这里插入图片描述
为什么我们可以在非static方法中使用this?因为this在局部变量表中是已经存在的。
(局部变量表中,0位置是this,1位置是k,2位置是i)
在这里插入图片描述
调用add(3,4)
在这里插入图片描述
dup,复制操作数栈的栈顶元素
之前有一道面试题:DCL 单例为什么要加 volitile?因为你看下面的第一条指令,我们知道,刚new出来对象是半初始化的对象,只是赋一个默认值,而involespecial才是调用构造方法,给变量赋初始值,而这两条指令之间是可能会发生指令重排的。
在这里插入图片描述
返回值
在这里插入图片描述
递归的调用
下面是m方法的执行,没有把main方法放上来
在这个3层递归中,使用到的是3个栈

另外,我们看到指令前面的数字 0,1,2,5,6,…,没有3,4的原因,是2指令的字节数比较多,占用了后面的字节数
在这里插入图片描述

invoke指令

  1. InvokeStatic 调静态方法
  2. InvokeVirtual 调一般的实例方法,自带多态
  3. InvokeInterface 通过接口调用的方法
  4. InvokeSpecial
    可以直接定位,不需要多态的方法
    具体有:private 方法 , 构造方法
    final方法不是invokeSpecial
  5. InvokeDynamic
    JVM最复杂的指令,>=JDK1.7
    lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
    每一个lambda表达式都有一个自己的内部类,java没有纯粹的函数。匿名内部类每次都是动态产生的。

lambda表达式,其实就是一个语法糖,匿名内部类的简化写法,但是具体细节应该有很多不同,

关于Lambda表达式的一个坑
如果你用Lambda表达式写出了这样的代码:

for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)

在1.8之前有一个巨大的bug,就是你在里面产生了很多对象,但是Perm Space在FGC的时候是不会回收的。

思考:
如何证明1.7字符串常量位于Perm,而1.8位于Heap?
提示:结合GC, 一直创建字符串常量,观察堆,和Metaspace

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值