文章目录
- 简介
- 代码分析
- 反编译之后对比
- 性能测试
- 内存与垃圾回收情况
- JDK和常用框架怎么写
- 总结
- 依赖
简介
不知道是谁最先提出了一个不要将变量定义在循环内。
然后我们在代码扫描中有一项是:【禁止new对象时在for循环内定义申明变量】
我也好奇为什么不能?
是影响了性能?还是影响了内存?还是影响了垃圾回收?
我们站在制定这条规则的人的角度来想一想可能的原因:
- 变量定义在for循环内,每次都要为变量引用分配空间,效率低
- 变量定义在for循环内,有引用,不利于内存回收
- …
真的如此吗?我们先从代码分析一下。
代码分析
我们先从代码的角度分析,局部变量定义在循环外和循环内的区别。
不熟悉局部变量和字节码的朋友可以先看一看:
JVM字节码与局部变量
示例代码如下:
import java.util.HashMap;
import java.util.Map;
public class Main {
private static final int limit = 1000_0000;
public static void main(String[] args) {
}
public void out() {
Map<String, String> base;
for (int i = 0; i < limit; i++) {
base = new HashMap<>();
base.put("1", "哦啊好");
System.out.println(base);
}
}
public void in() {
for (int i = 0; i < limit; i++) {
Map<String, String> base = new HashMap<>();
base.put("1", "哦啊好");
System.out.println(base);
}
}
}
我们可以:
# 编译class
javac Main.java
# 查看字节码
javap -v Main.class
我们可以看到,无论变量定义在循环外还是循环内,base变量在局部变量表都只占据了一个slot。
自然,也就没有每次循环都要给变量引用分配空间的问题。至于base堆上的内存空间,无论变量定义在循环内外,肯定都要分配,没有区别。
至于垃圾回收,无论变量base在循环内还是循环外,局部变量base引用的都是最新new出来的Map对象,旧的对象不被引用,自然可以被回收,一点也不影响。
基本一模一样,不信我吗可以看看反编译之后的代码。
反编译之后对比
//
// Source code recreated from a .class file
// (powered by FernFlower decompiler)
//
package vip.meet;
import java.util.HashMap;
import java.util.Map;
public class Main {
private static final int limit = 10000000;
public Main() {
}
public static void main(String[] args) {
}
public void out() {
for(int i = 0; i < 10000000; ++i) {
Map<String, String> base = new HashMap();
base.put("1", "哦啊好");
System.out.println(base);
}
}
public void in() {
for(int i = 0; i < 10000000; ++i) {
Map<String, String> base = new HashMap();
base.put("1", "哦啊好");
System.out.println(base);
}
}
}
我们可以看到反编译之后,out方法的变量被放在循环中了,变得和in一模一样了。
有朋友肯能说,我看到了,他们字节码不一样啊,反编译出来的代码不能说明什么。
把out稍微改一下,将int i定义放在Map的base之前,字节码就一样了。
public void out() {
int i = 0;
Map<String, String> base;
for (; i < limit; i++) {
base = new HashMap<>();
base.put("1", "哦啊好");
System.out.println(base);
}
}
如果,还觉得有疑问,我们下面来做点性能测试。
性能测试
我们先用benchmark做一些性能测试:
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.TimeValue;
import java.util.HashMap;
import java.util.Map;
public class BenchmarkLocalVarTest {
private static final int limit = 10000_0000;
@Benchmark
public void out() {
Map<String, String> base;
for (int i = 0; i < limit; i++) {
base = new HashMap<>();
base.put("1", "哦啊好");
}
}
@Benchmark
public void in() {
for (int i = 0; i < limit; i++) {
Map<String, String> base = new HashMap<>();
base.put("1", "哦啊好");
}
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(BenchmarkLocalVarTest.class.getSimpleName())
.forks(1)
.timeout(TimeValue.seconds(10))
.threads(1)
.warmupIterations(1)
.warmupTime(TimeValue.seconds(10))
.measurementTime(TimeValue.seconds(2))
.measurementIterations(5)
.build();
new Runner(options).run();
}
}
Benchmark Mode Cnt Score Error Units
BenchmarkLocalVarTest.in thrpt 5 0.349 ± 0.037 ops/s
BenchmarkLocalVarTest.out thrpt 5 0.335 ± 0.039 ops/s
如果还有朋友觉得这个测试只能说明性能,不能看出内存和垃圾回收,那我们在来看一下内存和垃圾回收。
内存与垃圾回收情况
变量定义在循环内gc情况:
变量定义在循环内内存情况:
变量定义在循环外gc情况:
变量定义在循环外内存情况:
我们可以看到,没有太大区别,如果觉得上面逻辑太简单,可以自己试一下更复杂的情况。
JDK和常用框架怎么写
如果这样还不放心,那我们再看一下其他大神怎么写的:
JDK LinkedBlockingDeque:
Spring ResourceUrlProvider:
apache io FileUtils:
guava Files:
总结
综上所述,【禁止new对象时在for循环内定义申明变量】并没有什么意义。
甚至,还带来了负面效果:
- 让变量作用域变大,存在被无意引用的风险
- 更容易产生变量名冲突
- 可读性变差
依赖
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
</dependency>