深刻理解Java虚拟机

一、泛型出现以前存在的问题:缓存

全部对象的类型都继承自Object,虚拟机只有到运行时才能知道这个Object具体是什么类型,在编译期是没法检查这个Object是否强制转型成功,会将ClassCaseException的风险转移到程序运行期。多线程

二、泛型的做用:架构

经过泛型,编译器能够在编译阶段发现类型不一致的问题并发

三、泛型擦除:ide

将Java代码编译成Class文件,经过反编译发现泛型都不见了,被替换为原生类型,并插入强制转型的代码。学习

//泛型擦除前
List<String> list = new ArrayList<>();
list.add("hello");
System.out.println(list.get(0));
//泛型擦除后
List list1 = new ArrayList();
list1.add("hello");
System.out.println((String) list1.get(0));
复制代码

二、运行期优化-代码优化

一、公共子表达式消除 在程序基本块中,若是一个表达式E已经被计算过了,下次再次使用的时候,若是表达式的变量值都没发生改变,就能够直接拿表达式的结果来代替E。优化

int x = 1;
int y = 2;
int z = x + y;
int w1 = x + y +2;
//编译器对公共子表达式(x+y)进行消除
int w2 = z + 2;
复制代码

2、方法调用

一、解析

类加载解析节点,将一部分符号引用转化为直接引用。前提是程序运行前有可肯定的调用版本,而且在运行期不可变。这些编译期可知、运行期不可变的方法调用就是解析。线程

二、静态分派和动态分派

一、静态分派:3d

根据静态类型来定位方法的分派叫作静态分派,发生在编译阶段。对象

//父类
public class Parent {
 
}
//子类
public class Son extends Parent {

}
//调用
public class MyTest {

  public void say(Parent parent) {
    System.out.println("parent say");
  }

  public void say(Son son) {
    System.out.println("son say");
  }

  public static void main(String[] args) {
    MyTest myTest = new MyTest();
    //实际类型为Parent
    Parent parent = new Parent();
    //实际类型为Son
    Parent son = new Son();
    myTest.say(parent);
    myTest.say(son);
  }
}
复制代码

返回结果:

Parent为变量的静态类型,Son为实际类型。其中静态类型是在编译期可知的,而实际类型是在运行期肯定下来的,编译器在编译阶段不知道某个对象的实际类型是什么,因此是用静态类型做为断定依据来选择使用哪一个重载版本的,因此选择了say(Parent)做为调用目标。

二、动态分派

public class Parent {

  public void say() {
    System.out.println("parent....");
  }
}

public class Son extends Parent {

  public void say() {
    System.out.println("son....");
  }
}
//调用
Parent parent = new Parent();
Parent son = new Son();
parent.say();
son.say();
复制代码

结果:

虚拟机根据实际类型的不一样来分派方法

基本步骤:

  • 找到栈顶第一个元素所指向的对象实际类型
  • 若是找到对应方法,进行访问权限验证,经过则直接引用,不经过则抛出异常。
  • 不然,按照继承关系从下向上对其各个父类进行方法的搜索和验证过程。
  • 若是没方法,则抛AbstractMethodError异常。

3、并发

一、处理器、缓存、内存的关系

二、主内存、工做内存的关系

  • 线程的工做内存中保存了被该线程使用的变量的主内存的拷贝副本
  • 线程对变量的读取、赋值等操做是在工做内存中进行
  • 不一样线程之间没法直接访问对方工做内存的变量,线程间变量值传递经过主内存来完成

三、内存间的交互操做

将变量从主内存拷贝到工做内存中,将工做内存同步到主内存中。定义了8中操做,每步操做都是原子的、不可再分。

  • lock(锁定):做用于主内存变量,将一个变量标识为一条线程独占的状态
  • unlock(解锁):做用于主内存的变量,把一个处于锁定状态的变量释放出来,才可被其余线程锁定。
  • read(读取):做用于主内存变量,把一个变量的值从主内存传输到线程的工做内存中。
  • load(载入):做用于工做内存的变量,把read操做从主内存获得的变量放到工做内存的变量副本中。
  • 在此我向你们推荐一个架构学习交流圈:830478757  帮助突破瓶颈 提高思惟能力
  • use(使用):做用于工做内存变量,当遇到须要使用变量的值得字节码指令时,会将工做内存的变量传给执行引擎。
  • assign(赋值):做用于工做内存的变量,当遇到给变量赋值的字节码指令时,会把一个从执行引擎接收到的值赋给工做内存的变量。
  • store(存储):做用于工做内存的变量,把工做内存的变量值传递给主内存中。
  • write(写入):做用于主内存的变量,把从工做内存中获得的变量值放入主内存的变量中。

注 :

  • read与load之间、store和write以前能够插入其余指令,会致使多线程操做的同步问题。
  • 一个变量在同时刻只容许一个线程对其进行lock操做。

四、volatile关键字解析

一、可见性:

  • 可见性:一条线程修改变量的值,新值对于其余线程是马上得知的。
  • synchronized和final也能实现可见性。
  • 普通变量:若是线程A修改了普通变量的值,须要向主内存进行回写。另外一条线程B在A回写完成后再从主内存进行读取操做,新变量值才能对线程B可见。
  • 注意:不是全部对volatile变量的写操做都会当即反应到其余线程中。
private volatile static int x;
public static void main(String[] args) {
  for (int i = 0; i < 20; i++) {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        for (int i1 = 0; i1 < 1000; i1++) {
          x++;
        }
      }
    });
    thread.start();
  }
  System.out.println("x="+x);
}
复制代码

最终的结果不是20000,说明volatile修饰的变量也没实现正确并发的目的。

缘由:

x++ 是由多条字节码指令构成的,包括取值,+1,赋值操做,volatile只能保证最后变量取到操做栈顶时该变量的同步性,可是在这以前其余线程是能够修改该变量的值。

二、volatile适用的场景:

  • 运算结果不依赖变量的当前值(例如 x = x+1 不可用)
  • 变量不须要与其余状态变量共同参与不变约束 (x = 1+y 不可用)

三、禁止指令重排序优化 普通变量只能保证执行过程全部依赖赋值结果的地方都能获得正确的结果,不能保证变量赋值的顺序与代码中执行顺序一致,

实现方式:在多线程访问同一内存时,至关于经过一个内存屏障,保证不能把后面的指令重排序到内存屏障以前的位置。

相关文章
相关标签/搜索