结论:个人推荐,变量定义在for循环内部
原因:
1.在10亿次的数量级上,耗时上,两者没有区别
2.用javap进行反汇编后,指令码上基本无区别,所以在执行时也无区别(变量定义在for循环外没有赋值为null时,区别只是临时变量表中的位置不同;并赋值为null时,只是多了一个null压栈和出栈)
2.50.用gc日志彻底粉碎,变量定义在for循环内,会‘循环创建引用,浪费内存’的错误说法
3.jdk源码中,变量定义在for循环内部
过程:
原本用javap分析完反汇编后就可以结束并得出结论
结果我突发奇想打算实际证伪下‘循环创建引用,浪费内存’
然后就有了下面代码中描述的操作,结果分析了半天gc打印的日志,发现两种方式也一样,并没有多出来的‘大量引用占用空间’
但是又发现,gc日志中显示占用的内存空间与预估占用的内存空间不符(预估方式请参考另一篇文档,计算对象的大小:https://blog.csdn.net/jt781861965/article/details/114813576)
后来才突然惊醒到,经过逃逸分析,这些对象都被直接在栈上分配了,并没有在堆上分配,所以也不存在gc的问题(逃逸分析默认是打开的,具体逃逸分析可参考:https://www.cnblogs.com/gnivor/p/6028001.html)
**关闭逃逸分析后,**1000w次的数量级上,耗时上,两者没有区别(相较于开启逃逸分析,差距还是很大);javap进行反汇编后,指令码与开启逃逸分析时无区别;gc日志中显示的占用内存也是一样多,最后通过计算得出一个对象占用17byte(因为有jvm自身消耗的内存,计算存在误差,因此结果与实际的一个User对象16byte吻合)
至此,彻底粉碎了,变量定义在for循环内,会‘循环创建引用,浪费内存’的错误说法
测试代码(vm参数在代码中):
package com.myspringboot.jvm.foru;
public class ForUserTest {
//开启逃逸分析使用下面的代码
//vm参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+PrintGC -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintGCApplicationStoppedTime
public static void main(String[] args) {
//填充内存
byte[] bytes1 = new byte[5 * 1024 * 1024];
long start = System.currentTimeMillis();
//考虑到业务场景,在for中获取到对象后,没有后续操作的话,没有意义;所以在new完之后,还进行了简单的操作
outFor();
//inFor();
System.out.println(System.currentTimeMillis() - start);
//强制gc
byte[] bytes2 = new byte[5 * 1024 * 1024];
}
public static void outFor() {
User user = null;
for (int i = 0; i < 999999999; i++) {
user = new User();
user.setI(i);
user.getI();
}
}
public static void inFor() {
for (int i = 0; i < 999999999; i++) {
User user = new User();
user.setI(i);
user.getI();
}
}
//分隔线=====================================================================================================
//关闭逃逸分析使用下面的代码
//vm参数:-server -XX:-DoEscapeAnalysis -verbose:gc -Xms400M -Xmx400M -Xmn300M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+PrintGC -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintGCApplicationStoppedTime
// public static void main(String[] args) {
// long start = System.currentTimeMillis();
// //考虑到业务场景,在for中获取到对象后,没有后续操作的话,没有意义;所以在new完之后,还进行了简单的操作
// outFor();
// //inFor();
// System.out.println(System.currentTimeMillis() - start);
// //强制gc
// byte[] bytes2 = new byte[200 * 1024 * 1024];
// }
//
// public static void outFor() {
// User user = null;
// for (int i = 0; i < 9999999; i++) {
// user = new User();
// user.setI(i);
// user.getI();
// }
// }
//
// public static void inFor() {
// for (int i = 0; i < 9999999; i++) {
// User user = new User();
// user.setI(i);
// user.getI();
// }
// }
}
package com.myspringboot.jvm.foru;
public class User {
private int i;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
用javap对class文件进行反汇编:
javap -c -l -v ForUserTest.class
运行时常量池
outFor()
inFor()
对其中的outFor()方法的反汇编指令码进行了解释
0: aconst_null
(将null压栈)
(栈:null)
1: astore_0
(弹出,将null放到局部变量表‘0’位置)
(栈:)
(局部变量表‘0’位置:null)
2: iconst_0
(将0压栈)
(栈:0)
3: istore_1
(弹出,将0放到局部变量表‘1’位置)
(栈:)
(局部变量表‘1’位置:0)
4: iload_1
(将局部变量表‘1’位置的值压栈)
(栈:0)
5: ldc #8 // int 999999999
(将运行时常量池中8位置的值999999999压栈)
(栈:0,999999999)
7: if_icmpge 34
(弹出栈中中的两个值,并进行比较,当局部变量表‘1’位置的值大于等于999999999时,跳转到34行开始执行;否则接着顺序执行)
(栈:)
10: new #9 // class com/myspringboot/jvm/foru/User
(从运行时常量池中9位置的值找到class对象,分配空间,类属性赋初始值后压栈)
(栈:0xFF(引用地址))
13: dup
(将栈顶的元素复制,并重新压栈(此时栈中有两个相同的元素))
(栈:0xFF,0xFF)
14: invokespecial #10 // Method com/myspringboot/jvm/foru/User."<init>":()V
(从运行时常量池中10位置的值找到init方法,弹出一个元素,对类属性赋默认值,并执行构造方法)
(栈:0xFF)
17: astore_0
(弹出,将0xFF放到局部变量表‘0’位置)
(栈:)
18: aload_0
(将局部变量表‘0’位置的值压栈)
(栈:0xFF)
19: iload_1
(将局部变量表‘1’位置的值压栈)
(栈:0xFF,0)
20: invokevirtual #11 // Method com/myspringboot/jvm/foru/User.setI:(I)V
(从运行时常量池中11位置的值找到setI方法,弹出两个元素,执行setI方法,因为没有返回值,所以不需要压栈)
(栈:)
23: aload_0
(将局部变量表‘0’位置的值压栈)
(栈:0xFF)
24: invokevirtual #12 // Method com/myspringboot/jvm/foru/User.getI:()I
(从运行时常量池中12位置的值找到getI方法,弹出一个元素,执行getI方法,并将结果压栈)
(栈:0)
27: pop
(将栈顶元素弹出)
(栈:)
28: iinc 1, 1
(将局部变量表‘1’位置的值加1,注意此操作没有压栈、弹出的动作)
(栈:)
31: goto 4
(转到4)
34: return
(返回)
开启逃逸分析的vm参数和gc日志
关闭逃逸分析的vm参数和gc日志