0%
这是一片思考的空间 -- arthinking
Spring 重构&代码整洁之道 软件设计 JVM 并发编程 数据结构与算法 分布式 存储 网络 微服务 设计模式
Java技术栈 - 涉及Java技术体系

深入理解运行时栈帧结构 | 栈帧,操作数栈,本地变量表

2、JVM运行时数据区是如何工作的 这节中,我们已经见过运行时栈帧结构的面貌了,现在我们再来深入的了解一下有关它的故事。

1、栈帧结构

我们再拿出这张图来看看,不过这次我们是要更深入的来了解下了。

image-20200107224256906

假设有如下调用关系:ClassA.invokeA() -> ClassB.invokeB() -> ClassB.doInvokeB() -> ClassC.execute(),则会生成以上的虚拟机栈,每个方法调用,都有一个栈帧入栈,调用完成,栈帧从虚拟机栈上出栈;虚拟机栈是一个LIFO的栈。

栈帧

每个栈帧都包括局部变量表、操作数栈、动态链接、方法会地址和其他的附加信息。

1.1、局部变量表

变量操:Slot,最小单位。

一个本地变量(Slot)可以存32位以内的数据,可以保存类型为 int, short, reference, byte, char, floath和returnAddress的数据,两个本地变量可以保存类型为long和double的数据;

**线程安全问题:**局部变量表建立在线程的堆栈上面,线程私有的,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。

何时确定栈帧需要多大内存空间?

在程序编译完成生成class文件之后,其实就已经确定了局部变量表的大小,以及操作数栈的大小。

以这个例子为例:Class文件16进制背后的秘密#3、解析Class文件实例

编译为class文件之后,通过反汇编指令,即可查看到对应的Java汇编指令,其中init方法的指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 类中自定义的init方法
public void init(java.lang.String);
descriptor: (Ljava/lang/String;)V
// 访问标记
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: ldc #3 // String test method 将常量”test method“的在常量池中的索引压入栈
2: astore_2 // 从栈中取出刚刚的索引,存储到本地变量表的tmp中
3: aload_0 // this引用入栈
4: iconst_1 // 数值1入栈
5: putfield #2 // Field a:I 数值1出栈,赋值给 常量池#2,这里是一个Fieldref,对应代码中的a变量
8: return
LineNumberTable:
line 9: 0
line 10: 3
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/itzhai/classes/TestA;
0 9 1 title Ljava/lang/String;
3 6 2 tmp Ljava/lang/String;

可见,编译为class文件之后,这些内容都确定了:

  • 操作数栈大小;
  • 本地变量表大小;

至于线程执行的时候,创建对象,都是在堆中分配的,栈帧指针保存了对象的引用,引用大小是固定的。

slot复用问题引出的:不使用的对象应该手动赋值为null?

如下代码,我们在作用域外执行gc,启动参数添加-verbose:gc:

1
2
3
4
5
6
7
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
// int a = 1; // 新加一个赋值操作
System.gc();
}

执行结果:

1
2
[GC (System.gc())  69530K->66344K(125952K), 0.0012475 secs]
[Full GC (System.gc()) 66344K->66223K(125952K), 0.0070326 secs]

发现局部变量表应用的对象并没有被回收。

我们取消上面的注释,在执行,结果:

1
2
[GC (System.gc())  69529K->66330K(125952K), 0.0132218 secs]
[Full GC (System.gc()) 66330K->687K(125952K), 0.0267554 secs]

发现回收了。

或者我们不取消注释,在启动参数添加参数:

-verbose:gc -Xcomp

发现也可以正常回收:

1
2
[GC (System.gc())  70861K->66360K(125952K), 0.0031858 secs]
[Full GC (System.gc()) 66360K->741K(125952K), 0.0103660 secs]

这是因为JIT编译优化的结果。

即使离开了方法的作用域,方法里面局部变量表里面的Slot没有被其他变量复用,它仍然是一个GC Root,不会作为垃圾被回收。

类变量有零值而局部变量无零值

局部变量表不想类变量有准备阶段赋值初始化,如果没有显示的在方法中给局部变量赋值,是读取不了变量的。

1.2、操作数栈

1.3、动态链接

1.4、方法返回地址

欢迎关注我的其它发布渠道

订阅IT宅
内功修炼
Java技术栈
Java架构杂谈是IT宅精品文章公众号,欢迎订阅:
📄 网络基础知识:两万字长文50+张趣图带你领悟网络编程的内功心法 📄 HTTP发展史:三万长文50+趣图带你领悟web编程的内功心法 📄 HTTP/1.1:可扩展,可靠性,请求应答,无状态,明文传输 📄 HTTP/1.1报文详解:Method,URI,URL,消息头,消息体,状态行 📄 HTTP常用请求头大揭秘 📄 HTTPS:网络安全攻坚战 📄 HTTP/2:网络安全传输的快车道 📄 HTTP/3:让传输效率再一次起飞 📄 高性能网络编程:图解Socket核心内幕以及五大IO模型 📄 高性能网络编程:三分钟短文快速了解信号驱动式IO 📄 高性能网络编程:彻底弄懂IO复用 - IO处理杀手锏,带您深入了解select,poll,epoll 📄 高性能网络编程:异步IO:新时代的IO处理利器 📄 高性能网络编程:网络编程范式 - 高性能服务器就这么回事 📄 高性能网络编程:性能追击 - 万字长文30+图揭秘8大主流服务器程序线程模型
📄 Java内存模型:如果有人给你撕逼Java内存模型,就把这些问题甩给他 📄 一文带你彻底理解同步和锁的本质(干货) 📄 AQS与并发包中锁的通用实现 📄 ReentrantLock介绍与使用 📄 ReentrantReadWriteLock介绍与使用 📄 ReentrantLock的Condition原理解析 📄 如何优雅的中断线程 📄 如何优雅的挂起线程 📄 图解几个好玩的并发辅助工具类 📄 图解BlockingQueue阻塞队列
📄 消息队列那么多,为什么建议深入了解下RabbitMQ? 📄 高并发异步解耦利器:RocketMQ究竟强在哪里? 📄 Kafka必知必会18问:30+图带您看透Kafka
📄 洞悉MySQL底层架构:游走在缓冲与磁盘之间 📄 SQL运行内幕:从执行原理看调优的本质 📄 洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法