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

内存区域总体设计

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

在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栈信息.PNGui

堆Stack区域

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

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参数增长线程栈大小。
相关文章
相关标签/搜索