![实战Java虚拟机:JVM故障诊断与性能优化(第2版)](https://wfqqreader-1252317822.image.myqcloud.com/cover/901/47378901/b_47378901.jpg)
2.4.1 局部变量表
局部变量表是栈帧的重要组成部分之一。它用于保存函数的参数及局部变量。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,函数栈帧销毁,局部变量表也会随之销毁。
由于局部变量表在栈帧之中,因此,如果函数的参数和局部变量较多,会使局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。
【示例2-4】下面的代码演示了这种情况,第1个recursion()函数含有3个参数和10个局部变量,因此,其局部变量表含有13个变量。而第2个recursion()函数不含有任何参数和局部变量。当这两个函数被嵌套调用时,第2个recursion()函数可以拥有更深的调用层次。
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/44_4.jpg?sign=1739689498-yiMv3CsmEGwr5TVfjMzyXeCgDut2tI4r-0-710729e32a3292b485ceb7488e794c9a)
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/45_1.jpg?sign=1739689498-mddr63MCZXi1lJdNGyJZkGUquNS3AyIG-0-17e45f1adac2e3e78fd653463a11b825)
使用参数-Xss128K执行上述代码中的第1个recursion()函数,输出结果如下:
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/45_2.jpg?sign=1739689498-SfLV5unzSbFeM39Q9i3qHfQw8aNQTWen-0-88a19823bfda373da589b596d18c5cbf)
使用参数-Xss128K执行上述代码中的第2个recursion()函数,输出结果如下:
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/45_3.jpg?sign=1739689498-unYQLI3Sx75uNEFlHjhokdCXteI23Zst-0-4a0ef3075e7db9e3daa69c116bcaef27)
可以看到,在相同的栈容量下,局部变量少的函数可以支持更深层次的函数调用。
使用jclasslib工具可以进一步查看函数的局部变量信息。图2.6显示了第一个recursion()函数的最大局部变量表的大小为26字。因为该函数包含总共13个参数和局部变量,且都为long类型,long类型和double类型在局部变量表中需要占用2字,其他如int、short、byte、对象引用等占用1字。
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/45_4.jpg?sign=1739689498-ZR6UdtRdtdRHDz73NwdaOryenM2VvCxl-0-56d2c6393315d30c9351edbd982df63b)
图2.6 最大局部变量表大小
说明:字(Word)指的是计算机内存中占据一个单独的内存单元编号的一组二进制串。一般32位计算机上一个字为4个字节长度。
图2.7显示了在Class文件中的局部变量表的内容(这里说的局部变量表和上述的局部变量表不同,这里指Class文件的一个属性,而上述局部变量表指Java栈空间的一部分)。
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/46_1.jpg?sign=1739689498-QRbhvmSu43zxpwghk2XQ30XJ5f6MaNNR-0-bb1b178b0935873e2048dd6a14e7344f)
图2.7 Class文件中的局部变量表
可以看到,在Class文件的局部变量表中,显示了每个局部变量的作用域范围、所在槽位的索引(index列)、变量名(name列)和数据类型(J表示long型)。
栈帧中的局部变量表中的槽位是可以重用的,如果局部变量a超过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用局部变量a的槽位,从而达到节省资源的目的。
【示例2-5】下面的代码显示了局部变量表槽位的复用。在localvar1()函数中,局部变量a和b都作用到了函数末尾,故b无法复用a所在的位置。而在localvar2()函数中,局部变量a在第10行时不再有效,故局部变量b可以复用a的槽位(1个字)。
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/46_2.jpg?sign=1739689498-UUDSU5wPI8ueSGik4UeclXwz72OvDV0z-0-b3d5d04d2f150d4687b2651a13677a0f)
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/47_1.jpg?sign=1739689498-8CiVlrr0GOGISFlX08vRVdtGmzRsERy0-0-97b826c9fffb6a1fb229b732a8005eb9)
图2.8显示了localvar1()的局部变量信息,该函数最大局部变量大小为3字,第0个槽位为函数的this引用(实例方法的第一个局部变量都是this引用),第1个槽位为变量a,第2个槽位为变量b,每个变量占1字,合计3字。
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/47_2.jpg?sign=1739689498-RMRnj9zkMSgqi5lkgcaWKz3xTqSgbxeH-0-f2a7cd94331c877d96a2900bf1d1f50c)
图2.8 localvar1()的局部变量信息
图2.9显示了localvar2()的局部变量信息,该函数的最大局部变量表为2字,虽然和localvar1()一样,拥有this、a、b等3个局部变量,但b复用了a的槽位(从它们都占用了第1个槽位可以知道这点)。因此,在函数执行中,同时存在的最大局部变量为2字。
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/47_3.jpg?sign=1739689498-TBwnr0wRRd9xuX8jbAOGKxh99wyWsDbT-0-676be2efc7dfc35d4af551829e05bb8a)
图2.9 localvar2()的局部变量信息
局部变量表中的变量也是重要的垃圾回收根节点,被局部变量表中直接或间接引用的对象都是不会被回收的。因此,理解局部变量表对理解垃圾回收也有一定的帮助。
【示例2-6】下面通过一个简单的示例,展示局部变量对垃圾回收的影响。
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/47_4.jpg?sign=1739689498-7JZeX7ysab05NOyOsRjKY0HiPhoH7zBd-0-915ccdaf953da34cb5a2228e519cf00d)
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/48_1.jpg?sign=1739689498-HeW6l4yPOf8oJbeMOeuEn9XaNGx3mpQV-0-446d8e15f3b139a424ee0d931fe3a699)
上述代码中,每一个localvarGcN()函数都分配了一块6MB的堆空间,并使用局部变量引用这块空间。
在localvarGc1()中,在申请空间后,立即进行垃圾回收,很明显,由于byte数组被变量a引用,因此无法回收这块空间。
在localvarGc2()中,在垃圾回收前,先将变量a置为null,使byte数组失去强引用,故垃圾回收可以顺利回收byte数组。
对于localvarGc3(),在进行垃圾回收前,先使局部变量a失效,虽然变量a已经离开了作用域,但是变量a依然存在于局部变量表中,并且也指向这块byte数组,故byte数组依然无法被回收。
对于localvarGc4(),在垃圾回收之前,不仅使变量a失效,更是声明了变量c,使变量c复用了变量a的字,由于变量a此时被销毁,故垃圾回收器可以顺利回收byte数组。
对于localvarGc5(),它首先调用了localvarGc1(),很明显,在localvarGc1()中并没有释放byte数组,但在localvarGc1()返回后,它的栈帧被销毁,自然也包含了栈帧中的所有局部变量,故byte数组失去引用,在localvarGc5()的垃圾回收中被回收。
读者可以使用参数-XX:+PrintGC执行上述几个函数,在输出的日志中,可以看到垃圾回收前后堆的大小,进而推断byte数组是否被回收。下面的输出是函数localvarGc4()的运行结果:
![](https://epubservercos.yuewen.com/8F60EA/26763631609316106/epubprivate/OEBPS/Images/49_1.jpg?sign=1739689498-hxqtnK1weWXansoexItHXMMqAwL9g81i-0-217be3837c100fd61848db243fc9bb3d)
从日志中可以看到,堆空间从回收前的6746KB变为回收后的376KB,释放了约6MB空间。进而可以推断,byte数组已被回收释放。