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

目录
相关文章
|
5月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
472 55
|
10月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
235 27
|
6月前
|
存储 NoSQL Redis
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 +  无锁架构 +  EDA架构  + 异步日志 + 集群架构
|
7月前
|
SQL 存储 缓存
【赵渝强老师】达梦数据库的内存结构
本文介绍了达梦数据库管理系统的内存结构,包括内存池、缓冲区、排序区和哈希区。内存池分为共享内存池和运行时内存池,能够提高内存申请与释放效率,并便于监控内存使用情况。缓冲区涵盖数据缓冲区、日志缓冲区、字典缓冲区和SQL缓冲区,用于优化数据读写和查询性能。排序区和哈希区分别提供排序和哈希连接所需的内存空间,通过合理配置参数可提升系统效率。文内附有具体配置示例及视频讲解,帮助用户深入理解达梦数据库的内存管理机制。
174 0
|
10月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
10月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
3月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
838 0
|
3月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
239 0
|
3月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
230 0