JVM内存布局和参数

内存布局

JDK8JVM 内存布局如下:

JVM内存布局

堆 Heap

Heap是导致OOM的主要原因,Heap中存储这几乎所有的实例对象,堆由垃圾回收器自动回收,由各个子线程共享使用,堆一般所占的空间也是所有内存区域最大的,堆的大小可以通过设置 -Xms 256M -Xmx1024M 设置最大最小值,-X 指的是JVM运行参数,-Xms 指的是最小堆容量, -Xmx 指的是最大堆容量,一般来说服务器在运行的过程中可能缩容和扩容,为了避免这种频繁的操作,通常将 JVMXmsXmx 设置成一样的大小,避免在 GC后调整堆大小造成的额外开支。

堆也分成新生代和老年代

新生代

新生代 = 一个Eden + 两个 Survivor

大部分对象都在Eden生成,Eden装满的时候就会触发 YGC(新生代垃圾回收),YGC 的时候 Eden 区没有被引用的对象的内存空间直接被回收,存活的对象会被移送到 Survivor 区。

Survivor 区又分为两块,每次 YGC 都会把对象移动到未被使用的那块空间,将当前正在使用的那块空间完全清除,之前用的现在变成未使用,之前未使用的变成正在使用,如果 YGC 需要移动的对象大于 Survivor 的大小,就直接移交给老年代。

同时每次 YGC 会给这些待在新生代的对象记个数,每次 YGC就加1,默认达到15次就移送到老年代,可以通过设置-XX:MaxTenuringThreshhold进行改变这个15的值。

对象分配和简单GC流程如下:

newObject

如果 Survivor 放不下或者超大对象的阀值超过了上限,则尝试在老年代中进行分配,如果在老年代也放不下就触发 FGC,如果 FGC 后还是放不下,就跑出 OOM,堆内存出线 OOM 的概率是所有内存耗尽异常中最高的,一般需要设置 -XX:+HeapDumpOnOutOfMemoryError, 让 JVM 遇到 OOM 异常能够输出堆内信息。

MetaSpace 元空间

JDK8 中Perm(永久代)已经被淘汰,取而代之的是 MetaspacePerm区启动时固定大小,很难进行调优,FGC 时会移动类元信息,如果动态加载类太多,容器产生 Perm 区的 OOM, 为了解决 Perm 的内存 OOM 问题,需要设置参数 -XX:MaxPermSize=1024mJDK8如果设置这个参数会有个提示告诉你 这个参数已经被忽略,8.0及以后已经被移除了。

字符串常量池被移动到堆内存中,其他内容比如类元信息,字段,静态属性,方法,常量等移动到元空间内。

元空间和永久代最大的区别就在于元空间不在虚拟机上,而是使用本地内存,默认情况下元空间的大小仅受本地内存大小的限制,,但是也可以通过如下参数对元空间进行限制。

  • XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • XX:MaxMetaspaceSize,最大空间,默认是没有限制。
  • XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
  • XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 。

JVM Stack 虚拟机栈

栈是一个先进后出的数据结构,后进来的先离开,就像子弹夹一样。

JVM 的虚拟机栈是描述 Java 方法执行的内存区域,是线程私有的,每个方法从开始调用到执行结束就是栈帧从入栈到出栈的过程,活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧,正在执行的方法是当前方法,执行引擎运行的时候,所有质量都只能对当前栈帧进行操作。 StackOverflow 表示的是请求的栈溢出,导致内存耗尽,就比如递归方法没处理好就可能出线这种情况。

虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法执行结束就会跳转到另一个栈帧上。

虚拟机栈包括如下:

局部变量表

局部变量表就是保存方法参数和局部变量的区域,局部变量不同于类属性的有准备阶段和初始化阶段,局部变量没有准备阶段,必须显式初始化。

操作栈

操作栈是一个初始状态为空的桶式结构栈,方法执行过程中会有各种指令往栈中写入和提取信息,JVM 的执行引擎是基于栈的执行引擎,其中栈指的是操作栈。

动态连接

每个栈帧包含一个在常量池中对当前方法的引用,目的是迟滞方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。

方法返回地址

方法执行有两种退出情况:

  • 正常退出: 正常执行到任何方法的返回字节码指令,比如RETURN,IRETURN,ARETURN等
  • 异常退出

无论是哪一种退出情况,都将返回至方法当前调用的位置,方法退出的过程也就是弹出当前栈帧。

退出的三种方式:

  • 返回值压入上层调用栈帧
  • 异常信息抛给能够处理的异常
  • PC计数器指向方法调用后的下一条指令

Native Method Stacks 本地方法栈

本地方法栈在 JVM 内存布局中,也是线程对象私有的,对于 JVM 来说,本地方法栈主要是为 Native 方法服务,线程调用本地方法时,会进入一个不受 JVM约束的世界,本地方法可以通过 JNI(Java Native INterface) 来访问虚拟机运行时的数据区,甚至还能调用寄存器,具有和 JVM 相同的能力和权限,对于内存不足的情况,可能会抛出 native heap OutOfMemory

类加载过程中,如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。

比如像我们可能见到的 System.currentTimeMills() 这个方法,JNI 使用 Java 深度使用操作系统的特性功能,复用非 Java 代码。但是对于 Java 来说,大量调用其他语言,会影响到程序稳定性,最好是使用中间标准框架来进行解耦,这样本地方法崩溃也不会影响到 JVM 的稳定。

Program Counter Register 程序计数寄存器

CPU 中,CPU 只有把数据装载到寄存器才能运行,寄存器存储指令相关的现场信息,由于 CPU 的时间片机制,众多线程在并发执行的过程中,一个 CPU 核心只能执行某一个线程的一个指令,时间片完了就可能切换到执行其他线程,这样必定导致经常中断和恢复。

每个线程创建后都会有自己的程序计数器和栈帧,程序计数器用来存放 执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖它,程序计数器在多个线程之间互不影响,这个区域也不会发生内存溢出异常,从线程共享的角度来看,堆和元空间是多个线程共享的,虚拟机栈,本地方法栈,程序计数器是线程私有的。

JVM常见参数

  • -Xmx: 初始堆的大小
  • -Xms: 最大堆大小,建议这两个参数大小保持一致,为物理内存的 1/4
  • -Xmn: 指定新生代的大小(Eden + Survior from + Survior to)的大小,增大新生代的大小,老年代的大小将被减小,sun 官方推荐 新生代的大小:堆 = 3 : 8
  • -XX:NewSize: 设置新生代大小
  • -XX:MaxNewSize: 设置新生代的最大值-Xmn 相当于设同时设置 NewSize=MaxNewSize
  • -XX:NewRation:老年代:新生代 = 4,即 old:(Eden + Survivor from + Survivor to) ,则说明新生代为整个堆区的 1/5
  • -XX:SurvivorRation:设置 Eden 区和 Survivor。
    默认值为8;即:Eden:Survivor=8:1 ==> Eden:Survivor from:Survivor to = 8:1:1
    若值为3,即:Eden:Survivor=8:1 ==> Eden:Survivor from:Survivor to = 3:1:1

  • -Xss:栈内存的大小

  • -XX:MetaspaceSize: 元数据区的大小

JVM调试常用参数

  • –verbose:gc在虚拟机发生内存回收时在输出设备显示信息
  • -Xloggc:filename把GC相关日志信息记录到文件以便分析
  • -XX:-HeapDumpOnOutOfMemoryError当首次遭遇OOM时导出此时堆中相关信息
  • -XX:OnError=”<cmdargs>;<cmd args>“ 出现致命ERROR之后运行自定义命令
  • -XX:-PrintClassHistogram遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同
  • -XX:-PrintConcurrentLocks遇到Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同
  • -XX:-PrintGC每次GC时打印相关信息
  • -XX:-PrintGCDetails每次GC时打印详细信息
  • -XX:-PrintGCTimeStamps打印每次GC的时间戳
  • -XX:+PrintGCApplicationStoppedTime打印垃圾回收期间程序暂停的时间
  • -XX:+PrintHeapAtGC打印GC前后的详细堆栈信息
  • -XX:+PrintTenuringDistribution查看每次minor GC后新的存活周期的阈值,即在年轻代survivor中的复制次数.
  • -XX:-TraceClassLoading跟踪类的加载信息
  • -XX:-TraceClassUnloading跟踪类的卸载信息
  • -XX:-TraceLoaderConstraints跟踪类加载器约束的相关信息
  • -XX:ErrorFile=/opt/tomcat/bin/hs_error_%p.log Crash日志