Skip to content

一、栈

  • 含义:主要支持虚拟机 进行方法调用方法执行 的数据结构。
    • 作用:解决的是 运行问题。(即程序如何执行,或者如何处理数据)
    • 栈数据结构原则:先进后出,后进先出
    • 栈帧的内部结构主要包含:
      • 局部变量表
      • 操作数栈
      • 动态链接
      • 方法出口
  • 例如: StackSpace.java 示例代码, 运行如下代码。
java
package com.calvin.jvm.structure;

/**
 * 栈空间
 *
 *
 * @author Calvin
 * @date 2023/4/25
 * @since v1.0.0
 */
public class StackSpace {

    /**
     * Main 主方法 (栈帧)
     *
     * @param args 参数
     */
    public static void main(String[] args) {
       /*
        * 1 -> 为操作数栈
        *   - 底层汇编指令 (iconst_1) JVM采用 bipush 指令将常量压入栈中
        *
        * i -> 为局部变量
        *   - 存放在"局部变量表"
        *   - 单独开辟一个"变量槽"
        *   - 底层汇编指令 (iconst_1) JVM采用 bipush 指令将常量压入栈中
        */
       int i = 1;
       int j = i + 1;

    }
}

/**
 * 通过 javap -c -v StackSpace.class 命令,查询Class文件汇编后指令
 *
 * public class com.calvin.jvm.structure.StackSpace
 *   minor version: 0
 *   major version: 52
 *   flags: ACC_PUBLIC, ACC_SUPER
 * Constant pool:
 *    #1 = Methodref          #3.#19         // java/lang/Object."<init>":()V
 *    #2 = Class              #20            // com/calvin/jvm/structure/StackSpace
 *    #3 = Class              #21            // java/lang/Object
 *    #4 = Utf8               <init>
 *    #5 = Utf8               ()V
 *    #6 = Utf8               Code
 *    #7 = Utf8               LineNumberTable
 *    #8 = Utf8               LocalVariableTable
 *    #9 = Utf8               this
 *   #10 = Utf8               Lcom/calvin/jvm/structure/StackSpace;
 *   #11 = Utf8               main
 *   #12 = Utf8               ([Ljava/lang/String;)V
 *   #13 = Utf8               args
 *   #14 = Utf8               [Ljava/lang/String;
 *   #15 = Utf8               i
 *   #16 = Utf8               I
 *   #17 = Utf8               SourceFile
 *   #18 = Utf8               StackSpace.java
 *   #19 = NameAndType        #4:#5          // "<init>":()V
 *   #20 = Utf8               com/calvin/jvm/structure/StackSpace
 *   #21 = Utf8               java/lang/Object
 * {
 *   public com.calvin.jvm.structure.StackSpace();
 *     descriptor: ()V
 *     flags: ACC_PUBLIC
 *     Code:
 *       stack=1, locals=1, args_size=1
 *          0: aload_0
 *          1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 *          4: return
 *       LineNumberTable:
 *         line 11: 0
 *       LocalVariableTable:
 *         Start  Length  Slot  Name   Signature
 *             0       5     0  this   Lcom/calvin/jvm/structure/StackSpace;
 *
 *   public static void main(java.lang.String[]);
 *     descriptor: ([Ljava/lang/String;)V
 *     flags: ACC_PUBLIC, ACC_STATIC
 *     Code:
 *       stack=1, locals=2, args_size=1
 *          0: iconst_1
 *          1: istore_1
 *          2: return
 *       LineNumberTable:
 *         line 26: 0
 *         line 30: 2
 *       LocalVariableTable:
 *         Start  Length  Slot  Name   Signature
 *             0       3     0  args   [Ljava/lang/String;
 *             2       1     1     i   I
 * }
 */

二、【栈帧】结构

局部变量表

  • 含义: 一组 变量值存储空间,用于 存放方法参数方法内定义的局部变量

  • 一个 局部变量 可以保存一个类型为

    • 基础数据类型 boolean、byte、char、short、int、long、double、float
    • 引用数据类型 reference
    • 返回地址数据类型 returnAddress
  • 例如:

shell
 LocalVariableTable:
 *         Start  Length  Slot  Name   Signature
 *             0       3     0  args   [Ljava/lang/String;
 *             2       1     1     i   I

操作数栈

  • 含义:方法执行的过程中,会有 各种字节码指令操作数栈 中写入和提取内容,也就是出栈入栈操作(与 Java 栈中栈帧操作类似)。
  • 例如:
shell
 Code:
 *       stack=1, locals=2, args_size=1
 *          0: iconst_1 # 将int类型的0值压入操作数栈
 *          1: istore_1 # 弹出操作数栈顶的值赋给局部变量表下标为1的变量
 *          2: return   # 最后返回局部变量表中槽1的位置

动态链接

  • 含义:每个栈帧都包含 一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了 支持方法调用过程中的动态连接(Dynamic Linking)

  • 静态解析:在类加载阶段中的解析阶段会将符号引用转为直接引用。

  • 动态连接:另外的一部分将在每一次运行时期转化为直接引用。

  • 例如:

shell
1: invokespecial #1                  // Method java/lang/Object."<init>":()V

方法出口

  • 含义:方法开始执行后。
  • 只有 2 种方式可以退出:
    • 方法返回指令
    • 异常退出
  • 例如:
shell
2: return

三、【栈】产生问题

栈溢出

  • 栈深度超过虚拟机分配给线程的栈大小时, 就会出现栈溢出错误

  • 示例如下: 栈溢出错误

java
package com.calvin.jvm.structure.stack.question.example;

/**
 * 栈溢出
 *
 * @author Calvin
 * @date 2023/10/8
 * @since v1.0.0
 */
public class StackOverflow {

    /**
     * i 计算变量
     */
    private int i;

    /**
     * 加
     */
    public void plus() {
        // 叠加
        i++;
        // 递归过深产生栈溢出
        plus();
    }

    /**
     * 主要
     *
     * @param args 参数
     */
    public static void main(String[] args) {
        StackOverflow stackOverFlow = new StackOverflow();
        try {
            stackOverFlow.plus();
        } catch (Error e) {
            System.out.println("Error:stack length:" + stackOverFlow.i);
            e.printStackTrace();
        }
    }
}
  • 输出打印日志
log
Error:stack length:56217
java.lang.StackOverflowError
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)
	at com.calvin.jvm.structure.stack.question.example.StackOverflow.plus(StackOverflow.java:23)

栈溢出问题是指在程序执行过程中,当函数调用或递归调用过多次导致栈空间不足,从而导致程序崩溃或异常终止的情况。解决栈溢出问题的方法可以有以下几种:

  1. 优化递归算法:如果您的代码中存在递归调用,尝试优化递归算法减少递归深度或使用尾递归等技术来降低栈空间的使用。
  2. 增加栈空间大小:可以通过修改编译器运行时环境的设置,增加程序的栈空间大小。具体的方法取决于您使用的编程语言和开发环境。
  3. 使用循环替代递归:如果可能的话,将递归调用转换为循环结构,以减少栈空间的使用。这种方法可能需要重新设计和重构代码。
  4. 检查内存泄漏:栈溢出问题有时也可能是由于内存泄漏引起的。确保您的代码中没有未释放的资源或内存泄漏问题,可以使用内存分析工具来帮助检测和解决这些问题。
  5. 使用动态内存分配:如果栈空间不足以容纳大量数据或对象,可以考虑使用动态内存分配(如堆内存)来存储这些数据,以减少对栈空间的需求。

请注意,解决栈溢出问题可能需要对代码进行深入的调试和分析。具体的解决方法可能因编程语言、开发环境和具体情况而异。

内存逃逸

产生背景

  • 内存逃逸是指原本应该被存储在栈上的变量,因为一些原因被存储到了堆上。
  • New 出来对象不一定存放在中,有可能存放在栈帧中。
  • 如何将上的对象分配到,需要使用逃逸分析手段
  • 是编译语言中的一种优化分析
  • 而不是一种优化的手段
  • 通过对象的作用范围的分析,为其他优化手段提供分析数据从而进行优化。
  • 逃逸行为分为2种:
    • 方法逃逸
    • 线程逃逸
  • 当一个对象方法中定义之后作为参数传递到其它方法中.
  • 案例如下:
java
package com.calvin.jvm.heap.gc.example;

/**
 * 内存逃逸分析
 *
 * @author Calvin
 * @date 2023/9/12
 * @since v1.0.0
 */
public class MemoryEscapeAnalysis {

    /**
     * 用户
     *
     * @author calvin
     * @date 2023/06/28
     */
    public static class User {
        /**
         * 字节
         */
        private byte[] bytes = new byte[1024 * 1024];
    }


    /**
     * 方法逃逸
     *
     * - 当一个对象在方法中定义之后,作为参数传递到其它方法中.
     *
     * @return {@link User}
     */
    public static User methodEscapeByHeap() {
        User user = new User();
        return user;
    }


    /**
     * 方法未逃逸
     *
     * @return {@link User}
     */
    public static void methodEscapeByStackFrame() {
        User user = new User();
    }


    /**
     * 主方法
     *
     * -XX:+DoEscapeAnalysis (显示开启逃逸分析)
     * -XX:+PrintEscapeAnalysis (查看逃逸分析的筛选结果)
     *
     * @param args 参数列表
     */
    public static void main(String[] args) throws InterruptedException {


        System.out.println("================================= 开始: 方法未逃逸(存放在栈帧)=================================");
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            // 当前 user 被使用该方法存放在栈帧中
            methodEscapeByStackFrame();
        }
        long end2 = System.currentTimeMillis();
        System.out.println("程序执行的时间:" + (end2 - start2) / 1000d + "秒");
        System.out.println("================================= 结束: 方法未逃逸(存放在栈帧)=================================");


        System.out.println("================================= 开始: 方法逃逸 (存放在堆空间) =================================");
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            // 当前 user 被使用该方法存放在堆空间中
            User user = methodEscapeByHeap();
        }
        long end1 = System.currentTimeMillis();
        System.out.println("程序执行的时间:" + (end1 - start1) / 1000d + "秒");
        // Thread.sleep(1000 * 10);
        System.out.println("================================= 结束: 方法逃逸 (存放在堆空间) =================================");

    }

}
  • 类变量实例变量,可能被其它线程访问到

案例如下:

java
package com.calvin.jvm.heap.gc.example;

/**
 * 内存逃逸分析
 *
 * @author Calvin
 * @date 2023/9/12
 * @since v1.0.0
 */
public class MemoryEscapeAnalysis {


  /**
   * 用户
   */
  private static User user = new User();

  /**
   * 用户
   *
   * @author calvin
   * @date 2023/06/28
   */
  public static class User {
    /**
     * 字节
     */
    private byte[] bytes = new byte[1024 * 1024];
  }



  /**
   * 主方法
   *
   * -XX:+DoEscapeAnalysis (显示开启逃逸分析)
   * -XX:+PrintEscapeAnalysis (查看逃逸分析的筛选结果)
   *
   * @param args 参数列表
   */
  public static void main(String[] args) throws InterruptedException {

    System.out.println("================================= 开始: 线程逃逸 (存放在堆空间) =================================");
    // 如类变量或实例变量,可能被其它线程访问到;

    Thread threadA = new Thread(() -> {
      User user1 = user;
      System.out.println("threadA 用户信息" + user1);
      try {
        Thread.sleep(1000 * 10);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    });
    Thread threadB = new Thread(() -> {
      User user2 = user;
      System.out.println("threadB 用户信息" + user2);
      try {
        Thread.sleep(1000 * 10);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    });

    threadA.start();
    threadB.start();
    System.out.println("================================= 开始: 线程逃逸 (存放在堆空间) =================================");

  }
}

四、【内存逃逸】解决方式

显式开启逃逸分析

  • 启动参数添加 -XX:+DoEscapeAnalysis(默认开启) 显式开启逃逸分析
shell
java -jar  -Xms300m -Xmx300m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails xxxx.java

解决方案

  • 如果存在逃逸行为,则可以对该对象进行如下优化:

同步消除 (锁消除)

  • 它主要针对多线程环境中的同步操作进行优化,以提高程序的性能。
  • 在 Java 中,同步消除的案例可以涉及到使用锁(synchronized关键字)来保护临界区的代码。
  • 编译器会进行逃逸分析,判断锁对象是否可能逃逸出当前线程的范围,如果没有逃逸的可能性,就可以进行同步消除优化。
java
package com.calvin.jvm.heap.gc.example;

/**
 * 内存逃逸优化-同步消除方式
 *
 * @author calvin
 * @date 2023/09/13
 */
public class SyncEliminationOptimize {

    /**
     * 计数器
     */
    private int counter = 0;

    /**
     * 增量
     */
    public void increment() {
        // `synchronized`关键字来保护临界区
        synchronized (this) {
            counter++;
        }
    }

    /**
     * 获取计数器
     *
     * @return int
     */
    public int getCounter() {
        // `synchronized`关键字来保护临界区
        synchronized (this) {
            return counter;
        }
    }

    /**
     * 主要
     *
     * @param args 参数
     */
    public static void main(String[] args) {
        SyncEliminationOptimize example = new SyncEliminationOptimize();

        for (int i = 0; i < 1000000; i++) {
            example.increment();
        }

        System.out.println("Counter: " + example.getCounter());
    }
}

在上面的例子中,我们使用了 synchronized关键字来保护 increment()getCounter() 方法中的临界区。

  • 由于 counter变量没有逃逸出当前线程的范围,编译器可以进行同步消除优化将锁消除掉,从而提高程序的性能
  • 需要注意的是,同步消除是由编译器自动进行的优化,具体是否发生同步消除取决于编译器的实现和优化策略
  • 因此,无法保证在所有情况下都会发生同步消除。

标量替换

  • 标量替换是指将一个对象拆分成为其成员变量
  • 并将这些成员变量作为独立的标量值来处理,而不是作为一个整体对象。
  • 可以减少对象的创建销毁开销,提高程序的性能。
java
package com.calvin.jvm.heap.gc.example;

/**
 * 内存逃逸优化-标量替换
 *
 * @author calvin
 * @date 2023/09/13
 */
public class ScalarReplacementOptimize {

    /**
     * 点
     *
     * @author calvin
     * @date 2023/09/13
     */
    private static class Point {
        /**
         * x 轴
         */
        private int x;

        /**
         * y 轴
         */
        private int y;

        /**
         * 点
         *
         * @param x x 轴
         * @param y y 轴
         */
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        /**
         * 获取 x 轴
         *
         * @return int
         */
        public int getX() {
            return x;
        }

        /**
         * 获取 y 轴
         *
         * @return int
         */
        public int getY() {
            return y;
        }
    }


    /**
     * 标点
     *
     * @param x x 轴
     * @param y y 轴
     * @return {@link Point}
     */
    private static Point createPoint(int x, int y) {
        return new Point(x, y);
    }

    /**
     * 主要
     *
     * @param args 参数列表
     */
    public static void main(String[] args) {
        // 标点
        Point point = createPoint(10, 20);
        System.out.println("Point: (" + point.getX() + ", " + point.getY() + ")");
    }
}

在上面的例子中,我们定义了一个 Point 类,它有两个成员变量 x 和 y 。

  • createPoint() 方法中,我们创建了一个 Point 对象并返回。
  • 然而,由于 Point 对象没有逃逸出 createPoint() 方法的范围,编译器可以进行标量替换优化,将 Point 对象拆分为两个独立的标量值 x 和 y
  • 通过标量替换优化,编译器可以避免创建额外的对象,并直接使用独立的标量值来表示 Point 对象的成员变量。这样可以减少内存开销垃圾回收的压力,提高程序的性能。
  • 需要注意的是,标量替换是由编译器自动进行的优化,具体是否发生标量替换取决于编译器的实现和优化策略。因此,无法保证在所有情况下都会发生标量替换。

栈上分配

  • 栈上分配是指将对象分配在线程栈上,而不是在堆上进行分配。
  • 这样可以减少堆内存的使用垃圾回收的开销,提高程序的性能。
java
package com.calvin.jvm.heap.gc.example;

/**
 * 内存逃逸优化-栈上分配
 *
 * @author calvin
 * @date 2023/09/13
 */
public class StackAllocationOptimize {

    /**
     * 点
     *
     * @author calvin
     * @date 2023/09/13
     */
    private static class Point {
        /**
         * x 轴
         */
        private int x;

        /**
         * y 轴
         */
        private int y;

        /**
         * 点
         *
         * @param x x 轴
         * @param y y 轴
         */
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        /**
         * 获取 x 轴
         *
         * @return int
         */
        public int getX() {
            return x;
        }

        /**
         * 获取 y 轴
         *
         * @return int
         */
        public int getY() {
            return y;
        }
    }


    /**
     * 标点
     *
     * @param x x 轴
     * @param y y 轴
     * @return {@link StackAllocationOptimize.Point}
     */
    private static StackAllocationOptimize.Point createPoint(int x, int y) {
        return new StackAllocationOptimize.Point(x, y);
    }

    /**
     * 主要
     *
     * @param args 参数列表
     */
    public static void main(String[] args) {
        // 标点
        StackAllocationOptimize.Point point = createPoint(10, 20);
        System.out.println("Point: (" + point.getX() + ", " + point.getY() + ")");
    }
}