Java内存模型(Java Memory Model):往往是指Java程序在运行时内存的模型,而Java代码是运行在Java虚拟机之上的,由Java虚拟机通过解释执行(解释器)或编译执行(即时编译器)来完成,虚拟机在执行Java程序的过程中,会把它管理的内存划分为几个不同的数据区域,这些区域都有各自的用途、创建时间、销毁时间。故Java内存模型,也就是指Java虚拟机的运行时内存模型。它包括程序计数器(Program Counter Register)、虚拟机栈(栈 Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。其实还有常量池,只不过他一般都是在方法区中。

jvm_memory

Java栈:

  • 栈是一种非常常见的数据结构,它采用典型的先进后出(后进先出 LIFO)的操作方式完成的。每一个栈都包含一个栈顶,每次出栈是将栈顶的数据取出,同样存数据也是存到栈顶。栈式一块连续的内存区域,大小是有操作系统觉决定的(这个可以在配置文件中配置)。所以栈的大小比较小,远远小于堆。主要存储一些引用和局部变量(基本类型的局部变量)和方法调用信息,也就是每个方法调用被压入栈中,当他运行完的时候(return)被弹出,每个方法也就是一个栈帧,所以栈内是有严格的生命周期的,同时当方法调用过多的时候,栈空间不足会抛出java.lang.StackOverFlowError。在Hot Spot虚拟机中,可以使用**-Xss参数来设置栈的大小栈的大小直接决定了函数调用的可达深度**
  • Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。所以Java栈是线程是不共享的,而堆内的数据是共享的
  • 由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。
  • 栈帧(Stack Frame)由三部分组成:局部变量区、操作数栈、帧数据区(动态链接方法,返回地址,额外的信息)。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入Java栈。
    • 局部变量表 (locals大小,编译期确定),一组变量存储空间, 容量以slot为最小单位。
    • 操作栈(stack大小,编译期确定),操作栈元素的数据类型必须与字节码指令序列严格匹配
    • 动态连接, 指向运行时常量池中该栈帧所属方法的引用,为了 动态连接使用。
      • 前面的解析过程其实是静态解析;
      • 对于运行期转化为直接引用,称为动态解析。
    • 方法返回地址
      • 正常退出,执行引擎遇到方法返回的字节码,将返回值传递给调用者
      • 异常退出,遇到Exception,并且方法未捕捉异常,那么不会有任何返回值。
    • 额外附加信息,虚拟机规范没有明确规定,由具体虚拟机实现。

Java堆:

  • Java的堆,是用来存储真正的对象,即new的时候向内存申请的地方,Java是自动分配,他存储对象真正的信息。Java中通过引用去访问对象,即通过栈中的引用(存的是堆中的地址来拿到堆中的数据,这一点和C语言的指针很想,不过在Java中对堆的内存回收和处理都是通过JVM的GC(Garbage Collection)来处理内存。从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

    • 从内存回收角度,Java堆被分为新生代和老年代;这样划分的好处是为了更快的回收内存;
    • 从内存分配角度,Java堆可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);这样划分的好处是为了更快的分配内存;
  • 堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。堆空间不足会抛出异常java.lang.OutOfMemoryError。他是线程共享,每一线程都去堆(heap)去取值,是被所有Java线程锁共享的,也就是线程不安全的。关于在堆上内存分配是并发进行的,虚拟机采用CAS加失败重试保证原子操作,或者是采用每个线程预先分配TLAB内存。

  • 对象创建的过程是在堆上分配着实例对象,那么对象实例的具体结构如下:对于填充数据不是一定存在的,仅仅是为了字节对齐。HotSpot VM的自动内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例数据不是8的倍数,便需要填充数据来保证8字节的对齐。该功能类似于高速缓存行的对齐。

    java_object

方法区:

  • 方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值,在java 8中移除方法区增加了MetaData区,也就是元数据区。

PC寄存器/程序计数器:

  • 当前线程所执行的字节码行号指示器,严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。
  • 当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)

常量池:

  • 常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

本地方法栈:

  • 本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

下面是详细的图片介绍:

stack_heap_info