Bootstrap

Java内存模型

一、整体结构

二、详细解析

java运行程序(进程)时,会有对应的栈、堆、非堆空间,其大小根据默认值或配置参数指定。

1、堆

堆内存是所有线程共用的内存空间,JVM 将 Heap 内存分为年轻代(Young generation)和 老年代(Old generation, 也叫 Tenured)两部分。 年轻代还划分为 3 个内存池,新生代(Eden space)和存活区(Survivor space), 在大部分 GC 算法中有 2 个存活区(S0, S1),在我们可以观察到的任何时刻,S0 和 S1 总有一个是空的, 但一般较小,也不浪费多少空间。

异常

OutOfMemoryError

配置参数

(1)-Xms,初始堆大小,默认大小为物理内存的1/64(<1GB),当(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制

(2)-Xmx,最大堆大小,默认大小为物理内存的1/4,当(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

(3)-Xmn,年轻代大小(1.4or lator) 注意:此处的大小为eden+ 2 survivor space,增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

(4)-XX:NewRatio,年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

(5)-XX:SurvivorRatio,Eden区与Survivor区的大小比值,如果设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

2、Non-Heap

Non-Heap 本质上还是 Heap,只是一般不归 GC 管理,里面划分为 3 个内存池。 Metaspace, 以前叫持久代(永久代, Permanent generation), Java8 换了个名字叫 Metaspace. CCS, Compressed Class Space, 存放 class 信 息的,和 Metaspace 有交叉。 Code Cache, 存放 JIT 编译器编译后的本地机器 代码。

异常

OutOfMemoryError

配置参数

(1)-XX:MetaspaceSize,初始元数据区大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

(2)-XX:MaxMetaspaceSize,设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。默认没有上限,在技术上,Metaspace的尺寸可以增长到交换空间。

3、栈

每启动一个线程,JVM 就会在栈空间栈分配对应的线程栈, 比如 1MB 的空间(- Xss1m)。 线程栈也叫做 Java 方法栈。 如果使用了 JNI 方法,则会分配一个单独的本地方法栈 (Native Stack)。 线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 B,B 调用 C。。。每执行到一个方法,就会创建对应的栈帧(Frame)。

异常

OutOfMemoryError、StackOverflowError

配置参数

(1)-Xss,设置线程栈占用内存大小。

(2)-XX:ThreadStackSize;Thread Stack Size,(0 means use default stack size)

栈帧

栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本上就能确定。 比如返回值需要有一个空间存放,每个局部变量都需要对应的地址空间,此外还有给指令使用的 操作数栈,以及 class 指 针(标识这个栈帧对应的是哪个类的方法, 指向非堆里面的 Class 对象)。

(1)局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)

一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。

虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。

(2)操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。

操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。

当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

(3)动态链接

在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。

Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。

这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。

(4)调用栈

当一个方法开始执行时,可能有两种方式退出该方法:正常完成出口或异常完成出口

正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。

异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。

无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。

无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。

方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。

一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

下图为方法执行过程中栈帧工作流程:

图中局部变量区即为局部变量表,通过指令istore将变量ab放到局部变量表,该区域也在编译阶段就确定大小;

图中求值栈即为操作数栈,在编译为字节码时就确定了操作数栈最大深度。图中先求解ab之和,通过指令iload将ab的值分别加载到操作数栈,然后执行iadd指令求得结果后将结果放到栈顶;

我们自己写了段代码,看看其编译后class文件:

源代码如下:

public class MethodStackLearn {

    public static int add(int a, int b) {
        double c = 2;
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(add(1, 2));
    }
}

javac -g编译后通过javap -verbose查看字节码如下:

  Last modified 2020年12月13日; size 661 bytes
  MD5 checksum e49a4103e5fb42d06db60dbdb880338f
  Compiled from "MethodStackLearn.java"
public class MethodStackLearn
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#29         // java/lang/Object."":()V
   #2 = Double             2.0d
   #4 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #7.#32         // MethodStackLearn.add:(II)I
   #6 = Methodref          #33.#34        // java/io/PrintStream.println:(I)V
   #7 = Class              #35            // MethodStackLearn
   #8 = Class              #36            // java/lang/Object
   #9 = Utf8               
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LMethodStackLearn;
  #16 = Utf8               add
  #17 = Utf8               (II)I
  #18 = Utf8               a
  #19 = Utf8               I
  #20 = Utf8               b
  #21 = Utf8               c
  #22 = Utf8               D
  #23 = Utf8               main
  #24 = Utf8               ([Ljava/lang/String;)V
  #25 = Utf8               args
  #26 = Utf8               [Ljava/lang/String;
  #27 = Utf8               SourceFile
  #28 = Utf8               MethodStackLearn.java
  #29 = NameAndType        #9:#10         // "":()V
  #30 = Class              #37            // java/lang/System
  #31 = NameAndType        #38:#39        // out:Ljava/io/PrintStream;
  #32 = NameAndType        #16:#17        // add:(II)I
  #33 = Class              #40            // java/io/PrintStream
  #34 = NameAndType        #41:#42        // println:(I)V
  #35 = Utf8               MethodStackLearn
  #36 = Utf8               java/lang/Object
  #37 = Utf8               java/lang/System
  #38 = Utf8               out
  #39 = Utf8               Ljava/io/PrintStream;
  #40 = Utf8               java/io/PrintStream
  #41 = Utf8               println
  #42 = Utf8               (I)V
{
  public MethodStackLearn();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMethodStackLearn;

  public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=2
         0: ldc2_w        #2                  // double 2.0d
         3: dstore_2
         4: iload_0
         5: iload_1
         6: iadd
         7: ireturn
      LineNumberTable:
        line 7: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0     a   I
            0       8     1     b   I
            4       4     2     c   D

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: iconst_2
         5: invokestatic  #5                  // Method add:(II)I
         8: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        11: return
      LineNumberTable:
        line 12: 0
        line 13: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  args   [Ljava/lang/String;
}
SourceFile: "MethodStackLearn.java"

此处我们只关注add方法,可以看到字节码中包含

descriptor,括号内为参数类型都是I,表示int,括号外的为返回值类型int

flags,表示方法属性,为public static的

Code,表示方法内部代码,stack即为操作数栈大小为2,locals为局部变量区大小占用4个slot,args_size为参数个数

LocalVariableTable,为局部变量表,Start表示该变量有效起始范围,Length表示变量作用范围长度,Name表示变量名称,Signature表示变量类型,比如c变量在3行生成存储后,即第四行生效,该方法一共长度为8,所以c的Length为4,类型为D,即double

4、直接内存

jdk1.4加入了NIO,引入了基于通道与缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为内存引用进行操作,可减少Java堆和native堆复制数据,提高性能,其分配大小受制于机器内存大小。

异常

OutOfMemoryError

三、问题收集分析

1、Java中的各种常量池存在哪里?各常量池使用流程有啥区别?

全局字符串池(string pool也有叫做string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)

字符串池在JDK1.6以前是存放在永久区中,但是在JDK1.7以后就被转移到堆上。

例子:

public class StringTest {
    public static void main(String[] args) {
        //创建了两个对象,一份存在字符串常量池中,一份存在堆中
        String s = new String("aa");
        //检查常量池中是否存在字符串aa,此处存在则直接返回
        String s1 = s.intern();
        String s2 = "aa";

        System.out.println(s == s2);  //false
        System.out.println(s1 == s2); //true
    }
}

intern方法逻辑:

在JDK1.6中,如果字符串常量池中已经存在该字符串对象,则直接返回池中此字符串对象的引用。否则,将此字符串的对象添加到字符串常量池中,然后返回该字符串对象的引用。

在JDK1.7中,如果字符串常量池中已经存在该字符串对象,则返回池中此字符串对象的引用。否则,如果堆中已经有这个字符串对象了,则把此字符串对象的引用添加到字符串常量池中并返回该引用,如果堆中没有此字符串对象,则先在堆中创建字符串对象,再返回其引用。

class文件常量池(class constant pool)

用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。

一般包括下面三类常量:

  • 类和接口的全限定名

  • 字段的名称和描述符

  • 方法的名称和描述符

运行时常量池(runtime constant pool)

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。

2、程序计数器和操作数栈区别?

程序计数器为线程所有,每个线程都会创建一个程序计数器,用于记录当前线程执行到哪个指令;操作数栈为栈帧中一部分,每个方法调用都会创建栈帧,用于记录存储方法中操作的数据。

3、线程私有分配缓冲区有什么作用?

TLAB是线程的一块私有内存,它是虚拟机在堆内存的eden划分出来的,如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存, 只给当前线程使用, 这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已,当一个TLAB用满 ,就会新申请一个TLAB。

4、直接内存为非运行时数据区,不受jvm管控,使用场景除了NIO还有啥?

主要用作缓存。

除了NIO的DirectByteBuffer外,还可以通过Unsafe类创建堆外内存。但两种方法都只能手动分配和释放内存,无法做到像堆内存一样支持垃圾回收。

Ehcache支持分配堆外内存,又支持KV操作,还无需关心GC。

5、触发Young GC和Full GC条件是什么?

Young GC

(1)当young gen中的eden区分配满的时候触发。young GC后old gen的占用量通常会有所升高。

Full GC

(1)触发young GC前,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则触发full GC;

(2)创建直接分配到老年代的大对象或大数组时,如果老年代空间不足,会触发full ;

(2)在元数据区分配空间但已经没有足够空间时,会触发full GC;

(3)System.gc()、heap dump带GC,默认也是触发full GC

感谢KiKiMing老师以下各位大佬: