哈喽,咱们好,磊哥的功能优化篇又来了!

其实写这个功能优化类的文章初衷也很简单,第一:目前市面上没有太好的关于功能优化的系列文章,包含一些付费的文章;第二:我需要写一些和别人不同的知识点,比方咱们都去写 SpringBoot7 C p H X S T G 了,那我就不会把要点全部放在 SpringBoot 上。而功能优化方面的文章又比较少,因而这便是我写它的理由。

至于能不能用上?是不是刚需?我想每个人都有自己的答案。就像一个好的剑客,N ) @ , N终其一生都会对宝剑痴迷,我相信读到此文的你也是相同。

回到今日的主题,这次咱们来评测一下局部变量和全局变量的功能差异,首要咱们先在项目中先增加 Oracle 官方提供的 JMH(Java MicrobencG $ ! $ J Thmark Harness,JAVA 微基准测验套件)测验框架,装备如下@ ! M $ p ` z

<!-- https://mvnrepository.com/artiD } o 4fact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.j; = ` h 9 ^ K k xmh</groupId>
<artifac^ p }tId>jmh-core</artifactId>
<version>{version}</version>
</dependency>

然后编写测验代码:

import org.openjdk.jmh.annotation0 d Es.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.optiL Y a ? &ons.Options;
import org.openjdk.jmh.7 7 [ % ; #runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测验完结时刻
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2,T 3  E + + time = 1, timeUnit = Ti[ E h ^ x ~ F @ [meUnitB S j Z.SECONDS) // 预热 2 轮,每次 1s
@MA R weasurement(iteratt + 9 =ions =w . ! = 5, time = 3, timo 2 9 H 3 ) Z ! Cej u 6 K UUnit = TimeUnit.SECONDS) // 测验 5 轮,每次 3e c V # u us
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测验线程一个实例
public class VarOptimizeTest {
char[] myChars = ("Oracle Cloud Infrastructure Low data networking fees and " +
"automated migry n 3 W I *ation Oracle Cloud IQ a Bnfrastructure platform is built for " +
"enterp) ; 8 wrises that are looking fo} g _ y ; q Er higher pV ~ W 8 6 m g OerformancR = 1 / ] n ue computing with easy " +
"migration of their on-premisesy b @ 6 f T C @ i applications to the Cloud.").toCharArray();
public static void main(String[] args) throws RunnerException {
// 发动基准测验
Options opt = new OptionsBuilder(` D s ! Q 1 g $ +)
.include(VarOptimizeTef { L [ /st.class.getSimpleNam; * r k ? {e()) // 要导入的测验类
.I D u & ] } ;build();
new Runner(opt).run(); // 履行测验
}
@BI (  % ~ J U {enchmark
public int globalVarTest() {
int count = 0;
for (int i = 0; i < myChars.length; i++) {
if (myChars[i]e B F | 9 $ == 'c') {
count++;
}
}
return count;
}w ` L
@Benchmark
public int localityVarTest(A ; n 7 V) {
char[] localityChars = myChars;
int count = 0;
for (int i =O z H 4 7 7 : l 0; i < localityChars.lengths A 9 K k a U = C; i++) {
if (localityChars[i] == 'c') {
count++;
}
}
return count;
}
}

其间 globalVarTest 办法运用的是全局变量 myChars 进行循环遍历的,而 localitH | - S [ 6yVarTest 办法运用的是局部变量 localityChars 来进行遍历循环的,运用 JMH 测验的成果如下:

局部变量竟然比全局变量快 5 倍?

f A ) z,什么鬼?这两个办法的功能不是差不多嘛!为毛,你说差 5 倍?

局部变量竟然比全局变量快 5 倍?

CPU Cache

上面的代码之所以功能差不多其实是因为,全局变量 myChars 被 CPU 缓存了,每次咱们查询时不会直接从目标的实例域(目标的实际存储结构)中查询的,而是直接从 CPU 的缓存中查询的,因而才有上面的成果。

为了还原真实的功能(局部变量和全局变量),因而咱们需要运用 volatile 要害来修饰 myI ? i K ? u 2 uChars 全局变量,这样 CPU 就不会缓存此变量了, volM w latile 原本的语义是禁用 CPU 缓存的,咱们修正的代码如下:

import org.openjdk.j4 ) 4 3 p ; y jmh.annot! M mations.*;
import org.openjdk.jmh.runner.Runner;
import oW * 6 , _rg.openjdk.jmh.runner.RunnerExceptiZ I e 8 5on;
import org.openjdk.jmh.runner.options.Options;
import org.oB v T P / 8 _ h ipenjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkM : # B _ 3 F l DMode(Mode.AverageTimec | V E + 5) // 测验完结时刻
@OutputTimeUnit(Tim[ %  $ u . KeUnit.NANOSECONDS)
@Warmup(iterations = 2, tN a k B 2 X E 2 :ime = 1, timeUnit =_ m Z TimF { ! v a A , ceUnit.SECONDS) // 预热 2 轮,] E r 7 * | ?每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测验 5W ( m f z I - U E 轮,每次 3s
@Fork(1) // fork 1 个线程
@Statt 8 # [ ;e(Scope.Thread)M ; + 4 E z w e // 每个测验线程一个实例
public class VarOptimizeTest {
volatile char[] myChars = ("Orac@ M e / O A 9 xle Cloud Infrastructure Low dat_ | g s s * -a networking fees and " +
"automated migrata 5 _ 0 ( D  ] @ion Oracle Cloud Infrastructure platform is built for " +
"enterprises that are looking for higherb 3 | a f = performance compum  Q } ! U 4 6 {ting with easy " +
"migration of their on-premises applications to the Cloud.").toCharArray();
public static void main(String[] args) throws RunnerException {
// 发动基准测验
Options opt = new OptionsBuilder()
.include(VarOptimizeTest.class.getSimpleName()) // 要导入的测验n F C Y 8 } `
.build();
new Runner/ 6 C O L 2 E(opt).run(); // 履行测验
}
@Benchmark
p@ L nublic int globalVarTest() {
int countK R K d : g F = 0;
for (int i = 0; i < myChars.length; i++) {
if (myChar4 , D B P Es[i] == 'ca Y  o H Q H') {
count++;
}
}
return count;
}
@1 / DBenchmark
public int localityVarTest() {
char[] localityChars = myChars;
int count = 0;
for (int i = 0; i < localityChars.length; i++) {
if (loca[  R W # : JlityChars[i] == 'c') {
count++;
}
}
return count;
}
}

终究的测验成Z # y果是:

局部变量竟然比全局变量快 5 倍?

从上面的成果能够看出,局部变量的功能比全局变量的功能快了大约 5.02 倍

至于为什么局部( 1 * M . y 6 :变量会比全局变量快?咱们稍后再说,咱们先来聊聊 CPU 缓存的事。M j y j /

在计算机系统中,CPU 缓存9 – v M(CPU Cache)是用于减少处理器访问内存所需平均时刻的部件。在金字塔式存储系统中它位于自顶向下的第二层,仅次于 CPU 寄存器,如下图所示:

局部变量竟然比全局变量快 5 倍?

CPU 缓存的容量远小于内存,但速度却能够挨近处理器的频率。当处理器宣布内存访问恳U = { x ? = C X –求时,会先检查缓存内是否有恳求数据。假如T 3 5 } X s Q存在(命中),则不经访问内存直接回来该数据;假如不存在(失效),则要先把内存中的相应数据载入缓存,再将其回来l g Z k Y W %处理器。

CPU 缓存能够分为一级缓存(L1),二级缓存(L2),+ f u ;部分高端 CPU 还具有三级缓存(L3),这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递加的。当 CPU 要读取一个数据时,首要从一级缓存中查找,假如没有找到再从二级缓存中查找,假如仍是没有就从三级缓存或内存中查找。

以下是各级缓存和内存, _ & Y j呼应时w c x d . 4 S + w刻的比照图:

局部变量竟然比全局变量快 5 倍?

(图片来源:cenalulu)

从上图能够看出内存的呼应速度要* O a { b比 CPU 缓存慢许多

局部变量竟然比全局变量快 5 倍?

局部变量为什么快?

要理解为什么局部变量会比全局变量快这个问题,咱们只需要运用 javac 把他们编译成字节码就能够找到原因了,编译的字节码如下:

javap -w T 0 # f 7c VarOptimize
正告: 文件 ./VarOptimize.class 不包含类 Va4 ) f ; F Z ] GrOptimize
Compiled from "VarS w COptimize.java"
public class com.example.optm  cimize.VarOptimize {b 7 W | Y ! y }
char[] myChars;
public com.example.Q r V n B Roptimize.VarOptimize();
Code:
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."<K R L k / 1 sinit>":()V
4: aload_0
5: ldc           #7p F ? q : s O                  // String Oracle Cloud Infrastructure Low data networking fees and automated9 8 6 T B  migration OraT b [ t F M : tcle Cloud Infrastructure platformq S 0 x @ [ is built for enterprises that are looking for highK 6 ver performance computing with easy migration of their on-premises applications to the Cloud.
7: invokevirtual9 Y ; , w H B #9                  // Method java/lang/St! I jring.toCharArray:(o v q S | } 6 %)[C
10: putfield      #15                 // Field mt G n % Q 2 - `yChars:[C
1B 5  D # i x I3: return
public static void main(java.lang.String[]);
Code:
0: new           #16                 // class com/example/optimize/VarOptimize
3: dup
4: i] 6 x 1 T nnvokespecial #21                 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #s M % X22                 // Method globalVarTest:()V
12: aload_1
13: invokevirtual #25                 // MeR Q  ; J ]thod lock - 8 a I ! zalityVarTest:()V
16:X W ( return
public void globalVarTest(B ( ! , # C i N);
Code:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: aload_0
6: getfield      #15                 // Field myChars:[C
9: arraylength
10: if_icmp| t & ]ge     33
13: aload_0
14: getfield} S / s N 3 @ -      #15                 // Field myChars:[C
17: iload_2
18: caloB - a K t ~ Lad
19: bipush        99
21: if_icmpn3   6 8 ` ]e     27
24: iinc          1, 1
27: ii- { d Mnc          2, 1
30: goto          4
33: return
public void local:  ) , @ . tityVarTest();
Code:
0: aload_0
1: getfield      #15                 // Field myChars:: F k -[C
4: ast7 l ? p A L I Bore_1
5: icon. r X d V d g gst_0
6: istore_2
7: iconst_0
8: istore_3
9: iload_3
10: aload_1
11: arraylen- & 6 /  K i 1 1gth
12: if_icmpge     32
15: aload_1
16: iload_3
17: caload
18: bipush        99
20: if_icmpne     26
23: iinc          2, 1
26: iinc          3, 1
29: goto          9
32: return
}

其间要害的信息就在 getfield 要害字上,getfield 在此处的语义是从堆上获取变量,从上述的字节码能够看出 globalVarTest 办法在循环的内部每次都通过 getfield 要害字从堆上获取变量,而 localityVarTest 办法并没有运用 getfieldo m j t S害字,而是运用了出栈操作来进行事务处理,而从堆中获取变量比出栈操作要慢许多,因而运用全局变量会比局部变量慢许多。关于堆、栈的内容重视公众号「Java中文社群」我在后面的 JVM 优化的章节会独自解说。

关于缓存

有人或许会说无所谓,横竖运用全局变量会运用 CPU Cache,这样功能也和局部变量差不多,那我就随便f N z * A S ^ I 6用吧,横竖也差不多。

但磊哥的建议是,能用局部变量的绝不运用全局变量,因W 7 v . ? 3 r为 CPU 缓存有以下 3 个问题:

  1. CPU Cachj ( = e te 采用的是 LRU 和 Random 的铲除算法,不常运用的缓存和随机抽取一部分缓存会被删除去d ~ c ` 3 K T A #,假如正好是你用的那个全局变量呢?
  2. CPU Cache 有缓存命中率的问题,也便是有必定的几率^ – 8 ] ;会访问不到缓r s m存;
  3. 部分 CPU 只有两级缓存(L1 和 L2),因而能够运用的空间是有限的。

综上所述,咱们不能把程序的履行功能彻底托付给一个不那么稳定的系统硬件,所以能用局部变量坚决不要运p ; A ` ] | ]用全局变量

要害点:编写适合你的代码,在功能、可读性和实用性之间,找到属于你N x B b * O的平衡点!

总结

本文咱们讲了局部变量的和全局变量的区别,假如运用全局变量会用 getfield 要害字从堆中获取变量^ y p ^ L ^ P i 8,而局部变量则是通过出栈来获取变量的,因为出栈操L y } P ` K v y p作要比堆操作快许多,因而局部变量操作也会比全局变量快许多,所以建议你运用局部变量而不s [ d p d W是全局变量。

高手之间对决,比拼的便是细节。

最后的话

原创不易,觉得有帮助,点个「」让我知道,谢谢你!

重视公众号「Java中文社群」回复“干货”,获取 50 篇原创干货– w y T u w | Top 榜

局部变量竟然比全局变量快 5 倍?