JVM一共分为五个区域,虚拟机栈、本地方法栈、方法区、堆、程序计数器。一个Java应用在开始运行的时候就会被分解成不同的数据被放到这五个区域里,要想理解JVM内存结构图我们就得先知道这几个区域分别存着什么有什么作用。
Heap(堆区)
Heap是Java OOM错误最主要的发源地,它存储着几乎所有的实例的对象并且是线程共享的,通常情况下所占用的内存空间是所有区域里最大的一块。堆分为两个区域:新生代和老年代。一般新生成的对象会被放到新生代,对象到了晚年会放到老年代,但如果新生的对象大到新生代放不下的时候也会直接放到老年代。
新生代又分成了三个区域:1个Eden区+2个Survivor区。绝大部分对象在 Eden 区生成 ,Eden 区装填满的时候会触发 Young Garbage Collection YGC 。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收,存活的对象会被送往Survivor区,Survivor区分为S0和S1两块内存空间。每次YGC的时候会把存活的对象复制到空的那块空间,然后将另一块空间完全清除掉。这就是我们常说的复制清除算法,S0和S1两块空间总有一块是空的,它们俩总会交换使用状态。如果 YGC 要送往的对象大于 Survivor 区容量的上限 ,则直接移交给老年代。那么会不会有一些对象一直在Survivor区呢?答案是不会的,每一个对象都有一个计数器,每次YGC的时候会加1,当计数器到15的时候就会被送往老年代。当然这个值可以用XX:MaxTenuringThreshold 参数来配置。
老年代就是存放新生代放不下的对象或者是在新生代经历N次YGC都幸存下来的对象。当老年代内存空间也被占满的时候就会触发Full Garbage Collection FGC,FGC也会释放出一些内存空间,直到老年代也满了的时候就会抛出OOM。
对象内存分配图这里再补充几个关于堆的几个命令:
-Xms256M 设置堆的默认空间大小
-Xmxl024M 设置堆的最大空间大小
-XX:MaxTenuringThreshold 设置对象幸存次数阈值,超过这个值就会被送往老年代
-XX:+HeapDumpOnOutOfMemoryError 设置让JVM发生OOM错误的时候输出堆栈信息,可以方便找异常原因
这里有一个小技巧,把Xms和Xmx的值设置成一样的可以避免JVM在GC后来调整堆大小时带来额外的压力。
Metaspace(元数据区)
元数据区在逻辑上被称为方法区,Metaspace是JDK1.8版本之后的实现,在JDK1.7及之前的版本中方法区的实现是永久代(Perm)。
在JDK1.7永久代用于存储一些虚拟机加载类信息,常量,字符串、静态变量等。在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM。
在JDK1.8之后元空间存储一些类元信息、字段、静态属性、方法、常量等,元空间和永久代有一个很大的不同点是:元空间并不占用虚拟机内存,用的是本地内存。
元空间与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。
设置元空间大小有两个命令
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=512m
Program Counter Register(程序计数寄存器)
在程序计数寄存器(Program Counter Register)中,Register 的命名源于CPU 的寄存器, CPU 只有把数据装载到寄存器才能够运行。它是线程私有的,每一个线程都有一个独立的程序计数器。程序计数器占很小的一块内存空间,几乎可以忽略不记。由于CPU是轮询工作的,在众多线程并发执行时,一个CPU的一个内核只会执行一个线程中的一个指令。程序计数器用来记录指令的偏移量和行号,这样CPU下次轮询过来的时候就能正确执行线程的下一个指令。
其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。
JVM Stacks (虚拟机栈)
栈是一个先进后出的数据结构。JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。楼中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。
虚拟机栈虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理。每执行完一个方法就会跳转到下一个栈帧,当整个栈空的时候就代表线程执行完了。打个比方,main方法执行的时候main方法这个栈帧就会在栈底,main方法调用完其他方法后最后执行,执行完主线程就结束了。每个栈帧包含包括有局部变量表、操作栈、动态连接、方法返回地址等。
-
局部变量表是存放方法参数和局部变量的区域。相对于类属性变量的准备阶段和初始化阶段来说,局部变量没有准备阶段,必须显式初始化。如果是非静态方法,局部变量表头部存储的是这个方法所属对象的引用,随后才是参数和局部变量。
-
操作栈 是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写人和提取信息。JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。
-
动态连接 指每个栈中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。
-
方法返回地址 无论方法是否正常完成,都需要返回到方法被调用的位置。方法执行时有两种退出情况:
- 第一,正常返退出,即正常执行到方法的返回,在字节码指令里有RETURN、RETURN、ARETURN 等。
- 第二,方法异常退出,异常退出有三种方式:返回值压入上层调用栈帧、异常信息抛给能够处理的栈帧、程序计数器指向方法调用后的下一条指令。
Native Method Stacks (本地方法栈)
本地方法栈也是线程私有的,本地方法栈服务于Native方法。本地方法可以通过 JNI ( Java Native Interface )来访问虚拟机运行时的数据区 ,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。当大量本地方法出现时会削弱对系统的控制力,因为它的出错信息都比较黑盒。对于内存不足的情况 ,本地方法栈还是会抛出 native heap OutOfMemory。
网友评论