美文网首页
java虚拟机学习

java虚拟机学习

作者: echoSuny | 来源:发表于2020-03-25 17:46 被阅读0次

    java虚拟机,也就是我们通常叫的JVM。为什么我们要了解java虚拟机呢?总结起来有三点:
    1:写出更好更健壮的java程序
    2:提高java应用的性能,排除问题
    3:面试(最实际的问题😄)
    --------------------------------------这是分割线--------------------------------------

    JVM运行的时候会把它管理的内存划分为不同的数据区。如图所示:


    虚拟机内存分配图

    其中编号为3,4,5的区域为线程私有的,线程私有意味着启动一个线程就会单独分配一块这样的区域,跟随线程产生和消亡 ,不需要过多考虑内存回收问题。剩余的1和2是共享区域。不管有多少线程,这些区域就只有一份。下面依次来解释这5个区域分别的作用:
    (1)方法区:
    作用:存放类信息,常量,静态变量以及即时编译期编译后的代码。
    (2)堆:
    作用:存放对象实例(几乎所有的对象),数组
    堆是用来存放对象的,无论是成员变量,局部变量还是类变量,它们指向的对象都存储在堆内存中。众所周知,一个对象的产生过程是用过new关键字实现的,但是在虚拟机内部却分成了大概5个步骤:检查加载—>分配内存—>内存空间初始化—>设置—>对象初始化。检查加载就是由类加载器加载到内存中;内存分配首先要检查有没有足够的内存进行分配,分为指针碰撞和空闲列表。如果是多线程的情况下,还要考虑并发安全问题。可以使用CAS机制或者本地线程分配缓冲(存在于栈帧中);内存空间初始化就是一些变量,比如声明一个变量int i,如果不赋予初始值的话,就会默认的赋值0,就是在这一步之中实现的;设置主要就是在对象头中设置一些类的属性,比如类的HashCode,年龄信息,属于哪个类,锁信息等等;最后就是真正的初始化。


    访问对象方式
    上图展示的是两种对象的访问方式。左边为句柄池方式,右边为直接引用。直接引用缺少了中间的句柄池,所以直接引用的方式访问的速度更快。而句柄池的方便之处在于方便垃圾回收。假设右边的对象实例数据被回收了,直接操作句柄池就可以了,而不用改动引用。
    我们都知道对象是分配在堆内存中的,那么是不是所有的对象都分配在堆中呢?显然不是,如果是的话上面的括号中也不用标注是几乎所有。 其实有时候对象是可以在栈上面分配的。这是因为虚拟机为了提升执行速度,使用了“逃逸分析技术”。逃逸分析技术是一种优化技术,而不是直接的优化手段。只是为了其他的优化手段提供依据的分析技术。逃逸分析技术的基本行为是分析对象的动态作用域。牵涉的JVM参数如下:

    -XX:+DoEscapeAnalysis 启用逃逸分析。默认是打开的。冒号后面换成-号,则是关闭
    -XX:+ElininateAllocations 标量替换。默认是打开的。冒号后面换成-号,则是关闭
    -XX:+UseTLAB 本地线程分配缓冲。默认是打开的。冒号后面换成-号,则是关闭
    -XX:+UseTLAB需要是开启的,然后会在栈帧中分配一个缓冲区域,不然对象是没有地方可以分配。否则的话即使-XX:+DoEscapeAnalysis是打开的也没有用。另外-XX:+ElininateAllocations也是需要打开的,不然同样无效。

    public void test(){
      User user = new User();
      user.setName("jack")
    }
    

    所谓逃逸分析就是在方法中的,也就是上面例子中的user对象,如果在这个方法的外面没有使用到,也就是对象在这个方法的作用域之内,就会使用逃逸分析,会在栈中的分配缓冲区域中存储。但是只会存放一个,即时你使用for循环去生成很多个。可以预见的是如果没有这项优化技术,循环生成N多个对象需要频繁的进行GC,而使用了逃逸分析之后就不需要GC了,因为此时的对象是在栈当中,而栈又是属于线形私有的,那么线程死亡了,所有的就都消失了。
    (3)虚拟机栈:
    作用:存储当前线程运行方法所需要的数据(八种基本类型以及对象的引用 ),指令以及返回地址
    了解虚拟机栈之前需要先了解一下栈(stack)。栈是一种数据结构,出口和入口只有一个,特点是先进后出。可以想象成一个水杯(假设水是不流动的)。往杯子里倒水的行为叫入栈,从杯子里往外倒水则叫出栈。肯定最先倒入水杯的水会被压在最下面,那么往外倒水的时候,最先入栈的肯定最后出栈。

    public void A(){
            System.out.println("A方法执行之前");
            B();
            System.out.println("A方法执行之后");
        }
    
        public void B(){
            System.out.println("B方法执行之前");
            C();
            System.out.println("B方法执行之后");
        }
    
        public void C(){
            System.out.println("C方法执行之前");
            // do something
            System.out.println("C方法执行之后");
        }
    // 输出结果
    A方法执行之前
    B方法执行之前
    C方法执行之前
    C方法执行之后
    B方法执行之后
    A方法执行之后
    

    可以看到A方法最先入栈,却最后出栈。这也符合了栈的特点。这也解释了为什么叫虚拟机栈。
    在虚拟机栈中,每一个方法在执行的同时都会创建一个“栈帧”。 栈帧可以划分为:局部变量表,操作数栈,动态连接,返回地址等等。

    // 一个普通的类
    public class Test {
        public static void main(String[] args) {
            Test test = new Test();
            int index = 0;
            test.helper(index);
        }
    
        private void helper(int index) {
            String str = "hello";
            System.out.print(str + index);
        }
    }
    
    // 这是反编译之后的部分代码
    {
      public com.mzw.myrouter.Test();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 4: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/mzw/myrouter/Test;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=3, args_size=1
             0: new           #2                  // class com/mzw/myrouter/Test
             3: dup
             4: invokespecial #3                  // Method "<init>":()V
             7: astore_1
             8: iconst_0
             9: istore_2
            10: aload_1
            11: iload_2
            12: invokespecial #4                  // Method helper:(I)V
            15: return
          LineNumberTable:
            line 7: 0
            line 8: 8
            line 9: 10
            line 10: 15
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      16     0  args   [Ljava/lang/String;
                8       8     1  test   Lcom/mzw/myrouter/Test;
               10       6     2 index   I
    }
    SourceFile: "Test.java"
    
    虚拟机栈分配

    图中右侧的行号为0的操作是new,对应的就是Test test = new Test()这一行代码。总之所有的代码会按照上面的行号0,3,4,7等等来不断的进行入栈出栈直到走完方法。图中的返回地址对应的就是行号15处的return。本地局部变量表则对应的就是方法的参数args,index。动态连接则是对应的多态。
    (4)本地方法栈:
    作用:保存native方法的信息
    当JVM的线程调用的native方法后,JVM不再为其在虚拟机栈当中创建栈帧,JVM只是简单的动态连接并直接调用native方法。
    (5)程序计数器:
    作用:指向当前线程正在执行的字节码指令的地址或者行号。

    // 线程A
        public void inc(int i){
            i++;
            System.out.print(i);
        }
    
        // 线程B
        public void dec(int i){
            i--;
            System.out.print(i);
        }
    

    假设上面的代码分别跑在两个不同的线程A和线程B。我们都知道线程能够执行是需要获取到CPU执行时间片才能够运行方法。还需要知道的一点就是++操作需要分三步:首先获取i的值,然后进行+1操作,最后把值重新赋给i。--操作也是如此。假设线程A刚获取到了i的值的时刻,线程切换了,线程B开始执行并执行了减1操作,随后又切换到线程A了。假如没有程序计数器的话,线程A是无法知道自己在切换到线程B之前执行到了三步中的哪一步的,那么切回来的时候,就无法进行下一步了,因为线程A不知道自己执行到哪一步了。所以程序计数器的作用就是确保多线程情况下程序的正常执行。

    相关文章

      网友评论

          本文标题:java虚拟机学习

          本文链接:https://www.haomeiwen.com/subject/eykzyhtx.html