08-JVM的内存结构之程序计数器和虚拟机栈

简介: 接下来我们继续深入第二个环节,也就是JVM的内存结构,很多人想到BAT等大厂去面试,但是现在互联网大厂面试几乎都会考核JVM相关知识的积累,所在在了解完了JVM的类加载机制之后,我们有必要一起来学习下JVM的内存区域划分。

其实我们通过类的加载过程也能知道,在准备阶段我们的类以及静态变量都会进行空间的分配,JVM在运行我们的代码时,是必须要使用多块内存空间的,不同空间里面存放不同的数据,然后配合我们的代码流程,完整系统的运行起来。

程序计数器

首先我们来看第一个内存区域:程序计数器

Program Counter Register 程序计数器(PC寄存器)

  • 作用,是记住下一条jvm指令的执行地址
  • 特点
    • 是线程私有的
    • 不会存在内存溢出

首先我们来看一段非常简单的代码:

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

这个代码大家都能看懂,但是JVM能看懂吗?答案是:NO!

JVM是不识别我们写的代码的,我们的java代码会被编译为.class字节码文件,而字节码文件中的代码才是JVM能识别和执行的,这些代码我们也叫【字节码指令】,它对应了一条一条的机器指令,JVM通过将这些指令再解释翻译为机器指令,来操作我们的计算器进行执行。

上述的代码对应的字节码指令如下:

 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

具体的指令含义后续再做讲解,我们需要知道的是【程序计数器】就是记录下一条JVM所要执行的指令地址

通过之前的加载图进行表示:

2虚拟机栈

学习路线图 (1)_20210721223011.png

定义

Java Virtual Machine Stacks (Java 虚拟机栈)

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

解释

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

Java代码的执行一定是由线程来执行某个方法中的代码,哪怕就是我们的main()方法也是有一个主线程来执行的,在main线程执行main()方法的代码指令的时候,就会通过main线程对应的程序计数器来记录自己执行的指令位置。

main()方法本质上是一个方法,在main()中也可以调用其他的方法,而每个方法中也有自己的局部变量数据,因此JVM提供了一块内存区域用来保存每个方法内的局部变量等数据,这个区域就是Java虚拟机栈

2.每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

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

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

这时在虚拟机栈内存中就会先创建对应main方法的栈帧,同时记录保存对应的局部变量:

虚拟机栈_20210721223117.png

而如果我们在main()方法中调用一个其他的方法:

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("哈哈哈哈");
    }
}

对应的虚拟机栈:
虚拟机栈 (1)_20210721223107.jpg

并且当method1方法执行完毕后会弹出该栈队列,最后弹出main()方法栈帧,代表整个main方法代码执行完毕。这也对应了栈的特点:先进后出

流程图小结
一次编写,到处运行 (1)_20210721223149.png


栈内存相关面试案例剖析

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

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

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

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

    我们先来看官网给出的每个栈帧默认的大小分配:

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

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

    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

    参考以下示例代码:

      //方法内局部变量:线程安全
        public static void method1(){
         
         
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb);
        }
        //方法内局部变量引用对象:线程不安全
        public static void method2(StringBuilder sb){
         
         
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb);
        }
        //方法内局部变量引用对象提供暴露:线程不安全
        public static StringBuilder method3(){
         
         
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            return sb;
        }
    
  2. 栈内存溢出

    什么原因导致栈内存溢出(Stack Overflow)

    1)栈帧过多导致内存溢出, 将抛出StackOverflowError异常。


常见的情况就是递归调用,不断产生新的栈帧,前面的栈帧不释放

我们可以通过以下代码来测试和实验:

   /**
    * @Description:   VM Args: -Xss128k
    对于不同版本的Java虚拟机和不同的操作系统, 栈容量最小值可能会有所限制, 这主要取决于操作系统内存分页大小。 譬如上述方法中的参数-Xss128k可以正常用于32位Windows系统下的JDK 6, 但是如果用于64位Windows系统下的JDK 11, 则会提示栈容量最小不能低于180K, 而在Linux下这个值则可能是228K, 如果低于这个最小限制, HotSpot虚拟器启动时会提示:The Java thread stack size specified is too small. Specify at least 228k
    */
   public class JavaVMStackSOF {
   
   
       private int stackLength = 1;
       public void stackLeak() {
   
   
           stackLength++;
           stackLeak();
       }
       public static void main(String[] args) throws Throwable {
   
   
           JavaVMStackSOF oom = new JavaVMStackSOF();
           try {
   
   
               oom.stackLeak();
           } catch (Throwable e) {
   
   
               System.out.println("stack length:" + oom.stackLength);
               throw e;
           }
       }
   }

打印结果:

2)栈帧过大导致内存溢出, 将抛出StackOverflowError异常。

我们这次可以尝试将每一个栈帧的局部变量给多占用一点空间,这样每个栈帧的大小就会变大,我们还是设定每个线程栈空间为128K,看看以下代码运行后,多少次就会撑满内存:

   /**
    * @Description: VM Args: -Xss128k
    */
   public class JavaVMStackSOF2 {
   
   
       private static int stackLength = 0;
       public static void test() {
   
   
           long unused1, unused2, unused3, unused4, unused5,
                   unused6, unused7, unused8, unused9, unused10,
                   unused11, unused12, unused13, unused14, unused15,
                   unused16, unused17, unused18, unused19, unused20,
                   unused21, unused22, unused23, unused24, unused25,
                   unused26, unused27, unused28, unused29, unused30,
                   unused31, unused32, unused33, unused34, unused35,
                   unused36, unused37, unused38, unused39, unused40,
                   unused41, unused42, unused43, unused44, unused45,
                   unused46, unused47, unused48, unused49, unused50,
                   unused51, unused52, unused53, unused54, unused55,
                   unused56, unused57, unused58, unused59, unused60,
                   unused61, unused62, unused63, unused64, unused65,
                   unused66, unused67, unused68, unused69, unused70,
                   unused71, unused72, unused73, unused74, unused75,
                   unused76, unused77, unused78, unused79, unused80,
                   unused81, unused82, unused83, unused84, unused85,
                   unused86, unused87, unused88, unused89, unused90,
                   unused91, unused92, unused93, unused94, unused95,
                   unused96, unused97, unused98, unused99, unused100;
           stackLength++;
           test();
       }

       public static void main(String[] args) throws Throwable {
   
   
           try {
   
   
               test();
           }catch (Error e){
   
   
               System.out.println("stack length:" + stackLength);
               throw e;
           }
       }
   }

打印结果:

我们发现仅51次就撑爆了!

小结:

无论是由于栈帧太大还是虚拟机栈容量太小, 当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。

目录
相关文章
|
10月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
869 55
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
496 4
|
11月前
|
存储 NoSQL Redis
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 +  无锁架构 +  EDA架构  + 异步日志 + 集群架构
|
12月前
|
SQL 存储 缓存
【赵渝强老师】达梦数据库的内存结构
本文介绍了达梦数据库管理系统的内存结构,包括内存池、缓冲区、排序区和哈希区。内存池分为共享内存池和运行时内存池,能够提高内存申请与释放效率,并便于监控内存使用情况。缓冲区涵盖数据缓冲区、日志缓冲区、字典缓冲区和SQL缓冲区,用于优化数据读写和查询性能。排序区和哈希区分别提供排序和哈希连接所需的内存空间,通过合理配置参数可提升系统效率。文内附有具体配置示例及视频讲解,帮助用户深入理解达梦数据库的内存管理机制。
401 0
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
1032 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
185 3
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
444 10
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
362 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制