JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。

经典JVM内存布局分为:本地方法栈程序计数器虚拟机栈堆区元数据区

Head(堆区)

  1. 堆区存储程序所有的实例对象,由垃圾收集器自动回收,堆区由各子线程共享。可以在运行时动态调整内存空间,一般使用命令-Xmx/-Xms表示。

    -Xms:最小堆容量
    -Xmx:最大堆容量
    ------
    -X表示JVM运行参数
    ms表示memory start
    mx表示memory max

    一般在线上环境上,堆的最小最大容量是设置成一样的,因为堆空间的不断扩容或回缩,都会造成不必要的系统压力。

  2. 堆分为两大块:新生代老年代
    新生代=1个Eden区 + 2个Survivor区/S0和S1。对象产生之初在新生代,进入暮年(垃圾处理计数器到15后)到老年代,如果是新生代都无法接收的超大对象就会接收到老年的。
  • 大部分新生的实例对象都存在Eden区中,当新生代进行垃圾回收后,会把存活对象从Eden区中移送到Survivor区中。

  • 那么为什么会有两个Survivor区呢,每次发生垃圾回收时,存活的对象都存放在还未使用的S区中,如果另一个S区中的使用空间完全清除后,再交换两个空间的使用状态。

  • 如果对象在Survivor区中无法存放,就会尝试在老年代区中进行分配。如果老年代中都无法存放该对象,就会触发OOM异常,说明堆内存耗尽。

  • 每个对象都有一个计数器,当每次进行垃圾回收后,该计数器就会+1,当计数器值到阈值(默认15,可设置)后,就会将该对象转移到老年代中。这就是上面提到的暮年。

Metaspace(元空间)

  1. 说到元空间,这里要涉及到JDK的版本问题。在JDK8之前,Perm是Metaspace的前身,而且Perm固定大小而且是全局共享的,如果动态加载类过多,会容易产生Perm区中OOM。可以使用-XX:MaxPermSize=1280m,因为还存在垃圾处理方面的出现的问题,所以在JDK8后就使用Metaspace元空间代替了Perm区。

  2. 元空间在本地内存中分配,在JDK8中,之前Perm区的所有字符串常量都移到堆区中保存,其他包括:类元信息字段静态属性方法常量都移到了元空间中保存。

JVM stack (虚拟机栈)

  1. 是描述Java方法执行的内存区域,也就是说每个方法从调用到执行完毕,都是栈帧从入栈到出栈的过程,而且它是线程私有的。
  2. 在活动线程中,位于栈顶的帧成为当前栈帧,正在执行的方法称为当前方法,栈帧是方法运行的基本结构。
  3. 栈帧包括局部变量表操作栈动态链接方法返回地址
  • 局部变量表
    局部变量表是存放方法参数局部变量的区域。局部变量没有准备阶段,所以必须显式初始化。如果是非静态方法,则需要在Index[0]中存放该方法所属对象的引用后,再保存参数和局部变量。

  • 操作栈
    操作栈是一个初始化状态为桶式结构栈。在方法执行的过程中,会有各种指令在栈中写入、提取信息。

  • 动态链接
    每个栈帧都包含一个在常量池中对本方法的引用。

  • 方法返回地址
    方法退出的过程相当于弹出当前栈帧。方法执行时有两种退出情况:
    第一种正常退出,即遇到返回字节码指令,比如:return,ireurn,areturn。
    第二种异常退出。
    退出有三种方式:返回值压入上层调用栈帧、异常信息抛给能处理的栈帧、pc计数器指向方法调用后的下一条指令。

Native method stacks(本地方法栈)

线程私有,但是相对于JVM而言,虚拟机栈主内,而本地方法栈主外。本地方法栈为Native方法服务,线程开始调用本地方法时,会进入到不受JVM控制的世界中。本地方法可以通过JNI(java native interface)来访问虚拟机运行时数据区,甚至可以调用寄存器。
JNI本地方法中最著名为:System.currentTimeMillis(),JNI可以使用java深度使用操作系统的特性功能。但是如果大量使用的话就会丧失java跨平台的特性。

Program Counter Register(程序计数寄存器)

线程私有,CPU只有把数据转载到寄存器中才可以运行,由于CPU的轮值时间片限制,在线程并发执行时,需要进行线程的中断处理恢复处理。那这个过程如何保证无误呢??
每个线程在创建后都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等。

总结

最后,从线程的角度触发,堆和元空间都是线程共享的,虚拟机栈、本地方法栈、程序计数器都是线程内部私有的。