java虚拟机学习

2020-03-25  本文已影响0人  echoSuny

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不知道自己执行到哪一步了。所以程序计数器的作用就是确保多线程情况下程序的正常执行。

上一篇下一篇

猜你喜欢

热点阅读