系列文章目录
目录
前言
Java栈是一块线程私有的内存空间。如果说Java堆和程序数据密切相关,那么Java栈就是和线程执行密切相关的。线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。
一、Java栈的基本结构
Java栈是一种先进后出的数据结构,只支持出栈和入栈两种操作。出栈和入栈的单位被称为栈帧,它是Java栈的重要组成部分。Java栈结构如图1.1所示
由于每次函数调用都会生成对应的栈帧,从而占用一定的栈空间。因此如果栈空间不足,那么函数调用自然无法继续进行,当系统空间不足时就会抛出StackOverflowError栈溢出错误。一般出现在递归调用出口判断异常是出现。当前版本的JVM Stack会自动进行扩容,但并不会无限的扩容。
二、局部变量表
局部变量表时栈帧的重要组成部分之一。它用于保存函数的参数和局部变量。局部变量表中的变量只有在当前函数调用中有效,当函数调用结束后,函数销毁,局部变量表也会销毁。
需要说明的是栈帧中的局部变量表中的槽位是可以重用的,如果局部变量表a超过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用局部变量a的槽位,从而达到节省资源的目的。
public class TestLocalParam {
/**
* 方法中局部变量a和b都作用到函数末尾,
* 所以变量b无法复用变量a的槽位
*/
public void test1() {
int a = 1;
int b = 0;
System.out.println(a);
}
/**
* 局部变量a的作用域结束后,
* b可以复用a的槽位
*/
public void test2() {
{
int a = 0;
System.out.println(a);
}//a的作用域结束
int b = 0;
}
}
除此之外,局部变量表中的变量也是重要的垃圾回收根节点,被局部变量表中直接或者间接引用的对象都是不会被回收的。因此理解局部变量表对理解垃圾回收也有一定的帮助。
三、操作数栈
操作数栈也是栈帧的重要组成内容之一,它主要用于保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
四、帧数据区
除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些信息都保存在Java栈帧的帧数据区中。
除了上述信息(支持常量池解析、正常方法返回和异常派发的数据)外,虚拟机的实现者也可以将其他信息放入帧数据区,如用于调试的数据等
帧数据区信息补充
动态链接
每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。常量池中对类型、字段和方法的引用在开始时都是符号。当虚拟机在常量池中搜索的时候,如果遇到指向类、接口、字段或者方法的入口,假若它们仍然是符号,虚拟机那时才会进行解析为直接引用,这被称为动态链接。
函数正常返回
如果是通过return正常结束,虚拟机必须恢复发起调用的方法的栈帧,包括设置PC寄存器指向发起调用的方法中的指令。假如方法有返回值,虚拟机必须将它压入到发起调用的方法的操作数栈。
异常表
要处理Java方法执行期间的异常退出情况,帧数据区还必须保存一个对此方法异常表的引用。异常表定义了在这个方法的字节码中受catch子句保护的范围,异常表中的每一项都有一个被catch子句保护的代码的起始和结束位置可能被catch的异常类在常量池中的索引值,以及catch子句内的代码开始的位置。
五、栈上分配
针对那些作用域不会逃逸出方法的对象,在分配内存时不在将对象分配在堆内存中,而是将对象属性打散后分配在栈(线程私有的,属于栈内存)上,这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给gc增加额外的无用负担,从而提升应用程序整体的性能。
逃逸分析
通俗的说,如果一个对象的指针被多个方法或者线程引用时,那么我们称这个对象的指针发生了逃逸。逃逸可以分为全局逃逸(一个对象的引用逃出了方法或者线程,成员变量、返回值)和参数逃逸(作为另外一个方法的调用参数);
逃逸分析示例代码1
/**
* Desc: 逃逸分析示例代码
* Author:liusg
* Date:2022/12/5
**/
public class EscapeAnalysis {
private Node node = new Node(); //全局变量
/**
* 全局变量赋值发生指针逃逸
*/
public void VariablePointerEscape() {
node = new Node();
}
/**
* 方法返回指针逃逸
*/
public Node methodPointerEscape() {
return new Node();
}
/**
* 外部实例引用发生指针逃逸
*/
public void instancePassPointer() {
methodPointerEscape().print(this);
}
static class Node {
private Integer data;
private Node leftChild;
private Node rightChild;
private void print(EscapeAnalysis escapeAnalysis) {
System.out.println("current node ==" + escapeAnalysis.node.data);
}
}
}
逃逸分析示例代码2
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb; //发生了逃逸
}
public static String createStringBuffer1(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
//未发生逃逸,本质上返回的是重新new()的新字符串,详见toString()代码
return sb.toString();
}
逃逸分析的作用
-
栈上分配:一个方法中的对象,若该对象没有发生逃逸,则可以将这个对象分配在栈上
-
消除同步:线程同步的代价是相当高的,同步带来的后果是降低了并发性和程序性能。逃逸分析以判断某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么该对象的同步操作就可以转化为没有同步的操作,这样可以大大提高并发性能
-
标量替换:java虚拟机中的原始数据类型(int,long等)都不能在进一步分解,他们就可以成为标量。相对的,如果一个数据可以继续分解,那么他成为聚合量,java中最典型的聚合量就是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个这个对象是可以分解的,那么程序真正执行的时候可能不创建这个对象,而改为直接创建它的若干个被这个方法能够使用到的成员变量来代替。拆散后的变量便可以被单独的分析与优化,可以分别分配在栈帧或者寄存器上,原来的对象就不需要整体被分配在堆中。
六、TLAB
TLAB (Thread Local Allocation Buffer)线程本地分配缓存区。JVM在内存新生代Eden 区中开辟的一小块线程私有区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden 区的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
TLAB的作用
- .一定程度上避免了堆内存指针碰撞问题,避免了内存竞争
- 线程私有没有锁的开销
- .线程私有适合被快速GC不需要可达性分析进行判断
TLAB的局限性
- GC更频繁,由于每个TALB所占用的空间都要比线程实际需要的空间大小大一些,所以一批对象直接存储在Eden区会比存储在TALB区占用更多的空间,进而容易引发Minor GC。
- TALB允许内存浪费,会导致Eden区内存不连续。
知识补充
栈上分配和TLAB哪个优先级高
不废话上图
由上边的流程图可知,栈上分配的优先级是高于TLAB的。其实也很好解释,直接在栈空间上进行分配不涉及到GC等问题,效率更高。
指针碰撞问题
在多线程场景下,当一个线程需要创建对象,这时指针还没来得及修改(指针是在新对象占完位之后才能进行修改),如果另一个线程也需要分配空间,就会造成两个对象空间冲突,这就称之为指针碰撞,TALB能在一定程度上解决指针碰撞问题。
总结
本篇文章我们介绍了Java栈空间的相关知识,包括栈的结构,各部分的功能、栈上分配、TLAB、指针碰撞等相关理论知识,汇总如下
- java栈是是先进后出的数据结构,其主要组成部分是栈帧,栈帧由局部变量表、操作数栈和帧数据区组成。
- 局部变量表主要存储函数的参数以及局部变量;操作数栈负责保存计算的中间结果以及作为变量的临时存储空间;帧数据区中维护着指向常量池的指针,以及函数返回和通过维护异常表引用来进行异常控制;
- 在JVM中逃逸分析是很最重要的知识点,它是栈上分配、消除同步、标量替换的基础;
- 如果开启了栈上分配,当对象未达到逃逸标准并且对象满足一定条件时可以直接进行栈上分配;
- 对象在堆空间进行分配时容易产生指针碰撞的问题,TLAB是一种解决指针碰撞的方式之一。基本思想是在Eden区为每个线程维护一段线程私有的空间,如果对象为非共享变量则可以分配到该线程私有空间中;
- 栈上分配比TLAB的优先级更高;
- 指针碰撞可以理解为多线程要通过操作同一个内存空间