JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))

简介: 这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。

前言

  1. 前边的 JVM知识体系学习 1-4讲的是 class loader (类加载)类对象等知识。那这里讲的就是 类加载之后运行时 的数据区域,也就是 java 运行时数据区(java runtime data area),如下图所示:
    在这里插入图片描述
  2. JVM 文档 是 JDK 13版本
  3. 本博客记录了JVM运行时区域的内容
    • 线程私有:JVM栈、本地方法栈、PC(程序计数器)
    • 线程公有:Java 堆(在第六节里详细讲解)、方法区(7之前永久代实现,7之后元空间实现)
  4. 还记录了 栈中指令涉及局部变量表、操作数栈的实现过程。举了多个例子。

零、问题引入

很简单的小程序,为什么执行第六行代码是8,执行第7行代码是9呢。引入问题,后面解答。

看了答案之后的思维发散:说明 i++ 还是 ++i,都不是原子操作,在字节码层面是由三个指令的,(题外话,也就是JVM知识体系三中的会可能发生指令重排的情况。)。

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);
    }
}

一、java Runtime Data Area

0、概述

  1. 数据运行时内存包含如下
    DM 是JDK1.4之后出现的,为NIO部分。
    在这里插入图片描述
  2. 对应的文档在哪呢

1、Program Counter 程序计数器(线程私有)

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

  • JVM是这样说的:
    1. Each Java Virtual Machine thread has its own pc (program counter) register.
    2. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method for that thread.
    3. If that method is not native , the pc register contains the address of the Java Virtual Machine instruction currently being executed.

2、JVM stacks(重点)(线程私有)

  • JVM 是这样说的:

    1. Each Java Virtual Machine thread has a private Java Virtual
      Machine stack, created at the same time as the thread.
    2. A Java Virtual Machine stack stores frames
  • 每一个线程对应一个栈,每个方法对应一个栈针。JVM 虚拟机 所管理的

3、Native Method Stacks本地方法栈(线程私有)

  • JVM 是这样说的:

    1. An implementation of the Java Virtual Machine may use
      conventional stacks called native method stacks
  • c 和 c++。java 调用了 JNC ,调用了 c 和 c++时就会调用 ,没法调试和调优

4、Direct Memory

JDK 1.4 版本之后 ,增加了一个新的 Direct Memory 即直接内存,NIO的内容。 所有的内存都归java 虚拟机(JVM)直接管理,为了增加IO的效率,在JDK1.4之后增加了 DM(直接内存),从java虚拟机内部 可以访问操作系统管理的内存的。用户空间可以访问内核空间的内存

5、Method Area 方法区(重点)(线程公有)

a、MA

  • JVM 是这样说的:

    1. The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads.
    2. It stores per-class structures
  • 存储各种常量池,包含 :(JVM知识体系学习一中的常量池就是第一个 Class常量池)

    1. Class文件中的常量池(存放存放两大常量:字面量(Literal)和符号引用(Symbolic References)。
      (下面小节专门讲解常量池。很详细)
    2. 运行时常量池(run-time constant pool,运行之后Class文件中的常量池加载到内存即运行时常量池)
    3. 字符串常量池(1.7 和静态变量放到堆上)、
    4. 基本数据类型包装类常量池(6个)(Float 和Double 没有)、静态变量
      上面这 6 种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
  • JVM 版本的 方法区

    1. Perm Space 永久代 (<=JDK1.6版本)
      Class常量池、字符串常量池、运行时常量池、静态变量、基本数据类型包装类常量池 位于PermSpace
      不会触发FGC清理
      大小启动的时候指定,不能变
    2. Perm Space 永久代 (=JDK1.7版本,开始去永久代)
      字符串常量池、静态变量 转移到堆上,其他还在永久代
      不会触发FGC清理
    3. Meta Space 元空间 (>=JDK1.8版本)
      字符串常量池 位于堆
      会触发FGC清理
      不设定的话,最大就是物理内存

b、常量池

c、JDK1.6、JDK1.7、JDK1.8 内存模型演变

  • JDK1.6、JDK1.7、JDK1.8 内存模型演变:https://www.cnblogs.com/xiaofuge/p/14244755.html
    在这里插入图片描述
  • JDK 1.6:有永久代,静态变量存放在永久代上。
  • JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。开始去永久代。逐渐的减少永久代的使用。
  • JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。

6、Run-Time Constant Pool(属于MA)

  • JVM 是这样说的:
    1. A run-time constant pool is a per-class or per-interface run-time
      representation of the constant_pool table in a class file
  • 运行时常量池 位于方法区中

7、Heap 堆(线程共有)

  • JVM 是这样说的:
    1. The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads.
    2. The heap is the run-time data area from which memory for
      all class instances and arrays is allocated.

GC时好好聊:JVM知识体系学习六第三章: https://blog.csdn.net/qq_40036754/article/details/128651806

8、总体图

从下图看出:

  • PC、JVM stacks、native method stack 是线程独有的
  • heap、method area 是线程共享的
    在这里插入图片描述

二、java stack 中的 栈针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. (框架用于存储数据和部分结果,以及执行动态链接、方法的返回值和分派异常)
  • 每一个线程是一个栈针,每个方法对应一个栈帧,比如main()方法应该第一个进入栈中,调用的方法依次入栈(作为栈针)。
    在这里插入图片描述

1、栈针的四部分

  • 每一个栈针包括 以下四部分:
    在这里插入图片描述
    1. Local Variable Table:本地变量表
    2. Operand Stack:操作数栈
      • 对于long的处理(store and load),多数虚拟机的实现都是原子的
      • jls文档里 17.7,没必要加volatile
    3. Dynamic Linking:动态链接
      • 文档详解:https://blog.csdn.net/qq_41813060/article/details/88379473
      • jvms文档的 2.6.3
      • 就是class文件里的常量池 constant pool(常量池)里的符号链接,看有没有解析,如果没有解析就动态解析;如果解析了则直接使用,所以Dynamic Linking 就是这个东西。
      • 比如:有a()方法和b()方法,a()方法中调用了b()方法,那么执行a()方法中的b()方法时,则需要去常量池中去找b()方法,这个找的过程就是动态链接。 Dynamic Linking
    4. return address:返回地址。
      • a() 调用了 b(),方法a调用了方法b, b方法的返回值放在什么地方,这就是 return address。

2、Local Variable Table 局部变量表

a、问题引入的代码

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);
    }
}

b、查看字节码中的本地变量表

在这里插入图片描述
如上图所示:

  1. LineNumberTable:行号
  2. LocalVariableTable:本地变量表,包括两个,一个是main()方法的形参 args,一个是 变量 i
  3. 右边绿色的 cp info #15:cp,是 constant pool 常量池(在MA中);所以是常量池第15块地址存储的,可以点进去查看。
    在这里插入图片描述

3、Operand Stack(解释i++和++i的问题)

a、解释指令集:i++

先看下图的指令
在这里插入图片描述
在这里插入图片描述

  1. 指令一 bipush 8:(byte int push)就是 讲byte 转成 int 型 push 到栈中。即 将8 压入栈中。

    • 点击指令,可以直接进入到指令的解释网页
    • 下图
      在这里插入图片描述
  2. 指令二istore_1:通过下面指令解释可以看出,将操作栈中的数出栈,赋值到局部变量表(二.2)中 下标为2的变量中。即将8出栈赋值到i上。

    • 指令解释如下
      在这里插入图片描述
      在这里插入图片描述
  3. 到了这儿,i=8 这条语句就执行完了,就是通过 1和 2这两条指令完成的。
    在这里插入图片描述

  4. 指令三 iload_1:取出局部变量表下标为1的值,然后入操作栈;即 i=1取出,入操作栈。

    • 解释如下
      在这里插入图片描述
  5. 指令四 iinc 1 by 1:局部变量表下标为1 的值 自加1。此时 局部变量表下标为1 的 i 值 变成了 9。

    • 解释
      在这里插入图片描述
  6. 指令五 istore_1:同指令2,将操作栈中的值出栈,然后赋值到局部变量表下标为1的变量 i 上。所以将8赋值到局部变量表 i=9上,则为8。

  7. 此时,到这里,完美的解释了 i++ 的操作。i++ 的指令一共有三步,也就是第三、第四、第五个指令。

b、解释指令集:++i

执行的字节码如下图:唯一的不同,第三行和第四行交换了。 ++i 的指令也是第三、第四、第五个指令,共三个。

在这里插入图片描述
解释如下:

  1. bipush 8:将8压栈
  2. istore_1:8出栈,赋值到 局部变量表 下标为1 的 i 上,完成 i=8
  3. iinc 1 by 1:局部变量表下标为1 的i变量的值,自加1,完成 ++i,i = 9。
  4. iload_1:局部变量表下标为1 的i 变量 的值 9 取出,入操作栈。
  5. istore_1:操作栈 出栈,赋值 到 局部变量表 下标为1 的 i上,为9。

三、栈的执行流程

1、案例 1_sipush 指令

a、代码

在这里插入图片描述

b、字节码指令集

在这里插入图片描述

c、指令解说

和上面的案例相比,这里第一个指令是 sipush(原来是 bipush), 原因是: 200>127,所以不再是 byte 类型,而是 short 类型 转成 int 类型。

在这里插入图片描述

2、案例 2_局部变量表中的this

a、代码

在这里插入图片描述

b、字节码指令集

在这里插入图片描述

c、指令解说

  1. 第一个指令是 sipush 是正常的,因为300>127。
  2. 但是为什么第二个指令的下标变成2了呢,原因是: 只要是 非 static 方法都有一个 this 变量在局部变量表中,然后形参 k 是第二个,则 i 是第三个,所以这里的指令下标是 2
    在这里插入图片描述

前边的main 方法没有 this,是因为 main 方法有static。
在这里插入图片描述

  • 要点:非static的局部变量表的第一个是this。

3、案例 3_加法指令_iadd

a、代码

在这里插入图片描述

b、字节码指令集

在这里插入图片描述

c、指令解说

  • 先说下局部变量表,有 this,a,b,c 共四个。
  • 指令描述如下:
    1. 指令 iload_1 iload_2 分别按顺序入操作栈 ;
    2. 指令iadd 则是将 int 类型的两个数出栈 相加 在入栈,此时栈中只有7;
    3. 指令 istore_3 则将操作栈 栈顶元素 7 出栈 赋值到局部变量表下标为 3c
      在这里插入图片描述

4、案例 4_创建对象指令

a、代码

在这里插入图片描述

b、字节码 指令集

m1()方法
在这里插入图片描述
main()方法
在这里插入图片描述

c、指令解说

  • 从下图中也可以看出,线程栈中有两个栈针,一个 main() 方法,一个 m1() 方法。
  • 局部变量表:
    • main() 中:argsh
    • m1() 中:thisi
  • 执行顺序如下:
    1. new 指令先 创建一个对象(load、linking、initializing等初始化),
    2. dup 指令,复制操作数栈栈顶的值,再入操作数栈。此时操作数栈中有两个一样的 Hello_02 对象,都指向对象的地址。
    3. invokespecial 指令,是执行 默认的构造函数 init。到了这儿,对象构造才算完毕,然后弹出栈就没啦,此时操作数栈中只有一个对象了。(invoke 指令有五种,后面在讲)
    4. astore_1 指令(注意这里是 a 开头),是将操作数栈中的对象出栈 赋值h 上。
    5. aload_1 指令,从局部变量表中在取出 入操作数栈。
    6. invokevirtual 指令,执行 m1() 栈针,然后 出栈
      • 然后执行 sipush 200istore_1return 指令。然后返回地址
    7. 然后再执行,return 执行完即可。
  • 注意的点:
    • dup 指令:就是将操作栈中最上面的值复制一份,再压入操作栈中。
    • 半初始化状态,就是 newinvokevirtual 中间有个 dup 指令。

在这里插入图片描述
(上面main方法的栈针中的局部变量表中的this 错了,应该是args)

  • 可以想到 栈溢出(over stack)
  • 问题:DCL(双重检查,在JVM知识体系学习三 中有详细说明) 为什么要使用 volatile
    • 因为指令之间可能会指令重排

5、案例 5

a、案例5_1

i、代码

在这里插入图片描述

ii、字节码指令集
  1. m1方法:
    在这里插入图片描述
  2. main方法
    在这里插入图片描述
iii、指令解说
  • m1() 方法

    1. bipush:看文档;就是将 byte 转成 int 类型的值,压入操作数栈
    2. ireturn:返回int 值,并出栈
  • main() 方法

    1. 其余指令在上面案例中都讲过,我这里就稍微描述下
    2. new 需要仔细看文档 。开辟对象地址,并不是完整的创建对象,然后将对象引用入操作数栈。
    3. dup 复制对象引用入操作数栈。
    4. invokespecial:调用构造函数,完成对象初始化,并出栈,此时占中还有一个对象引用。
    5. astore_1:出栈,存储到局部变量表中下标为 1h 中。
    6. aload_1:取出下标为 1 的局部变量边的位置的值,入操作数栈。
    7. invokevirtual:调用 m1 方法
      • m1 的字节码指令集。
    8. pop函数,因为是返回,直接出栈即可。
  • 注意是main 方法 指令中倒数第二个指令,与下个案例做对比。

b、案例5_2

i、代码

在这里插入图片描述

ii、字节码指令集

m1方法(和上个案例一样)
在这里插入图片描述
main方法
在这里插入图片描述

iii、指令解说
  • 这里的指令与上一个案例只有一个地方不一样,就是main方法倒数第二个指令
  • 这里是将操作数栈中的值取出,然后赋值给局部变量表中下标为 2 的变量 i 中。

6、案例 6 (递归指令集解说)

a、代码

在这里插入图片描述

b、字节码指令集:

m方法:
在这里插入图片描述
main方法
在这里插入图片描述

c、指令解说

  • 有些难度,递归指令的描述。

四、常用指令

目前还没面试官问过,增长见识和知识点就OK。

1、常用命令

  1. store
  2. load
  3. pop
  4. mul
  5. sub
  6. invoke

2、invoke-共5个指令

  1. InvokeStatic:调用静态方法
    在这里插入图片描述

  2. InvokeVirtual:自带多态,执行类的成员方法。
    在这里插入图片描述

  3. InvokeInterface:调用接口方法。
    在这里插入图片描述

  4. InovkeSpecial

    • 可以直接定位,不需要多态的方法
    • private 方法 , 构造方法
      在这里插入图片描述
  5. InvokeDynamic

    • 1.7版本添加的指令,这时 java 开始支持动态语言。
    • JVM最难的指令
    • lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;

public class T05_InvokeDynamic {
    public static void main(String[] args) {

        I i = C::n;
        I i2 = C::n;
        I i3 = C::n;
        I i4 = () -> {
            C.n();
        };
        System.out.println(i.getClass());
        System.out.println(i2.getClass());
        System.out.println(i3.getClass());

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

    @FunctionalInterface
    public interface I {
        void m();
    }

    public static class C {
        static void n() {
            System.out.println("hello");
        }
    }
}

在这里插入图片描述

3、JDK1.8之前的一个BUG

  • for(;;) {I j = C::n;}
    • JDK 在小于1.8之前叫 Perm Space(永久代),这时上面的代码会产生大量的class类,会存放在 MethodArea(方法区),这时在1.8之前 MethodArea 会产生一个巨大的bugFGC(Full GC)不回收、不清理
    • 所以在JDK1.8之前,会产生OOM,内存溢出,但是在1.8之后,如果清除不掉,也会产生OOM。
相关文章
|
13天前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
158 1
|
20天前
|
数据采集 搜索推荐 Java
Java 大视界 -- Java 大数据在智能教育虚拟学习环境构建与用户体验优化中的应用(221)
本文探讨 Java 大数据在智能教育虚拟学习环境中的应用,涵盖多源数据采集、个性化推荐、实时互动优化等核心技术,结合实际案例分析其在提升学习体验与教学质量中的成效,并展望未来发展方向与技术挑战。
|
2月前
|
并行计算 Java API
Java 基础篇完整学习攻略
本教程涵盖Java基础到高级内容,包括模块化系统、Stream API、多线程编程、JVM机制、集合框架及新特性如Records和模式匹配等,适合零基础学员系统学习Java编程。
66 0
|
30天前
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
112 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
|
2月前
|
前端开发 Java API
新手 Java 学习资料结合最新技术的精选推荐及高效学习资源参考
本文为新手推荐了涵盖Java基础到最新技术的学习资料,包括官方文档、在线课程、书籍、学习网站及实践平台,帮助系统掌握Java编程,并通过Spring Boot实战提升开发能力。
94 1
|
2月前
|
NoSQL Java 数据库
Java 全栈学习超全面知识图谱构建完整 Java 知识体系
本文全面讲解Java核心技术体系,涵盖基础语法、面向对象、集合框架、主流框架(Spring、Spring Boot、MyBatis)及三大实战项目(微服务电商、响应式博客、企业后台系统),助你系统掌握Java全栈开发技能。
122 1
|
2月前
|
存储 算法 安全
JAVA 八股文全网最详尽整理包含各类核心考点助你高效学习 jAVA 八股文赶紧收藏
本文整理了Java核心技术内容,涵盖Java基础、多线程、JVM、集合框架等八股文知识点,包含面向对象特性、线程创建与通信、运行时数据区、垃圾回收算法及常用集合类对比,附有代码示例与学习资料下载链接,适合Java开发者系统学习与面试准备。
570 0
|
27天前
|
存储 搜索推荐 安全
Java 大视界 --Java 大数据在智能教育学习效果评估与教学质量改进中的应用(209)
本文探讨了 Java 大数据在智能教育中的创新应用,涵盖学习效果评估、教学质量改进及个性化教学方案定制等内容,结合实战案例与代码解析,展现技术如何赋能教育智能化转型。
|
3月前
|
Java 数据库 数据安全/隐私保护
银行流水生成器在线制作,银行转账p图在线生成,java实现最牛的生成器【仅供学习用途】
本资料探讨银行系统核心技术,涵盖交易记录生成、电子回单加密验真及基于Java的财务管理系统开发。主要内容包括:交易记录实体类设计(不可变性与数字签名)
|
2月前
|
存储 运维 Kubernetes
Java启动参数JVM_OPTS="-Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError"
本文介绍了Java虚拟机(JVM)常用启动参数配置,包括设置初始堆内存(-Xms512m)、最大堆内存(-Xmx1024m)及内存溢出时生成堆转储文件(-XX:+HeapDumpOnOutOfMemoryError),用于性能调优与故障排查。
253 0