ReentrantLock、AQS底层实现原理

ReentrantLock介绍:

在单线程情况下交替执行与队列无关,因为使用JDK级别解决同步问题。

可重入锁: 线程已经获取某个共享资源的锁之后,释放锁之前,当前线程还可以再次对于这个共享资源获取锁(锁计数器[state+1])。

在jdk1.6之前->ReentrantLock和Synchronized的区别:

ReentrantLock:一部分在JDK级别解决,一部分在OS(操作系统)的Api执行,可使用Condition(条件)锁

Synchronized【Jvm内置锁】:所有锁都在OS(操作系统)Api执行

ReentrantLock类图解析:

  • ReentrantLock 实现 Lock 和 Serializable 接口
  • ReentrantLock 内部类 Sync、NonfairSync 和 FairSync 类

Sync:继承 AbstractQueuedSynchronizer 抽象类
NonfairSync(非公平锁)与 FairSync(公平锁) : 继承 Sync 抽象类

java.util.concurrent(j.u.c) 基于AQS实现:

AQS核心三大板块:

  • CAS
  • 自旋
  • LockSupport(park,unpark)

AQS核心源码分析:

https://www.processon.com/view/link/5e841dc6e4b0a2d87025a5af

文件可能比较大,加载的慢,请耐心等候……

Synchronized底层实现原理

synchronized介绍:

synchronized 关键字在多线程环境下作为线程安全的同步锁

synchronized作用:

1.同步代码块(当前对象锁[this] 或 自定义对象锁) 
2.同步静态方法(当前类的Class实例,Class数据存在永久代中,该类全局锁)
3.同步静态方法(当前对象锁)

同步代码块:

//Java代码
public void syncronizedTest(){
    synchronized (this){
        System.out.println("hello world");
    }
}

//代码反汇编 
JVM使用monitorentermonitorexit两个指令实现同步,即JVM为代码块的前后真正生成了两个字节码指令来实现同步功能。
monitorenter/monitorexit:
    每个对象都会与一个monitor(监视器锁)相关联,当某个monitor被拥有后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor

    1.每个monitor维护一个记录拥有次数的计数器,未被拥有monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为1
    2.当同一个线程再次获得该monitor的时候,计数器再次自增     
    3.当不同线程想要获得该monitor的时候,就会被阻塞
    4.当同一个线程释放monitor(执行monitorexit指令)时,计数器自减。当计数器减为0时,monitor将被释放,其他线程可获得该monitor    
  

同步方法(静态方法,非静态方法):

//静态方法
public static synchronized void syncronizedStaticTest(){
    System.out.println("hello world");
} 

//非静态方法
public synchronized void syncronizedTest(){
    System.out.println("hello world");
}

//静态方法与非静态方法反汇编

JVM使用acc_synchronized标识来实现,即JVM通过在方法访问标识符(flags)中加入acc_synchronized来实现同步功能

    同步方法是隐式的,一个同步方法会在运行时常量池中的method_info结构体中存放acc_synchronized标识符。      当一个线程访问方法时,会去检查是否存在acc_synchronized标识符,如果存在,则要先获得对应的monitor锁,然后执行方法,当方法执行结束(正常return或抛出异常)都会释放对应的monitor锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。

总结:

同步方法和同步代码块底层都是通过monitor来实现同步的。

同步方法和同步代码块的区别:
    同步方法是通过方法中的access_flags中设置acc_synchronized标志来实现,同步代码块是通过monitorenter和monitorexit来实现。每个对象都与monitor相关联,而monitor可以被线程拥有或释放。

Java内存模型-volatile有序性

/**
 * @Description:Java内存模型-有序性
 * @Author:chenxi
 * @Date:2020/3/22
 **/
public class JMMOrderTest {

    private static int a, b = 0;
    private static int x, y = 0;

    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        for (; ; ) {
            count++;a = 0;b = 0;
            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    int block = 0;
                    do{
                        block++;
                    }while (block < 20000);

                    a = 1;
                    x = b;
                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });

            thread_1.start();
            thread_2.start();
            thread_1.join();
            thread_2.join();

            if (x == 0 && y == 0) {
                System.err.println("execute count = " + count + "  a-b-x-y: " + a + "-" + b + "-" + x + "-" + y);
                System.err.println("x and y is 0");
                break;
            } else {
                System.out.println("execute count = " + count + "  a-b-x-y: " + a + "-" + b + "-" + x + "-" + y);
            }
        }
    }
}

假设会出现以下场景:

假设Thread_1先启动执行结束、Thread_2后启动执行、Thread_2后启动执行
结果为:a = 1;b = 1;x = 0;y = 1;
假设Thread_2先启动执行结束、Thread_1后执行
结果为:a = 1;b = 1;x = 1;y = 0;
假设Thread_1先启动执行,在代码块中a = 1执行过程中阻塞了,Thread_2执行时在Thread_1执行x = b之前先执行b = 1,那么x,y = 1
执行顺序如下
   Thread_1:
      a = 1;
  Thread_2:
      b = 1; 
  Thread_1:
      x = b; // x = b = 1;
  Thread_2:
      y = a; // y = a = 1;

控制台输出如下:

......
execute count = 1689  a-b-x-y: 1-1-0-1
execute count = 1690  a-b-x-y: 1-1-1-0
execute count = 1690  a-b-x-y: 1-1-1-1
execute count = 1690  a-b-x-y: 1-1-0-0 
x and y is 0

那么为什么会出现 1-1-0-0 ???

CPU会进行指令重排:

在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排序

            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    int block = 0;
                    do{
                        block++;
                    }while (block < 20000);

                     x = b; //改变执行顺序
                     a = 1; //改变执行顺序

                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    y = a;  //改变执行顺序 
                    b = 1;  //改变执行顺序 
                }
            });
假设Thread_1先启动执行,在代码块中x = b执行过程中阻塞了,Thread_2执行时在Thread_1执行a = 1之前先执行y = a,那么x,y = 0
执行顺序如下
  Thread_1:
      x = b; // x = b = 0;
  Thread_2:
      y = a; // y = a = 0; 
   Thread_1:
      a = 1;
  Thread_2:
      b = 1; 

此时,我们使用volatile来修饰x,y变量:

 private static volatile int x, y = 0; 

执行程序,x,y = 0的结果将不会再出现

因为volatile在写后面加上了storeload(内存屏障)

volatile如何防止指令重排:

volatile关键字通过”内存屏障“来防止指令被重排序

JMM采取保守策略,基于保守策略JMM内存屏障插入策略:

  • 在每个volatile操作的前面插入一个StoreStore屏障
  • 在每个volatile操作的后面插入一个StoreLoad屏障
  • 在每个volatile操作的后面插入一个LoadLoad屏障
  • 在每个volatile操作的后面插入一个LoadStore屏障

代码分析:

            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    ......//此处代码省略 

                    a = 1; //volatile写后增加storeLoad(内存屏障)
                    x = b; //先volatile读,再普通写
                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1; //volatile写后增加storeLoad(内存屏障) 
                    y = a; //先volatile读,再普通写 
                }
            });

自定义内存屏障:

  private static int x, y = 0;  

  ......//此处代码省略

            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    ......//此处代码省略

                    a = 1;
                    //手动增加内存屏障
                    getUnsafe().storeFence();
                    x = b;
                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    //手动增加内存屏障
                    getUnsafe().storeFence(); 
                    y = a;
                }
            }); 

private static Unsafe getUnsafe() {
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    }catch (Exception e){
        e.printStackTrace();
    }
    return null;
}

openJdk hotspot下 orderAccess_linux_x86.inline.hpp 源码解析:

openJdk下载地址:https://pan.baidu.com/s/13OvQIj5RrBWMVBQ8ENyV_w
提取码:e475

path: openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\os_cpu\linux_x86\vm下 orderAccess_linux_x86.inline.hpp 文件

Java内存模型-volatile原子性

/**
 * @Description:Java内存模型-原子性
 * @Author:chenxi
 * @Date:2020/3/22
 **/
public class JMMAtomicityTest {

    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter++;
                }
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);//three result: 9202 9560 9861
    }
}

以上程序运行三次返回结果如下:9202 -> 9560 -> 9861

此时2个线程分别做了counter++,实际写入主内存的值却只有1。

Java内存模型-volatile可见性

Java内存模型:

        Java线程内存模型与CPU缓存模型类似,基于CPU缓存模型建立,Java线程内存模型是标准化的,用于屏蔽各种硬件和操作系统的内存访问差异。

主要分为三大模块: 线程、工作内存、主内存

Java内存模型数据原子操作:
read(读取) 从主内存读取数据
load(载入) 将主内存读取到的数据写入工作内存
use(使用) 从工作内存读取数据来计算
assign(赋值) 将计算好的值重新赋值到工作内存中
store(存储) 将工作内存数据写入主内存
write(写入) 将store过去的变量赋值给主内存中的变量
lock(锁定) 将主内存变量加锁,标识为线程独占状态
unlock(解锁) 将主内存变量解锁,解锁后其他线程可以锁定该变量

Volatile:

        volatile是Java虚拟机提供的最轻量级的同步机制。
        volatile保证可见性(所有线程都能看到共享的最新状态)与有序性(禁止指令重排序优化),但是不保证原子性,保证原子性需借助synchronizedLock锁机制
Volatile如何保证内存可见性:

        read(读取)、load(载入)、use(使用)动作必须连续出现。
        assign(赋值)、store(存储)、write(写入)动作必须连续出现volatile关键字使变量的读、写具有了原子性。然而这种原子性仅限于变量(包括引用)基本类型的自增(如count++)等操作不是原子的。
        ②对象的任何非原子成员调用(包括成员变量和成员方法)不是原子的

volatile内存可见性实现原理:

        底层实现主要通过汇编lock前缀指令在汇编lock指令前后加上内存屏障),它会锁定这块内存区域的缓存并回写到主内存,此操作被称为”缓存锁定“,缓存一致性机制会阻止同时修改被两个以上的处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
/**
* @Description:Java内存模型-可见性
* @Author:chenxi
* @Date:2020/3/21
**/
public class JMMVisibilityTest {

    private static volatile boolean initFlag = false;

    public static void writeInitFlag(){ 
        System.out.println("start write initFlag...");
        initFlag = true;
        System.out.println("end write initFlag...");
    }

    public static void loadInitFlag(){
        while (!initFlag) {
        }
        System.out.println("current thread " +         Thread.currentThread().getName() + " sniffing initFlag value change");
    }

    public static void main(String[] args) {
        Thread thread_1 = new Thread(()->{
            loadInitFlag();
        }, "thread-1");

        Thread thread_2 = new Thread(()->{
            writeInitFlag();
        }, "thread-2");
        
        thread_1.start();

        try {
            Thread.sleep(1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        thread_2.start();
    }
}

Java内存模型缓存不一致性底层机制:

Bus总线加锁(性能太低):

        cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,其他cpu没法去读取或写入这个数据,造成阻塞,直到这个cpu使用完数据释放锁之后其他cpu才能读取该数据。总线加锁使CPU性能发挥不出来。

Java代码执行底层汇编查看vm配置:

-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*JMMVisibilityTest.writeInitFlag

MESI缓存一致性协议:

        多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会立马同步到主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效,在下一轮指令周期从主内存中重新load数据。
MESI协议中状态:

M:被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主内存中的数据不一致,该缓存行的内存需要在未来某个时间点(允许其他CPU读取主内存中相应数据之前)写回(write back)主内存中。 当被写回主内存后,该缓存行的状态会变为独享(Exclusive)状态
E:独享的(Exclusive) 该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主内存中数据一致,有其他CPU读取该内存时变成共享(Shared)状态。 当CPU修改该缓存行内容时,该缓存行状态变为被修改(Modified)状态
S:共享的(Shared) 该缓存可能被多个CPU缓存,并且各个缓存中的数据与主内存数据一致(clean)。 当有一个CPU修改该缓存行时,其他CPU中该缓存行内容被视为无效(Invalid)状态
I:无效的(Invalid) 该缓存无效(可能被其他CPU修改了该缓存行) 使用(use)时需重新从主内存中读取(read)数据
        注意:变量所属内存区域必须是在缓存行,不能超过缓存行大小(缓存行大小一般64字节,较大的为128字节),变量如果超过缓存行大小,缓存一致性协议无法工作,就会使用总线锁

MySql索引数据结构以及存储引擎

MySql索引数据结构:

1.二叉树
2.红黑树
3.Hash表
4.B-Tree
二叉树(Binary Search Tree):

存储结构:每个结点最多有两个子树的树结构。子树通常被称为“左子树”(left subtree)和“右子树”(right subtree)。

    1.若左子树不空,则左子树上所有结点的值均小于它的根结点的值
    2.若右子树不空,则右子树上所有结点的值均大于它的根结点的值
    3.左、右子树也分别为二叉排序树
    4.没有键值相等的结点
 || ||

缺点:查询次数多,I/O操作频繁,需从第一个节点往后进行比较,查询效率慢

红黑树(RED/BLACK TREE):

存储结构:二叉查找树,也称为平衡二叉树。

相比于二叉树:查询次数减少,比二叉树查询次数少一半

缺点:红黑树的高度在数据量大时不可控,查询叶子节点时,I/O操作次数也多。

Hash表(Hash Table):

存储结构:维护一个Hash表,以列值计算Hash值,把Hash值和磁盘文件的地址维护到Hash表中,查找时只需进行一次I/O操作。

缺点:范围查询时需全表扫描。

B-TREE:

存储结构:多路平衡树,设定树的高度。

1.叶节点具有相同的深度
2.叶节点的指针为空
3.节点中的数据索引从左到右递增排列

缺点:区间查找问题

B+TREE(B-TREE变种):

存储结构:Mysql使用B+Tree实现索引

1.非叶子节点不存储data,只存储索引,可以放更多索引
2.叶子节点不存储指针
3.顺序访问指针,提高区间访问的性能

MyISAM存储引擎:

索引文件和数据文件分离(非聚集索引)。

非聚集索引:存放引用地址

.FRM .MYD 存储索引表字段值 .MYI存储索引数据值对应引用地址

InnoDB存储引擎:

表数据文件本身按B+Tree组织的一个索引文件

聚集索引:叶节点包含完整的数据记录

.FRM .ibd存储索引数据

JVM栈帧内部结构-方法返回地址

方法返回地址(Return Address):

存放调用该方法的PC寄存器的值。

方法结束方式:

1) 正常结束
2) 出现未处理异常,非正常退出(通过异常完成出口退出的不会给他的上层调用者生产任何的返回值

无论通过哪种方式退出,在方法退出后到该方法被调用的位置,方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令地址,而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

Return:

        当一个方法开始执行后,执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,称正常完成出口

        字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short、int类型时使用)lreturn、freturn、dreturn、areturn(return指令供声明void的方法、实例初始化方法、类和接口的初始化方法使用)。

JVM栈帧内部结构-动态链接

动态链接(或运行时常量池的方法引用):

        每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令

        在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在class文件的常量池里。

比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换位调用方法的直接引用

方法的调用:

在Jvm中,将符号引用转换位调用方法的直接引用,与方法的绑定机制相关。

静态链接:

        当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变。

        将调用方法的符号引用转换为直接引用的过程称为静态链接

动态链接:

        被调用的目标方法在编译期无法被确定下来,只能够在程序运行期将方法的符号引用转换为直接引用,这种引用转换的过程具备动态性,称为动态链接

方法的绑定机制分为早期绑定(Early Binding)晚期绑定(Late Bingind)。绑定是一个字段、方法或类在符号引用被替换为直接引用的过程。

早期绑定:

被调用的目标方法在编译期可知,且运行保持不变。

晚期绑定:

        被调用方法在编译期无法被确定下来,只能够在程序运行期根据实际类型绑定相关的方法。

虚方法与非虚方法:

        如果方法在编译期就确定具体调用版本,这个版本在运行期间是不可变的。

        静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其它方法都为虚方法

虚拟机中方法调用指令:

普通调用指令:

1) invokestatic:调用静态方法,解析阶段确定唯一方法版本。
2) invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本。
3) invokevirtual:调用所有虚方法。
4) invokeinterface:调用接口方法。

动态调用指令:

5) invokedynamic:动态解析出需要调用的方法,然后执行

invokeinterface:固化在虚拟机内部,方法的调用执行不可人为干预。
invokedynamic:指令支持用户确定方法版本。
invokestatic:指令和invokespecial指令调用的方法称为非虚方法,其余(final修饰除外)称为虚方法

JVM栈帧内部结构-操作数栈

基本概念:

        操作数栈是基于数组的方式实现的。

        在方法执行过程中,根据字节指令,往栈中写入(入栈/push)数据或提取(出栈/pop)数据。

        某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们后再把结果压入栈。(比如:复制,交换,求和等操作)

        操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时存储空间。

        操作数栈是Jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

        每一个操作数栈都会拥有一个明确的栈深度用于存储数据,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
        栈中任何一个元素都是可以任意的Java数据类型
            1) 32bit的类型占用一个单位深度。
            2) 64bit的类型占用两个栈单位深度。
        操作数栈并非采用访问索引的方式进行数据访问的,只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

        如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
        
        操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

操作数栈字节码指令执行分析:

栈顶缓存(Top-Of-Stack Cashing)技术

        由于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作时,需要更多的入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数。

        操作数是存储在内存中的,频繁执行内存读/写操作影响执行速度,HotSpot提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

JVM栈帧内部结构-局部变量表

栈帧内部结构:

1) 局部变量表(Local Variables)
2) 操作数栈(Operand Stack)或表达式栈
3) 动态链接(Dynamic Linking)或指向运行时常量池的方法引用
4) 方法返回地址(Return Address)或方法正常退出或异常退出的定义
5) 一些附加信息

局部变量表(Local Variables):

    局部变量表被称之为局部变量数组或本地变量表。

    定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用(Reference)以及Return Address类型。

    由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题。

    局部变量表所需的容量大小是在编译器确定下来的,并保存在方法Code属性的Maximum local variables数据项中,在方法运行期间是不会改变局部变量表的大小。

    方法嵌套调用的次数由栈的大小决定,栈越大方法调用的次数越多。

    局部变量表中的变量只在当前方法调用中有效,在执行方法时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当前方法调用结束后,随着方法栈帧的销毁,局部变量表也随之销毁

Slot(槽)的理解:

    参数值的存放是在局部变量数组的index 0开始的,到数组长度-1的索引结束。

    局部变量表的基本存储单猥是Slot(变量槽)

    局部变量表中存放编译器可知的各种基本数据类型(8种),引用类型(reference),Return Address类型的变量。

    在局部变量表中,32位以内的类型占用一个Slot(包括Return Address类型),64位类型(long和double)占用两个Slot
    1) byte,short,char在存储前被转换位int,boolean也被转换位int(0表示false,非0表示true)。
    2) long和double占据两个Slot




    Java会位局部变量表中的每一个Slot都会分配一个访问索引,通过这个索引可以访问到局部变量表中指定的局部变量值。
    当一个实例方法被调用的时候,它的方法参数和方法体内部的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
    如果需要访问局部变量表的一个64bit的局部变量值时,只需要使用前一个索引即可。
    如果当前方法是由构造方法或实例方法创建的(非静态方法),那么该对象引用this将会存放在index为0的Slot处,其余参数按照参数表顺序继续排列。




Slot的重复利用:
    栈帧的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量就很有可能会重复用过期局部变量的槽位,达到节省资源的目的

静态变量与局部变量对比:

变量分类(按数据类型区分):1) 基本数据类型   2) 引用数据类型
在类中声明位置区分:
    1) 成员变量:在使用前,都经历过默认初始化赋值,
       类变量:在链接(Linking)的准备(Prepare)阶段,给类变量默认赋值,初始化(initialization)阶段给类变量显式赋值,即静态代码块赋值
    2) 局部变量:在使用前,必须进行显式赋值,否则编译不通过

局部变量表与GC垃圾回收关系:

 

在方法执行时,虚拟机使用局部变量表完成方法的传递。

      局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收