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异常。

目录
相关文章
|
3月前
|
Java
程序计数器和虚拟机栈
这篇文章介绍了Java虚拟机(JVM)的内存结构,特别解释了程序计数器(Program Counter Register)的作用,即用来记录下一条JVM指令的执行地址和行号。
程序计数器和虚拟机栈
|
3月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
140 4
|
3月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
1月前
|
Java
jvm复习,深入理解java虚拟机一:运行时数据区域
这篇文章深入探讨了Java虚拟机的运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、元空间和运行时常量池,并讨论了它们的作用、特点以及与垃圾回收的关系。
62 19
jvm复习,深入理解java虚拟机一:运行时数据区域
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
60 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
23天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
45 10
|
22天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
41 1
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
41 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
3月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
27 3