JAVA 堆内存管理是影响性能主要因素之一. 堆内存溢出是 JAVA 项目非常常见的故障, 在解决该问题之前, 必须先了解下 JAVA 堆内存是怎么工作的.
先看下 JAVA 堆内存是如何划分的, 如图:
JVM 内存划分为堆内存和非堆内存, 堆内存分为年轻代(Young Generation), 老年代(Old Generation), 非堆内存就一个永久代(Permanent Generation).
年轻代又分为 Eden 和 Survivor 区. Survivor 区由 FromSpace 和 ToSpace 组成. Eden 区占大容量, Survivor 两个区占小容量, 默认比例是 8:1:1.
堆内存用途: 存放的是对象, 垃圾收集器就是收集这些对象, 然后根据 GC 算法回收.
非堆内存用途: 永久代, 也称为方法区, 存储程序运行时长期存活的对象, 比如类的元数据, 方法, 常量, 属性等.
在 JDK1.8 版本废弃了永久代, 替代的是元空间(MetaSpace), 元空间与永久代上类似, 都是方法区的实现, 他们最大区别是: 元空间并不在 JVM 中, 而是使用本地内存. 元空间有注意有两个参数:
MetaspaceSize : 初始化元空间大小, 控制发生 GC 阈值
MaxMetaspaceSize : 限制元空间大小上限, 防止异常占用过多物理内存
为什么移除永久代?
移除永久代原因: 为融合 HotSpot JVM 与 JRockit VM(新 JVM 技术)而做出的改变, 因为 JRockit 没有永久代. 有了元空间就不再会出现永久代 OOM 问题了!
分代概念
新生成的对象首先放到年轻代 Eden 区, 当 Eden 空间满了, 触发 Minor GC, 存活下来的对象移动到 Survivor0 区, Survivor0 区满后触发执行 Minor GC,Survivor0 区存活对象移动到 Suvivor1 区, 这样保证了一段时间内总有一个 survivor 区为空. 经过多次 Minor GC 仍然存活的对象移动到老年代. 老年代存储长期存活的对象, 占满时会触发 Major GC=Full GC,GC 期间会停止所有线程等待 GC 完成, 所以对响应要求高的应用尽量减少发生 Major GC, 避免响应超时. Minor GC : 清理年轻代 Major GC : 清理永久代 Full GC : 清理整个堆空间, 包括年轻代和永久代所有 GC 都会停止应用所有线程.
为什么分代?
将对象根据存活概率进行分类, 对存活时间长的对象, 放到固定区, 从而减少扫描垃圾时间及 GC 频率. 针对分类进行不同的垃圾回收算法, 对算法扬长避短.
为什么 survivor 分为两块相等大小的幸存空间?
主要为了解决碎片化. 如果内存碎片化严重, 也就是两个对象占用不连续的内存, 已有的连续内存不够新对象存放, 就会触发 GC.
JVM 堆内存常用参数
参数 | 描述 |
---|---|
-Xms | 堆内存初始大小,单位 m、g |
-Xmx(MaxHeapSize) | 堆内存最大允许大小,一般不要大于物理内存的 80% |
-XX:PermSize | 非堆内存初始大小,一般应用设置初始化 200m,最大 1024m 就够了 |
-XX:MaxPermSize | 非堆内存最大允许大小 |
-XX:NewSize(-Xns) | 年轻代内存初始大小 |
-XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
-XX:SurvivorRatio=8 | 年轻代中 Eden 区与 Survivor 区的容量比例值,默认为 8,即 8:1 |
-Xss | 堆栈内存大小 |
垃圾回收算法(GC,Garbage Collection)
红色是标记的非活动对象, 绿色是活动对象.
标记 - 清除(Mark-Sweep)
GC 分为两个阶段, 标记和清除. 首先标记所有可回收的对象, 在标记完成后统一回收所有被标记的对象. 同时会产生不连续的内存碎片. 碎片过多会导致以后程序运行时需要分配较大对象时, 无法找到足够的连续内存, 而不得已再次触发 GC.
复制(Copy)
将内存按容量划分为两块, 每次只使用其中一块. 当这一块内存用完了, 就将存活的对象复制到另一块上, 然后再把已使用的内存空间一次清理掉. 这样使得每次都是对半个内存区回收, 也不用考虑内存碎片问题, 简单高效. 缺点需要两倍的内存空间.
标记 - 整理(Mark-Compact)
也分为两个阶段, 首先标记可回收的对象, 再将存活的对象都向一端移动, 然后清理掉边界以外的内存. 此方法避免标记 - 清除算法的碎片问题, 同时也避免了复制算法的空间问题.
一般年轻代中执行 GC 后, 会有少量的对象存活, 就会选用复制算法, 只要付出少量的存活对象复制成本就可以完成收集. 而老年代中因为对象存活率高, 没有额外过多内存空间分配, 就需要使用标记 - 清理或者标记 - 整理算法来进行回收.
垃圾收集器
串行收集器(Serial)
比较老的收集器, 单线程. 收集时, 必须暂停应用的工作线程, 直到收集结束.
并行收集器(Parallel)
多条垃圾收集线程并行工作, 在多核 CPU 下效率更高, 应用线程仍然处于等待状态.
CMS 收集器(Concurrent Mark Sweep)
CMS 收集器是缩短暂停应用时间为目标而设计的, 是基于标记 - 清除算法实现, 整个过程分为 4 个步骤, 包括:
初始标记(Initial Mark)
并发标记(Concurrent Mark)
重新标记(Remark)
并发清除(Concurrent Sweep)
其中, 初始标记, 重新标记这两个步骤仍然需要暂停应用线程. 初始标记只是标记一下 GC Roots 能直接关联到的对象, 速度很快, 并发标记阶段是标记可回收对象, 而重新标记阶段则是为了修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录, 这个阶段暂停时间比初始标记阶段稍长一点, 但远比并发标记时间段. 由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作, 所以, CMS 收集器内存回收与用户一起并发执行的, 大大减少了暂停时间.
G1 收集器(Garbage First)
G1 收集器将堆内存划分多个大小相等的独立区域(Region), 并且能预测暂停时间, 能预测原因它能避免对整个堆进行全区收集. G1 跟踪各个 Region 里的垃圾堆积价值大小(所获得空间大小以及回收所需时间), 在后台维护一个优先列表, 每次根据允许的收集时间, 优先回收价值最大的 Region, 从而保证了再有限时间内获得更高的收集效率.
G1 收集器工作工程分为 4 个步骤, 包括:
初始标记(Initial Mark)
并发标记(Concurrent Mark)
最终标记(Final Mark)
筛选回收(Live Data Counting and Evacuation)
初始标记与 CMS 一样, 标记一下 GC Roots 能直接关联到的对象. 并发标记从 GC Root 开始标记存活对象, 这个阶段耗时比较长, 但也可以与应用线程并发执行. 而最终标记也是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录. 最后在筛选回收阶段对各个 Region 回收价值和成本进行排序, 根据用户所期望的 GC 暂停时间来执行回收.
垃圾收集器参数
参数 | 描述 |
---|---|
-XX:+UseSerialGC | 串行收集器 |
-XX:+UseParallelGC | 并行收集器 |
-XX:+UseParallelGCThreads=8 | 并行收集器线程数,同时有多少个线程进行垃圾回收,一般与 CPU 数量相等 |
-XX:+UseParallelOldGC | 指定老年代为并行收集 |
-XX:+UseConcMarkSweepGC | CMS 收集器(并发收集器) |
-XX:+UseCMSCompactAtFullCollection | 开启内存空间压缩和整理,防止过多内存碎片 |
-XX:CMSFullGCsBeforeCompaction=0 | 表示多少次 Full GC 后开始压缩和整理,0 表示每次 Full GC 后立即执行压缩和整理 |
-XX:CMSInitiatingOccupancyFraction=80% | 表示老年代内存空间使用 80% 时开始执行 CMS 收集,防止过多的 Full GC |
-XX:+UseG1GC | G1 收集器 |
-XX:MaxTenuringThreshold=0 | 在年轻代经过几次 GC 后还存活,就进入老年代,0 表示直接进入老年代 |
为什么会堆内存溢出?
在年轻代中经过 GC 后还存活的对象会被复制到老年代中. 当老年代空间不足时, JVM 会对老年代进行完全的垃圾回收(Full GC). 如果 GC 后, 还是无法存放从 Survivor 区复制过来的对象, 就会出现 OOM(Out of Memory).
OOM(Out of Memory)异常常见有以下几个原因: 1)老年代内存不足: java.lang.OutOfMemoryError:Javaheapspace2)永久代内存不足: java.lang.OutOfMemoryError:PermGenspace3)代码 bug, 占用内存无法及时回收. OOM 在这几个内存区都有可能出现, 实际遇到 OOM 时, 能根据异常信息定位到哪个区的内存溢出. 可以通过添加个参数 - XX:+HeapDumpOnOutMemoryError, 让虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后分析.
熟悉了 JAVA 内存管理机制及配置参数, 下面是对 JAVA 应用启动选项调优配置:
JAVA_OPTS="-server -Xms512m -Xmx2g -Xmn1g -XX:PermSize=256m -XX:MaxPermSize=512m -XX:+UseConcMarkSweepGC -XX:+UseParallelGCThreads=8 XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:-PrintGC -XX:-PrintGCDetails -XX:-PrintGCTimeStamps -Xloggc:../logs/gc.log"
设置堆内存最小和最大值, 最大值参考历史利用率设置
设置 GC 垃圾收集器为 CMS 或者 G1
启用 GC 日志, 方便后期分析
小结
选择高效的 GC 算法, 可有效减少停止应用线程时间.
频繁 Full GC 会增加暂停时间和 CPU 使用率, 可以加大老年代空间大小降低 Full GC, 但会增加回收时间, 根据业务适当取舍.
这不是一个彩蛋, 是一个技术干货
在 2018/2019 年 Docker/Kubernetes 容器技术无疑是业内最火的技术. 根据招聘简介情况来看, 容器技术已成为运维工程师, 架构师必备技能. 为帮助大家快速掌握这门主流技术, 少走弯路, 提高核心竞争力. 决定写基于 Kubernetes 企业容器云平台落地与实践文章专栏, 给朋友在企业落地容器云平台提供一些企业实践性指导, 希望自己所学所思的东西能够帮助到大家, 能够有所启发.
掌握 Docker,Kubernetes 核心概念
熟悉 Docker 日常运维管理
熟练部署 Kubernetes 集群
熟悉容器云平台日常运维管理
容器云平台架构设计及规划
将微服务业务架构迁移到容器云平台
来源: http://blog.51cto.com/lizhenliang/2164876