Jvm(60),虚拟机字节码执行引擎----局部变量表
Posted qingruihappy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jvm(60),虚拟机字节码执行引擎----局部变量表相关的知识,希望对你有一定的参考价值。
在讲这一节之前我们先来抛出一个问题,为什么局部变量必须初始化才能使用,而全局变量却不需要初始化呢?
在这里先写出原因,因为全局变量static一般在类加载器准备的阶段就已经加载到方法区之中了,并且会给它附一个初始化的值比如说0,null之类的让后在把程序员初始化的值付给成员变量。而局部变量却不是这样的,它没有在方法区之中,相对于全局变量,局部变量的生命周期短,声明次数多,如果像全局变量一样给个初始值的话会影响性能,不给初始值又不安全,所以折中了一下,规定了用户需要先赋值再使用。如果没有初始化,类似c的随便指了一个地址。所以java直接编译失败了
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在
Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的 大容量。
局部变量表的容量以变量槽(Variable Slot,下称Slot)为 小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、
float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出"每个Slot占用32位长度的内存空间"是有一些差别的,它允许Slot的长度可以着处理器、操作系统或虚拟机的不同而发生变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为
public static void main(String[] args) {
byte[] placeholder = new byte[64 * 1024 * 1024]?
System.gc()?
如上代码很简单,即申请了了 64 MB 的数据,然后通知虚拟机进行垃圾收集。我们在虚拟机运行参数中加上"verbose:gc" 来看看垃圾收集的过程,发现在 System.gc() 运行后并没有回收这 64 MB 的内存。
没有回收 placeholder 所占的内存能说得过去,因为在执行 System.gc() 时,变量 placeholder 还处于作用域之内,虚拟机自然不敢回收placeholder 的内存。那我们把代码修改如下:
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024]?
}
System.gc()?
加入了花括号之后,placeholder 的作用域被限制在花括号之内,从代码逻辑上讲,在执行 System.gc() 的时候, placeholder 已经不可能再被访问了,但执行一下这段程序,会发现运行结果如下,还是有 64MB 的内存没有被回收,这又是为什么呢?在解释为什么之前,我们先对这段代码进行第二次修改如下:
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024]?
}
int a = 0?
System.gc()?
这个修改看起来很莫名其妙,但运行一下程序,却发现这次内存真的被正确回收了。
在如上代码中,placeholder 能否被回收的根本原因是:局部变量中的 Slot 是否还存在关于 placeholder 数组对象的引用。第一次修
改中,代码虽然已经离开了 placeholder 的作用域,但在此之后,没有任何局部变量表的读写操作,placeholder 原本占用的 Slot 还没有
被其他变量所复用,所以作为 GC Roots 一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量的内存、实际上已经不会再使用的变量,
手动将其设置为 null 值(用来代替那句 int a=0,把变量对应的局部变量表 Slot 清空)便不见得是一个绝对无意义的操作,这种操作可
以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到 JIT 的编译条件)下的 "奇技" 来
使用。Java 语言的一本著名书籍《Practical Java》中把 "不使用的对象应手动赋值为 null" 作为一条推荐的编码规则。
public class TestMain {
/**
- -verbose:GC
- -XX:+PrintGCDetails
- @param args
*/
public static void main(String[] args) {
byte[] bytes = new byte[64 * 1024 * 1024]?
//do something
bytes = null?
System.gc()?
}
关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在 "准备阶段"。通过之前的讲解,我们已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始化;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局
部变量定义了但没有赋初始值是不能使用的,不要认为 Java 中任何情况下都存在诸如整型变量默认为 0,布尔型变量默认为 false 等这样
的默认值。先来看看下面的代码:
public static void main(String[] args) {
int a?
System.out.println(a)?
段代码其实并不能运行,还好编译器能在编译期间就检查到并提示这一点,即便编译能通过或者手动生成字节码的方式制造出下面代码的效果,字节码校验的时候也会被虚拟机发现而导致类加载失败。
假如说
String city="";
if (CollectionUtils.isNotEmpty(cityList)) {
for (String ct : cityList) {
cityStr.append(ct + "、");
}
city = cityStr.substring(0, cityStr.length() - 1);
} else
下面还要用到city的
这种情况,就没必要初始化了,因为这种情况下city没有被用到,只是被赋值了,虽然照样可以执行,但是不友好,回报sonar问题的。可以改成这样的。
String city;
if (CollectionUtils.isNotEmpty(cityList)) {
for (String ct : cityList) {
cityStr.append(ct + "、");
}
city = cityStr.substring(0, cityStr.length() - 1);
} else
假如说是这样的
String city;
if (CollectionUtils.isNotEmpty(cityList)) {
for (String ct : cityList) {
cityStr.append(ct + "、");
}
city.toString();
city = cityStr.substring(0, cityStr.length() - 1);
} else city就必须初始化了,否则会报编译出错的。
ff
以上是关于Jvm(60),虚拟机字节码执行引擎----局部变量表的主要内容,如果未能解决你的问题,请参考以下文章