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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 这篇文章详细介绍了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。
相关文章
|
1天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
12 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1天前
|
Java 应用服务中间件 程序员
JVM知识体系学习八:OOM的案例(承接上篇博文,可以作为面试中的案例)
这篇文章通过多个案例深入探讨了Java虚拟机(JVM)中的内存溢出问题,涵盖了堆内存、方法区、直接内存和栈内存溢出的原因、诊断方法和解决方案,并讨论了不同JDK版本垃圾回收器的变化。
11 4
|
1天前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
13 3
|
1天前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
10 3
|
1天前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
11 3
|
1天前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1天前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
14 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
2天前
|
数据采集 XML 前端开发
Jsoup在Java中:解析京东网站数据
Jsoup在Java中:解析京东网站数据
|
1天前
|
Java 调度 UED
深入理解Java中的多线程与并发机制
本文将详细探讨Java中多线程的概念、实现方式及并发机制,包括线程的生命周期、同步与锁机制以及高级并发工具。通过实例代码演示,帮助读者理解如何在Java中有效地处理多线程和并发问题,提高程序的性能和响应能力。
|
7天前
|
监控 Java Linux
Java 性能调优:调整 GC 线程以获得最佳结果
Java 性能调优:调整 GC 线程以获得最佳结果
39 11