个人学习

19. java虚拟机总结-JVM 内存管理 (三)

2020-04-12  本文已影响0人  任振铭

JVM 内存区域划分

1.为什么进行内存区域划分?

Java自动内存管理机制是它和C++的区别所在。C++是手动内存管理,内存的释放需要开发者自己处理,Java的自动内存管理机制使程序写起来方便很多。但是为了管理这些快速的内存申请释放操作,就必须引入一个池子来延迟这些内存区域的回收操作,这个池子,就是堆,Java的垃圾回收就是针对的堆区。

--tip--Java内存布局一直在调整。比如,Java8及之后的版本,彻底移除了持久代,而使用Metaspace来进行替代。这也表示着 -XX:PermSize 和 -XX:MaxPermSize 等参数调优,已经没有了意义

2.java内存分区

java内存分区.png

针对上图:
1.JVM堆中的数据是共享的,是占用内存最大的一块区域。
2.可以执行字节码的模块叫作执行引擎。执行引擎在线程切换时怎么恢复?依靠的就是程序计数器。
3.JVM 的内存划分与多线程是息息相关的。像我们程序中运行时用到的栈,以及本地方法栈,它们的维度都是线程。
4.本地内存包含元数据区和一些直接内存。

可见JVM内存分为这几个区域:

a.虚拟机栈

Java 虚拟机栈是基于线程的。哪怕你只有一个 main() 方法,也是以线程的方式运行的

首先要知道,既然是栈,那就满足先进后出的顺序。虚拟机栈用于存储当前线程执行方法需要的数据,指令,返回地址,并执行Java方法。栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,也就是一个方法对应一个栈帧,栈帧创建后会入栈,一旦完成相应的调用,则出栈。Java程序的运行就是不断的有栈帧入栈和出栈的过程,所有的栈帧都出栈后,线程也就结束了。

每个栈帧包含局部变量表,操作数栈,动态连接,返回地址四个区域

线程和栈帧.png

我们以这段代码为例来分析Java虚拟机栈

public class TestJavaStack {
    //静态变量
    static String s = "hello";
    //常量
    final int a = 23;
    //普通成员变量
    float v = 12.0f;
    public int method1(){
        Object o = new Object();
        //int类型
        int a = 5 + 10;      //验证直接相加在编译阶段已合并完结果
        int b = a + 3;        //探究变量与常量的相加过程
        b = b + 6;             //验证int不在-1~5,在-128~127范围的指令是bipush
        b = b + 128;         //验证int不在-128~127,在-32768~32767范围的指令是sipush
        b = b + 32768;     //验证int不在-32768~32767,在-2147483648~2147483647范围的指令是ldc(ldc:从常量池取并压栈,所以这个范围的int是存在常量池)
        //short                  //验证byte、short、char在操作数栈压栈前均会转为int
        short a_s = 5 + 10;
        short b_s = (short)(a_s + 3);
        return b;
    }
    public static void main(String[] args) {
        TestJavaStack stack = new TestJavaStack();
        stack.method1();
    }
}

通过javap -c TestJavaStack.class 命令将class文件反编译

public class TestJavaStack {
  static java.lang.String s;

  final int a;

  float v;

  public TestJavaStack();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        23
       7: putfield      #2                  // Field a:I
      10: aload_0
      11: ldc           #3                  // float 12.0f
      13: putfield      #4                  // Field v:F
      16: return

  public int method1();
    Code:
       0: new           #5                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: astore_1

       /*** (int a = 5 + 10)***/
       //将15压入操作数栈的栈顶(编译过程中5+10合并成15,并且由于15在-128-127范围,即用bipush)  `压栈`
       8: bipush        15
      //从栈顶弹出并压入局部变量表访问索引为2的Slot `弹栈入局部变量表`  弹栈运算                                                            
      10: istore_2
      //将局部变量表中访问索引为2的Slot重新压入栈顶 `局部变量表入栈`   结果入栈                                                         
      11: iload_2

      /****(int b = a + 3)****/
      //将数值3压入操作数栈的栈顶(范围-1~5,即用指令iconst)   ` 压栈`   
      12: iconst_3
      //将栈顶的前两个弹出并进行加法运算后将结果重新压入栈顶                                                    
      13: iadd
      //从栈顶弹出并压入局部变量表访问索引为3的Slot
      14: istore_3
      
      /****(b = b + 6)****/
      //将局部变量表中访问索引为3的Slot重新压入栈顶          
      15: iload_3
      //将6压入操作数栈的栈顶(在-128-127范围,用bipush指令) 
      16: bipush        6
      //将栈顶的前两个弹出并进行加法运算后将结果重新压入栈顶        
      18: iadd
      //从栈顶弹出并压入局部变量表访问索引为3的Slot
      19: istore_3

      /****(b = b + 128)****/
      //将局部变量表中访问索引为3的Slot重新压入操作数栈栈顶,拿到b的值
      20: iload_3
      //将128压入操作数栈的栈顶(在-32768~32767范围,用sipush指令) 
      21: sipush        128
      //将栈顶两个弹出相加后再将结果重新入栈
      24: iadd
      //从栈顶弹出并压入局部变量表访问索引为3的Slot
      25: istore_3

      /****(b = b + 32768)****/
      //将局部变量表访问索引为3的Slot重新压入栈顶,(拿到了最新的b的值)
      26: iload_3
      //将32768 压入操作数的栈顶(在-2147483648~2147483647范围,用ldc指令) 
      27: ldc           #6                  // int 32768
      //将栈顶两个弹出相加后重新将结果入栈顶
      29: iadd
      //从栈顶弹出并压入局部变量表访问索引为3的Slot
      30: istore_3

      /**** 验证了short、byte、char压栈前都会转为int****/
      31: bipush        15
      33: istore        4
      35: iload         4
      37: iconst_3
      38: iadd
      39: i2s
      40: istore        5
      41: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #19                 // class TestJavaStack
       3: dup
       4: invokespecial #20                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #21                 // Method method1:()I
      12: pop
      13: return

  static {};
    Code:
       0: ldc           #22                 // String hello
       2: putstatic     #23                 // Field s:Ljava/lang/String;
       5: return
}
局部变量表

局部变量表采用访问索引的方式来进行数据访问的


7305851-a81763719c0e214b.png

一个方法对应一个栈帧,局部变量表用于存放方法中定义的局部变量。JavaStack类中,局部变量表中存储的首先是this,也就是当前对象,然后是Object类型tech13和基本数据类型zhifubao,weixin,对于Object类型的tech13,要知道栈中存储的只是引用,对象是在堆中的。

操作数栈

操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区,通过标准的入栈和出栈操作来完成一次数据访问(对于byte、short和char类型的值在入栈之前,会被转换为int类型)。它是数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈.

iconst: 用于int类型-1~5范围内数据压栈
bipush: 用于int类型-128-127范围内数据压栈
sipush: 用于int类型-32768~32767范围内数据压栈
ldc: 用于int类型--2147483648~2147483647范围内数据压栈(直接存在常量池)

动态连接

指的就是多态相关的东西

返回地址

方法的返回指令


返回地址.png
b.本地方法栈

本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域,这并不影响我们对 JVM 的了解。

c.程序计数器

指的就是代码反编译之后,操作码前边的数字,用于记录当前线程执行到的位置。为什么需要程序计数器?因为线程在获取 CPU 时间片上是不可预知的,需要有一个地方,对线程正在运行的点位进行缓冲记录,以便在获取 CPU 时间片时能够快速恢复。程序计数器指向当前线程正在执行的字节码指令的地址(行号),就是记录执行的位置,当线程切换时等再切换回来才能从正确的点继续执行

public static void main(java.lang.String[]);
    Code:
       0: new           #19                 // class TestJavaStack
       3: dup
       4: invokespecial #20                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #21                 // Method method1:()I
      12: pop
      13: return
d.堆

对大多数应用而言,Java 堆是虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一作用就是存放对象实例,几乎所有的对象实例都是在这里分配的(不绝对,在虚拟机的优化策略下,也会存在栈上分配、标量替换的情况,后面的章节会详细介绍)。Java 堆是 GC 回收的主要区域,因此很多时候也被称为 GC 堆。从内存回收的角度看,Java 堆还可以被细分为新生代和老年代;再细一点新生代还可以被划分为 Eden Space、From Survivor Space、To Survivor Space。从内存回收的角度看,线程共享的 Java 堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。

Java 的对象可以分为基本数据类型和普通对象。

对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用

对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。我们上面提到,每个线程拥有一个虚拟机栈。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

注意,像 int[] 数组这样的内容,是在堆上分配的。数组并不是基本数据类型。

e.元空间

关于元空间,有一个非常高频的面试题:“为什么有 Metaspace 区域?它有什么问题?”

说到这里,你应该回想一下类与对象的区别。对象是一个活生生的个体,可以参与到程序的运行中;类更像是一个模版,定义了一系列属性和操作。那么你可以设想一下。我们前面生成的 A.class,是放在 JVM 的哪个区域的?在 Java 8 之前,这些类的信息是放在一个叫 Perm 区(永久代,在堆中)的内存里面的。这个区域有大小限制,很容易造成 JVM 内存溢出,从而造成 JVM 崩溃。

Perm 区在 Java 8 中已经被彻底废除,取而代之的是 Metaspace。原来的 Perm 区是在堆上的,现在的元空间是在非堆上的,这是背景。关于它们的对比,可以看下这张图。


Cgq2xl4VrjaAIlgaAAJKReuKXII670.png

然后,元空间的好处也是它的坏处。使用非堆可以使用操作系统的内存,JVM 不会再出现方法区的内存溢出;但是,无限制的使用会造成操作系统的死亡。所以,一般也会使用参数 -XX:MaxMetaspaceSize 来控制大小。
方法区,作为一个概念,依然存在。它的物理存储的容器,就是 Metaspace。现在只需要了解到,这个区域存储的内容,包括:类的信息、常量池、方法数据、方法代码就可以了。

上一篇下一篇

猜你喜欢

热点阅读