您的位置 首页 java

跟我学Java内存管理—-JMM精华4(内存溢出与堆分代)

1 内存溢出

1.1 Java 堆溢出

Java 堆内存 的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。

要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如EclipseMemory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了 内存泄漏 (Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。

如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查 虚拟机 的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

以上是处理Java 堆内存问题的简略思路,处理这些问题所需要的知识、工具与经验在后面的几次分享中我会做一些额外的分析。

我在做框架的过程中,遇到过内存溢出的情况,其中有项目组开发人员自己的问题,也有我们框架组的问题(测试时不到位,忽略了,汗!!!)主要有下面4种情况:

  • 框架的链路追踪导致的内存溢出(采集的链路过深过细,采集的信息过多,并发上来后,就没抗住;例如一个链路中for循环查询很多次)
  • 项目组Excel导出导致的内存溢出(导出的数据过多,导出的查询条件没有做限制)
  • 项目组的查询接口导致的内存溢出(对非分页查询的查询数量没做限制)
  • 项目组CurrentHashMap不规范的用法导致的内存溢出。

1.2 栈溢出

上篇文章中讲到栈的结构,这里就不赘述了。栈溢出对应的错误是stackOverFlowError,说明方法的调用深度太深了,发生这种情况一般是有递归的无限调用(也不一定就是递归导致的)。

公司的项目组遇到过这样的情况,跟下面链接中的基本一样参:

1.3 常量池溢出

JVM 可以在运行时动态生成常量(例如String的intern方法),所以常量池也可能发生内存溢出。

1.4方法区溢出

方法区溢出一般是加载的Class文件过多(例如动态代理会动态生成Class,这些Class只存在于内存中)常量池也是方法区的一部分。

2 堆结构及对象分代

2.1 什么是分代,分代的必要性是什么

Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略。

堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率。

有了内存分代,情况就不同了:新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中。新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC。老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收。永久代中回收效果太差,一般不进行垃圾回收。还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。

2.1 分代的划分

Java虚拟机将堆内存划分为新生代、老年代和永久代。永久代是HotSpot虚拟机特有的概念(JDK1.8之后为metaspace替代永久代),它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且HotSpot也有取消永久代的趋势,在JDK 1.7中HotSpot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、 静态变量 等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。

内存简图如下:

2.3 新生代(Young Generation)

新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。

HotSpot将新生代划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄 阀值 (默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时,当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

2.4 老年代(Old Generationn)

在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

2.5永久代(Permanent Generationn)

永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

文章来源:智云一二三科技

文章标题:跟我学Java内存管理—-JMM精华4(内存溢出与堆分代)

文章地址:https://www.zhihuclub.com/183872.shtml

关于作者: 智云科技

热门文章

网站地图