码迷,mamicode.com
首页 > 其他好文 > 详细

Jvm(60),虚拟机字节码执行引擎----局部变量表

时间:2018-09-22 23:13:38      阅读:187      评论:0      收藏:0      [点我收藏+]

标签:访问   阶段   字节   bubuko   sonar   开发   @param   str   设置   

技术分享图片

在讲这一节之前我们先来抛出一个问题,为什么局部变量必须初始化才能使用,而全局变量却不需要初始化呢?

在这里先写出原因,因为全局变量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),虚拟机字节码执行引擎----局部变量表

标签:访问   阶段   字节   bubuko   sonar   开发   @param   str   设置   

原文地址:https://www.cnblogs.com/qingruihappy/p/9691489.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!