一、栈
- 含义:主要支持虚拟机
进行方法调用
和方法执行
的数据结构。- 作用:解决的是
运行问题
。(即程序如何执行,或者如何处理数据) - 栈数据结构原则:
先进后出,后进先出
。 栈帧
的内部结构主要包含:局部变量表
操作数栈
动态链接
方法出口
- 作用:解决的是
- 例如:
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)
栈溢出问题是指在程序执行过程中,当函数调用或递归调用过多次导致栈空间不足,从而导致程序崩溃或异常终止的情况。解决栈溢出问题的方法可以有以下几种:
优化递归算法
:如果您的代码中存在递归调用
,尝试优化递归算法
,减少递归深度
或使用尾递归
等技术来降低栈空间的使用。增加栈空间大小
:可以通过修改编译器
或运行时环境的设置
,增加程序的栈空间大小
。具体的方法取决于您使用的编程语言和开发环境。使用循环替代递归
:如果可能的话,将递归调用转换为循环结构
,以减少栈空间的使用。这种方法可能需要重新设计和重构代码。检查内存泄漏
:栈溢出问题有时也可能是由于内存泄漏
引起的。确保您的代码中没有未释放的资源或内存泄漏问题,可以使用内存分析工具来帮助检测和解决这些问题。使用动态内存分配
:如果栈空间不足以容纳大量数据或对象,可以考虑使用动态内存分配
(如堆内存)来存储这些数据,以减少对栈空间的需求。
请注意,解决栈溢出问题可能需要对代码进行深入的调试和分析。具体的解决方法可能因编程语言、开发环境和具体情况而异。
内存逃逸
产生背景
- 内存逃逸是指原本应该被存储在栈上的变量,因为一些原因被存储到了堆上。
- 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() + ")");
}
}