上篇文章中,主要讲解了java内存模型,我们知道:不同线程之间无法直接访问其它工作内存中的变量,线程变量值的传递需要通过主内存来完成。
本章主要详细说说jvm内存模型
JVM内存模型
对于不同版本的jdk,其内存模型有一定的区别。如下图所示:
程序计数器
程序计数器(Program Counter Register)也叫做PC寄存器。程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
jvm支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是JVM的方法,则该程序计数器保存当前执行指令的地址,倘若执行的native方法,则PC寄存器中为空。
- 当前线程私有
- 当前线程所执行的字节码的行号指示器
- 不会出现
OutOfMemoryError情况 - 以一种数据结构的方式放置在内存中
java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks)是每个线程有一个私有的栈,随着线程的创建而创建,其生命周期与线程一样。栈中存放着栈帧,一旦完成调用,则出栈。当所有的栈帧都出栈后,线程也就完成。
栈帧中存放着局部变量表、操作数栈、动态链接、方法出口和一些额外的附加信息。
栈的大小可以固定也可以动态的扩展。当栈调用深度大于JVM所允许的范围,则会抛出StackOverflowError错误。
- 线程私有,生命周期和线程相同
- java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等
- 当线程请求的栈深度大于虚拟机所允许的深度,会抛出
StackOverflowError - 当栈的扩展无法申请足够的内存,会抛出
OutOfMemoryError
本地方法栈
本地方法栈(Native Method Stacks)与Java虚拟栈的作用和原理非常相似。区别在于Java虚拟机栈为执行Java方法服务,而本地方法栈则是为执行本地方法服务。
方法区
方法区(Method Area)用于存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 线程共享
- 主要存储:类的类型信息、常量池、字段信息、方法信息、类变量和Class类的引用等
- 当方法区无法满足内存分配需求时,会抛出
OutOfMemoryError
堆
堆(Heap)是线程共享的一块内存区域。创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
由于现代VM采用分代收集算法,因此Java堆从GC的角度还可以细分为新生区(Eden区、From Survivor区和To Survivor区)和老年代。
- 线程共享
- 主要用于存储Java实例或对象
- GC发生的主要区域
- 是Java虚拟机管理的内存中最大的一块
- 当堆中没有内存完成实例分配,且堆也无法扩展,会抛出
OutOfMemoryError
JVM运行时内存
jvm运行时内存又称堆内存(Heap)。
Java堆从GC角度可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代。
对象的创建和消亡流程
创建一个对象首先会在栈上分配,栈上如果分配不下进入到Eden区,Eden区经过一次垃圾回收之后进入到From Survivor区,在经过一次垃圾回收之后进入到To Survivor。当年龄到达虚拟机设置的值之后,进入到old区。
新生代
主要用来存放新生的对象,一般占据堆内存的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
新生代又分为Eden区、From Survivor区和To Survivor区
- Eden区:新对象创建被存储的地方,(如果新对象占用内存过大,会被直接分配到老年代)。当Eden区内存不够的时候会触发MinorGC,对新生代进行一次垃圾回收。
- From Survivor区:保留一次MinorGC过程中的幸存者
- To Survivor区:上一次GC的幸存者,作为这一次GC的扫描者
Minor GC流程
- Minor GC采用复制算法
- 首先把Eden区和From Survivor区域中存活的对象复制到To Survivor区域(如果对象的年龄达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1(如果To Survivor不够位置了则放到老年区)
- 然后清空Eden和From Survivor中的对象
- 最后From Survivor和To Survivor互换,原来To Survivor成为下一次GC时的From Survivor区
为什么Survivor分区不能是0个?
如果Survivor是0的话,也就是说新生代只有一个Eden分区,每次垃圾回收之后,存活的对象都会进入到老年代,这样老年代的内存空间很快就会被沾满,从而触发最耗时的Full GC,这样收集器的效率将会大幅度降低。
为什么Survivor分区不能是1个?
如果 Survivor 分区是 1 个的话,假设我们把两个区域分为 1:1,那么任何时候都有一半的内存空间是闲置的,会导致空间利用率太低。
为什么Survivor分区是2个?
如果Survivor分区有2个分区,我们就可以把 Eden、From Survivor、To Survivor 分区内存比例设置为 8:1:1 ,那么任何时候新生代内存的利用率都 90% ,这样空间利用率基本是符合预期的。再者就是虚拟机的大部分对象都符合朝生夕死的特性,所以每次新对象的产生都在空间占比比较大的Eden区,垃圾回收之后再把存活的对象方法存入Survivor区,如果是 Survivor区存活的对象,那么“年龄”就+1,当年龄增长到15(可通过 -XX:+MaxTenuringThreshold 设定)对象就升级到老年代。
老年代
主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,Major GC不会频繁执行。再进行Major GC之前一般会进行一次Minor GC,让有的新生代的对象晋升入老年代,当空间不够时触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次Major GC。
Major GC流程
Major GC采用标记-清除算法。 首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。Major GC耗时较长,因为要扫描再回收。(关于GC算法,在下一章节中会重点讲讲)当老年代也装不下的时候,会抛出OOM异常。
永久区
指内存的永久保存区域,主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。GC不会在主程序运行期对永久区域进行清理,所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
Java8与元数据
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入Native Memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
内存分配策略
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
OOM 常见问题
1. Java heap space
当堆内存没有足够空间存放新创建的对象时候,会抛出java.lang.OutOfMemoryError:Javaheap space
导致原因:
- 创建一个超大对象
- 超出预期的访问量/数据量
- 内存泄漏,大量对象引用没有释放,jvm无法自动回收。常见使用了File等资源
解决方案
针对大部分情况,通常只需要调整-Xmx参数调高JVM堆内存即可。
- 如果是超大对象,从程序上拆分对象
- 如果业务峰值,限流降级、集群部署
- 如果内存泄漏,具体修改代码,例如关闭没有释放的连接
2. Permgen space
该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大
导致原因
永久代存储对象主要包括以下几类:
- 加载/缓存到内存中的 class 定义,包括类的名称,字段,方法和字节码
- 常量池
- 对象数组/类型数组所关联的 class
- JIT 编译器优化后的 class 信息
解决方案
根据 Permgen space 报错的时机,可以采用不同的解决方案,如下所示:
- 程序启动报错,修改
-XX:MaxPermSize启动参数,调大永久代空间 - 应用重新部署时报错,很可能是没有应用没有重启,导致加载了多份 class 信息,只需重启 JVM 即可解决
- 运行时报错,应用程序可能会动态创建大量 class,而这些 class 的生命周期很短暂,但是 JVM 默认不会卸载 class,可以设置
-XX:+CMSClassUnloadingEnabled和-XX:+UseConcMarkSweepGC这两个参数允许 JVM 卸载 class。
Metaspace
JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation),该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。
此类问题的原因与解决方法跟
Permgenspace非常类似,可以参考上文。需要特别注意的是调整 Metaspace 空间大小的启动参数为-XX:MaxMetaspaceSize