深入理解JVM之JVM运行时内存区域

 约 9 分钟

内存区域整体设计

根据JVM规范,最初Java内存分为5个区域,分别为(Heap)、JVM栈(Stack)、方法区(Method Area)、本地方法栈(Native Method Stack)、程序计数器(Program Counter Register)。方法区中,还有一块运行时常量池(Runtime Constant Pool)区域。

在JDK1.4版本新增的NIO特性中,新增了本地内存(Native Heap),此内存区域在JVM外分配,由OS管理,此区域又叫直接内存(Direct Memory),通过ByteBuffer的静态方法allocateDirect可在本地内存中分配直接内存Buffer,并通过JVM堆中的DirectByteBuffer对象引用此Buffer。

方法区主要用于存储类的元数据、运行时常量等数据,在JVM规范中,对于方法区的管理比较宽松,没有明确定义方法区的实现位置,JVM参考虚拟机HotSpot的实现上,把方法区放在堆的永久代中。

随着Spring等使用动态字节码、反射、代理技术框架的流行,运行过程中生成大量的类,方法区存储的内容越来越多,导致方法区内存容易溢出,出现 java.lang.OutOfMemoryError: PremGen space异常。

在JDK7开始对方法区结构进行了优化拆分,并在JDK8中彻底去掉了方法区。优化方法包括把类的元信息、符号引用(Symbols)移到本地内存区域,把静态变量(class static)、字面量(internal strings)移到堆区域。其中存储类的元信息区域叫做元空间(Metaspace)。

JVM内存分区.PNG

内存区域说明

区域 特征 作用 配置参数 异常 生命周期
线程共享 类实例对象存储空间 -Xms -Xsx -Xmn OutOfMemoryError JVM
线程私有 线程栈帧,局部变量、方法参数、操作数、动态链接等信息 -Xss(帧数) StackOverflowError OutOfMemoryError 线程
本地方法栈 线程私有 线程调用Native方法栈帧 - OutOfMemoryError 线程
程序计数器 此区域很小,为线程私有 记录线程运行的字节码行号等线程运行位置信息,用于线程的切换和恢复 - - 线程
直接内存 在OS Native内存中分配,可突破JVM内存大小限制 通过类似mmap在OS内存中分配,可避免内存在OS和JVM间拷贝 - OutOfMemoryError JVM
元空间 在OS Native内存中分配 保存类的元信息、符号引用等信息 XX:MetaspaceSize XX:MaxMetaspaceSize - JVM
运行时常量池 在堆中分配 保存静态变量、字面量等数据 - - JVM

栈Heap区域

每个线程运行时,会在栈中分配一个线程栈,栈由栈帧(Stack Frame)组成,每次调用方法时会生成一个栈帧,方法返回时则弹出栈顶帧。栈中保存了方法入参、局部变量、操作数栈、动态链接、方法出口信息等数据。当一个线程栈帧的栈数量超过-Xss定义的帧数时,会抛出StackOverflowError异常,一般在递归调用时容易发生此错误。
JVM栈信息.PNG

堆Stack区域

JDK8中,堆中移除了永生代区域,堆内存主要由新生代老年代两部分组成。其中新生代由一个伊甸园(Eden)和两个幸存者(Survivor)3部分组成,新生代的垃圾回收频率高,Minor GC时,把Eden和其中一个Survivor中的存活对象拷贝到另一个Survivor区,并清除前面两个区域的数据,通过这种结构和回收方式来提高垃圾回收效率,减少内存碎片。经过若干(默认15)次后还存活的对象,将进入老年代区,当老年代数据慢是会触发Major GC

JVM堆信息.PNG

老年代:新生代的内存大小默认比例为2:1。Eden和两个Survivor的比例为8:1:1。内存的分配比例可以通过 java -XX:+PrintFlagsFinal -version 命令进行查看。

[Global flags]
uintx InitialSurvivorRatio = 8
uintx NewRatio = 2

设置内存区域比例参数:

参数 说明
-XX:InitialSurvivorRatio 新生代Eden/Survivor空间的初始比例
-XX:Newratio 老年代和新生代的内存比例

程序计数器

当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。

OutOfMemoryError报错及解决方法

  1. java.lang.OutOfMemoryError:java heap space
    这种是java堆内存不够,一个原因是内存真不够,另一个原因是程序中有死循环。如果是java堆内存不够的话,可以通过调整JVM下面的配置来解决:-Xms、-Xmx
  2. java.lang.OutOfMemoryError:GC overhead limit exceeded
    这是JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。解决方案:

    1. 查看系统是否有使用大内存的代码或死循环;
    2. 通过添加JVM配置,来限制使用内存:-XX:-UseGCOverheadLimit
  3. java.lang.OutOfMemoryError: PermGen space
    这一部分用于存放Class和Meta的信息,Class在被Load的时候被放入PermGen space区域。所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。这种是永久代内存不够,可通过调整JVM的配置: -XX:MaxPermSize、-XXermSize
  4. java.lang.OutOfMemoryError: Direct buffer memory
    可能原因是本身资源不够或者申请的太多内存。如果不是内存泄漏的话,可以使用参数-XX:MaxDirectMemorySize参数,或者-XX:MaxDirectMemorySize
  5. java.lang.OutOfMemoryError: unable to create new native thread
    可能原因是系统内存耗尽,无法为新线程分配内存或者创建线程数超过了操作系统的限制。通过两个途径解决:

    1. 排查应用是否创建了过多的线程。通过jstack确定应用创建了多少线程
    2. 调整操作系统线程数阈值。操作系统会限制进程允许创建的线程数,使用ulimit -u命令查看限制。某些服务器上此阈值设置的过小,比如1024。一旦应用创建超过1024个线程,就会遇到java.lang.OutOfMemoryError: unable to create new native thread问题。如果是这种情况,可以调大操作系统线程数阈值。
    3. 增加机器内存。如果上述两项未能排除问题,可能是正常增长的业务确实需要更多内存来创建更多线程。如果是这种情况,增加机器内存。
    4. 减小堆内存。一个老司机也经常忽略的非常重要的知识点:线程不在堆内存上创建,线程在堆内存之外的内存上创建。所以如果分配了堆内存之后只剩下很少的可用内存,依然可能遇到java.lang.OutOfMemoryError: unable to create new native thread。考虑如下场景:系统总内存6G,堆内存分配了5G,永久代512M。在这种情况下,JVM占用了5.5G内存,系统进程、其他用户进程和线程将共用剩下的0.5G内存,很有可能没有足够的可用内存创建新的线程。如果是这种情况,考虑减小堆内存。
    5. 减小线程栈大小。线程会占用内存,如果每个线程都占用更多内存,整体上将消耗更多的内存。每个线程默认占用内存大小取决于JVM实现。可以利用-Xss参数限制线程内存大小,降低总内存消耗。例如,JVM默认每个线程占用1M内存,应用有500个线程,那么将消耗500M内存空间。如果实际上256K内存足够线程正常运行,配置-Xss256k,那么500个线程将只需要消耗125M内存。(注意,如果-Xss设置的过低,将会产生java.lang.StackOverflowError错误)。
  6. java.lang.StackOverflowError
    这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。可以通过优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小。
阅读 95

推荐阅读
目录