JVM学习日志(四) JVM 内存结构划分

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 简述 JVM 内存结构划分(程序计数器,虚拟机栈)

JVM 内存结构划分

  • 从java代码精力编译生成对应字节码文件,再经由类加载器加载,经历加载,验证,解析,初始化,阶段,整个过程我们称之为类加载阶段,也就是我们JVM 第一部分重要的开端

  • JVM内存模型图

    image-20230404152643611.png

  • JVM各个区域说明

    • 程序计数器:用于记录将要执行的JVM指令地址
    • 虚拟机栈:每个线程运行时所需要的内存,称为虚拟机栈;每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    • 本地方法栈:保存navtive方法进入区域的地址
    • 堆:通过new关键字创建对象都会使用堆内存
    • 方法区:存储类的信息以及运算时常量池

程序计数器

  • 程序计数器(Program Counter Register) 同时也叫PC寄存器

  • 作用:是记住下一条Jvm指令的执行地址

  • 特点:

    • 是线程私有的
    • 不会存在内存溢出
  • JVM是不识别我们写的代码的,我们的java代码会被编译为.class字节码文件,而字节码文件中的代码才是jvm能试别和执行的,这些代码我们也叫做【字节码指令】,他对应了一条一条的机器指令,JVM通过将这些指令在解释翻译成为机器指令,来操作我们的计算器进行执行,例如以下的代码

    public class Demo1{
         
        public static void main(String[] args){
         
            int num1 = 1;
            System.out.println(num1);
            int num2 = 2;
            System.out.pringln(num2);
        }
    }
    

    上述代码的字节码指令为

    0 iconst_1
    1 istore_1
    2 getstatic #2 <java/lang/System.out>
    5 iload_1
    6 invokevirtual #3 <java/io/PrintStream.println>
    9 iconst_2
    10 istore_2
    11 getstatic #2 <java/lang/System.out>
    14 iload_2
    15 invokevirtual #3 <java/io/PrintStream.println>
    18 return
    

    注意:这些字节码指令就是由字节码执行引擎来执行的

  • JAVA程序编译后的.class文件生成的字节码指令是由JVM基于线程执行的,那么在执行的时候会涉及到上下文切换的问题,这个时候就涉及到一个专门负责记录每个线程当前执行字节码指令语句坐标的空间,叫做程序计数器

  • image-20230409103206234.png

  • 由于JAVA多线程的的执行,我们的程序可能会开启多个线程并发执行不同的代码,所以会对应着有多个线程并发执行不同的代码指令,而每个线程底层是通过CPU分配给她时间片的方式,以此轮流来执行的,可能A线程执行一段时间后就切换成B线程执行了,B线程执行时间结束后,再切换回A线程执行,此时A线程需要知道自己上一次执行到什么地方了,才能再上次的位置继续执行下去

  • 所以程序计数器扮演了一个这样的角色:记录每个线程执行字节码指令位置,并且程序计数器每个线程都是私有的,专门为各自线程记录每次执行字节码指令的位置,方便下次线程切换回来时还能找到上次执行的位置继续执行

image-20230409103704501.png

虚拟机栈

image-20230409104448111.png

概念

  • 定义

    • java Virtual Machine Stacks (java虚拟机栈)
    • 每个线程运行时所需要的内存,成为虚拟机栈
    • 每个栈有多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 解释

    1. 每个线程运行时所需要的内存,称为虚拟机栈 --> 每个线程都有自己的JAVA虚拟机栈

    java虚拟机一方法作为基本的执行单元,"栈帧"(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素

    • java代码的执行一定是有线程来执行某个方法中的代码,哪怕是我们的main()方法也是有一个主线程来执行的,再main线程执行main()方法的代码指令的时候,就会通过main线程队医你给的程序计数器来记录执行自己执行的指令位置
    • main()方法本质上也是一个方法,再main()中也可以带哦用其他的方法,而每个方法中也有自己的局部变量数据,因此JVM提供了一块内存区域用来保存每个方法内部变量等数据,这个区域就是java虚拟机栈
  1. 每个栈有多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

    每个战阵主要存放:局部变量表,操作数栈,动态连接和方法返回地址等信息,每一个方法从调用开始至执行结束的过程,都对应着一个栈帧再虚拟机栈里面从入栈到出战的过程

  • 当我们在一个线程中调用了一个方法,就会对该方法创建一个对应的栈帧,比如说我们下面的代码

    public class Demo1{
         
        public static void main(String[] args){
         
            int num1 = 1;
            System.out.pringln(num1);
            int num2 = 2;
            System.out.pringln(num2);
        }
    }
    
  • 当我们运行上面的代码的时候,再JVM虚拟机栈内存中就会先创建一个main方法的栈帧,同时记录保存对应的局部变量,此时的内存模型图为

    image-20230412110556920.png

  • 而此时如果我们再main方法中调用另一个方法的话,如

    ```java
    public class Demo1{

    public static void main(String[] args){
        int num1 = 1;
        System.out.println(num1);
        int num2 = 2;
        System.out.println(num2);
        method1();
    }
    
  public static void method1(){
      int num3 = 20;
      System.out.println(num3);
  }

}
```

  • 此时的虚拟机栈内存模型

    image-20230412110911741.png

  • 并且当Method1方法执行完毕之后,弹出该队列,最后弹出来main()方法栈,代表整个main方法代码执行完毕,这也对应了栈数据的特点:先进后出
  • 每个线程都只能有一个活动栈帧,对应着当前正在执行的那个方法

    • 一个线程中的方法调用链可能会很长,以java程序的角度来看,同一时刻,同一条线程里面,在调用堆栈的所有方法都同时处于执行状态,而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为"当前栈帧"(Current Stack Frame),与这个栈帧所关联的方法被成为“当前方法”(Current Method),执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作、

    • 注意 虚拟机栈只是一份管理数据的内存区域,只负责存储,真正的执行还是由JVM字节码执行引擎来执行

    • 流程图小结

      image-20230412140139116.png

栈内存面试案例

  1. 垃圾回收是否涉及栈内存?

    栈帧每次执行结束后会自动弹栈,所以不会涉及到垃圾的产生,也就不会对栈内存进行垃圾回收

  2. 栈内存分配越大越好吗?

    并不是,假设分配好的物理内存是100MB,每个线程大小是1MB,那么可以分配100个线程,但是如果提升了线程栈的大小,那可以分配的对应的线程数就变少了

  • 官网给出的每个虚拟机栈默认大小分配:

    image-20230412140703831.png

  • 可以看到Linux系统上默认的就是1MB,当然我们可以通过-Xss进行大小的更改

  • 对于不同版本的Java虚拟机喝不同的操作系统,站容量最小值可能会有所限制,这主要取决于操作系统内存分页的大小,譬如上述方法中的参数-Xss128k可以张常使用32位Windows系统下的JDK6,大那是如果用于64位Windows系统下的JDK 11,则会提示栈容量最小不能低于180k,而在Linux下,这个值可能是228K,如果低于这个最小限制,HotSpot虚拟机启动时就会提示:The Java thread Stack size specified is too small, Specify at least 228K

  1. 方法内部的局部变量是否线程安全

    如果方法内的局部变量没有逃离方法的作用访问,他是线程安全的

    如果局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全

  2. 栈内存溢出

    • 什么原因导致占内存溢出
      • 栈帧过多导致栈内存溢出,将抛出StackOverflowError异常

    image-20230412143926880.png

  • 栈帧过大也会造成栈内存溢出
    • 我们这次可以尝试将每一个栈帧的局部变量多占用一点空间,这样每个栈帧的大小就会变大,我们还是设定每个线程栈空间为128K
  • 小结:
    • 无论是栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常

本地方法栈

image-20230412161740312.png

  • 本地方法栈(Native Method Stacks)与虚拟机所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到本地(Native)方法服务

  • 说明:

    • 对本地方法栈中方法使用的语言,使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一
    • 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败是分别抛出StackOverflowError和OutOfMemoryError异常
  • 为什么要使用本地方法?

    • java使用起来很方便,然而Java代码有一定的局限性,有时候不能和系统底层交互,或是追求程序的效率时,这时候就需要更加底层的语言和更快的运行效率

    • 方便与java之外的环境交互,如与操作系统或者某些硬件交换信息,本地方法为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节

    • 虚拟机本身就是由C++写的,一些操作系统特性JVM没有封装提供出来,那我们就可以自行的使用C语言来实现它,并通过本地方法来调用

    • 追求更快的运行效率

    • 这幅图展示了JAVA虚拟机内部线程运行的全景图,一个线程可能在整个生命周期中都执行java方法,操作他的java栈,或者他可能毫无障碍的在java栈和本地方法栈之间跳转

      image-20230412162857687.png

  • 该线程首先调用了两个java方法,而第二个java方法又调用了一个被你的方法,这样导致虚拟机使用了一个本地方法栈,假设这是一个C语言栈,期间有两个C函数,第一个C函数被第二个java方法当作本地方法调用,而这个C函数又调用了第二个C函数,之后第二个C函数又通过本地方法接口毁掉了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个java方法(它成为图中的当前方法)

image-20230412165121911.png

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
29 3
|
3月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
140 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
3月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
53 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
3月前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
80 10
|
3月前
|
Java 应用服务中间件 程序员
JVM知识体系学习八:OOM的案例(承接上篇博文,可以作为面试中的案例)
这篇文章通过多个案例深入探讨了Java虚拟机(JVM)中的内存溢出问题,涵盖了堆内存、方法区、直接内存和栈内存溢出的原因、诊断方法和解决方案,并讨论了不同JDK版本垃圾回收器的变化。
51 4
|
3月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
130 3
|
3月前
|
Python
log日志学习
【10月更文挑战第9天】 python处理log打印模块log的使用和介绍
59 0
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
505 1
|
3月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
51 4
|
26天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型