1、内存管理 - 栈 or 堆
无论是java还是C,内存分配,本质上就是 栈和堆两个类型。简单来说,代码逻辑处理在栈上,数据在堆上。
I、JVM内存模型
堆:新生代(Eden,survivor),年老代(Gen) -- 分配对象、数组等
非堆(栈):虚拟机栈,本地方法栈 -- 栈帧 分配局部变量、操作需要的空间比如方法链接
方法区-(永久代) -- 分配代码、全局变量、静态变量
Object o = new Object()
首先代码在 方法区中。
执行时,Object o 会存放在 java栈 的本地变量表中。
new Object 会在 java堆中。
II、JVM 内存分配过程
a、创建的对象都在堆的新生代(Eden)上分配空间;垃圾收集器回收时,把Eden上存活的对象和一个Survivor 的对象拷贝到另一个Survivor
一般是MinorGC
b、大对象直接进入老年代
-- 所以对于大对象比较多的,年老代分配的内存要多一点,年轻代分配的少一点
--可以配置对象大小的门限
c、长期存活的对象进入老年代
--在多次MinorGC时仍然存活的,进入老年代
--可以配置门限MinorGC次数,默认15次
原因很简单,因为 年轻代一般是复制算法,多次复制代价很大
老年代是 Full GC
d、年龄判断,如果 Survivor 的同龄对象占所有对象的一半,大于这个年龄的就直接进入老年代
MinorGC时,检查晋升老年代的对象是否大于 老年代剩余空间,如果大于则进行 Full GC
e、空间分配担保
在发生Minor GC 时,虚拟机检测之前 晋升到老年代的空间平均大小 是否大于老年代剩余空间,如果大于则直接进行 Full GC;如果小于,则查看HandlePromotionFailure 设置是否允许担保失败。如果允许,就进行Minor GC,并把存活的对象移到老年代,如果不允许,则进行Full GC
III、内存溢出
a、OutOfMemoryError
首先,堆内存不够分配、肯定会出现 内存溢出的问题
永久代,加载的类太多,也会有
栈内存申请不到也有
本机native直接内存溢出
内存溢出会出现在各个内存区域
b、StackOverflowError
递归调用(没有关闭条件)
线程太多
c、内存溢出定位过程
使用内存映像分析工具(Eclipse Memory Analyzer),对dump出的文件进行分析
确认内存中的对象是否必要的。 即分清楚出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
如果是内存泄漏通过工具查看 泄漏对象到 GC root的引用链
如果不是泄漏,则 检查 虚拟机的堆参数(-Xmx -Xms),从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况。
2、内存(垃圾)回收
在描述 java 垃圾回收之前,想象一下 C ++ 内存如何内存管理 和 垃圾回收。
通常new 一片内存区域,存储一些数据,假设就是 new int[]
频繁的操作删除后,留下了很多内存碎片
然后一般都是 memcpy 把数据转移到内存的一端,一般是都移动到开始端。
事实上,所有的内存回收后的管理,基本都是 拷贝移动已有数据。比如 Redis的 ziplist 就是这么设计的。
垃圾回收两个问题:
I、如何判断 对象不再被使用?
a、首先想到的是记录每一个使用者 - 引用计数器,事实上早期的java垃圾回收就是如此。
引用计数有个很大的困扰,几个对象间的互相循环引用,怎么办?引用计数一直存在。
b、标记引用链 + 从根开始 - 根搜索法
通过引用链可以识别对象引用关系;从根开始,就能识别脱离主链的 循环引用的问题。这样利用有向图,从根开始寻找整个引用链,把不再链上的对象都进行标记。
什么样的对象适合做根对象 GCroot
静态变量 - 程序加载首先进内存的对象,全局根
栈帧的变量 - 程序当前执行到的对象,临时根 (因为执行完毕,栈帧的数据就会回收,执行过程中,作为当前流程开始的对象同样也是根)
II、如何操作回收不用的对象?
前面已经描述过C++ 内存回收方法,java也非常类似
标记清除法 - 前面发现的对象,标记完后,进行删除, 类似 delete,这样会产生很多碎片
复制算法 - 把存活的对象,统一拷贝到 另一块完整内存
标记整理法 - 把存活对象移动到一端,剩下的内存统一清理,类似 memcpy,后delete
适用场景
复制算法,适用存活对象较少的场景,比如 新生代;标记整理算法和清除算法,适用于存活对象较多的场景。
III、垃圾回收器
除了标记清除法外,其他两种需要移动对象,都会造成程序的卡顿(移动过程中,对象不能被改变),这个问题数据库备份过程中也有同样的问题。
a、复制算法收集器 -- 基本都用在新生代
Serial收集器 - 单线程条件下运行 (一般client和默认的)
ParNew收集器 - 多线程条件下运行 (一般server模式适用)
Parallel Scanvenge收集器
ParNew VS Parallel Scanvenge
ParNew 关注卡顿时延; Parallel Scanvenge 关注系统吞吐量
b、标记整理算法收集器 -- 基本用在老年代
Serial Old收集器 - 单线程
Parallel Old收集器 - 多线程 关注吞吐量
c、标记清除算法收集器 -- 用在老年代
CMS(Concurrent Mark Sweep)收集器 关注时延(因为耗时最多的标记和清除不需要影响用户业务)
G1收集器(Garbage First)收集器
时延 or 吞吐量
可以这么理解,时延的目标是单次回收要尽快,减少单次时延,而整体卡顿累计时长可能更多,导致吞吐量下降;吞吐量则关注整体卡顿情况,累计时长要端,吞吐量要高,单次卡顿时延可能会较长。
在这两种策略下,关注时延的 可能是多次频繁小范围的GC、关注吞吐量的可能是 一次就彻底的大范围的GC
组合:
单线程版本 - Serial + Serial Old 用在 Client模式下 (一般很少使用)
吞吐量优先组合 - Parallel Scanvenge + Parallel Old(Serial Old 老版本) 用在 Server模式下
时延优先组合 - ParNew + CMS(Serial Old 备用)用在 Server 模式下
3、JVM 优化
I、JVM crash
JVM 宕机的问题分析,首先 JVM 是一个C++进程,同样可以采用 C++ coredump 的分析思路来分析 JVM (网上描述的,好像用jmap生成的dump不能用GDB调试)
a、定位的文件素材
crash 日志
生成 -XX:ErrorFile=/path/xxx.log;
执行命令-XX:OnError="string"
-XX:+ShowMessageBoxOnError -- 打开实时GDB调试
程序自带日志
coredump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/xx.log
linx: kill -3 | windows : Ctrl + Break
用JDK 自带命令jmap ,或者工具 JConsole和VisualVM
如果不能生成 则检查linux的 ulimit 配置
如果都找不到 到 /var/log/message中 找 cat messages|grep java java线程相关信息
b、分析文件
crash 日志
日志头(概要信息): -- 得到在哪个大的部分出现的问题。粗略信息
SIGSEGV - 执行 JNI 时出现的问题,一般是 编译加载类、执行JVM 外部代码出现的问题
EXCEPTION_ACCESS_VIOLATION - 执行 JVM 自身的代码
EXCEPTION_STACK_OVERFLOW - 堆栈出错
执行代码类型 C J VM 等
线程信息: -- 得到crash时线程的工作情况
线程类型 - Java Thread | VMThread | CompilerThread | GCTaskThread | WatcherThread | ConcurrentMarkSweepThread
线程状态 - _thread_in_native | _thread_uninitialized | _thread_new | _thread_in_vm | _thread_in_Java | _thread_blocked
安全点 safepoint 和锁 Mutex
安全点是标记线程运行到一个区域,JVM将其挂起,以便执行GC 等JVM操作。没有运行到安全点的线程,GC是不能回收其内存的;如果线程一直不到安全点可能会出现假死状态
内存heap情况
各个内存区域使用情况
其他信息
JVM参数,系统环境
分析重点:概要信息里,判断Crash时正在执行什么信息;当前线程状态;还有内存使用情况。
经典问题:内存溢出,一般永久代因为分配较少,出现问题的情况比较多;堆栈溢出,主要是 jni 本地栈溢出的可能较多
threaddump / heapdump 文件
当前线程运行状态、线程堆栈信息
类、对象使用情况
分析重点:基本信息里面,生成堆栈时的 异常线程和异常原因; wait/lock 等信息
经典问题:内存溢出
分析顺序 crash日志 > thread dump > heap dump
II、JVM OOM 问题
分析的文件和 crash 一样的。分析过程也是类似;
另外,可以通过JDK工具和命令实时监控分析。
不过,OOM 不一定会出现crash的情况。一般都是分析 heap dump文件。
a、分析内存 堆、非堆的使用情况; 看看是否是内存分配参数设置不合理。
b、分析 出现OOM的线程,正在操作的情况。找到导致泄露的对象的 GC Root 链
c、分析 类实例数最多、最大的 类的使用情况。(大对象、多对象)
-- 找到上面这两种情况下的 类和对象 是否需要/需要这么多,分清是 泄露还是对象生命周期不合理
d、分析 GC 的情况
-- 看看Full GC的情况
III、性能优化
程序的性能优化,无非就是 CPU、内存、IO 三种资源的占用情况分析。
性能优化的关键在于,分段排查,逐步逼近的方式,确定问题代码所在
先测量
再逐步逼近
找到问题代码
分析原因,并给解决方法
第一步:Linux 命令查看
top -H -p 找到 java进程和线程 中最耗资源的线程
第二步:在各种日志中找到对应的线程进行分析
a、卡顿时间较长 or 处理很慢 - 一般是CPU在高负荷运转,说明线程在高负荷执行;GC 时间较长等
查看GC 时长,GC 频次,各种GC占比 使用的是什么垃圾处理器
(比如 GCViewer 工具),GC的具体情况 -XX:+PrintGCTimeStamps -Xloggc:/tmp/gc.log -XX:+PrintGCDetails
查看各个线程处理情况,lock wait/notify 等情况;长时间运行的线程
查看JNI线程使用情况
连续生成两次的 线程堆栈(core) 文件,对比,查看 对象变化,线程执行变化;如果执行方法没有变化的线程,一般就是有问题的线程
IV、JDK自带工具
a、命令行工具
内存信息 jmap工具
生成dump jmap dump:format=b,file=xxx pid
内存统计 jmap -heap
内存跟踪 jstat
线程堆栈跟踪 jstack
配置信息 jinfo
分析工具 jhat
利用命令行就是 jmap+jstack,然后详细信息通过jhat
b、可视化工具
JConsole
JVisualVM - 其中 BTTrace 可以嵌入到每个方法追踪每个方法的执行(通过类似 asm/CGLib 字节码加载替换)
对比两个 dump 的差异,找出对象的
V、性能分析工具
MAT
IBM Heap - 可以分析出OOM中的 内存占用最大的地方,可以溯源GC root,找到对象树
VI、配置建议
a、内存分配,各个代的分配,32位JVM下 堆分配 1G,年轻代 一半,年老代一半。持久代64M
-Xmx512m
-Xms512m
-- 最大最小保持一致,避免频繁扩容和收缩
-Xmn256m
-- 年轻代一般在 一半左右,官方推荐 3/8
-XX:PermSize=64m
-- 持久代一般固定在 64M左右
-XX:MaxPermSize=128m
b、垃圾收集器选择
-XX:+UseParNewGC
--设置年轻代使用 ParNew收集器 并行
-XX:+UseConcMarkSweepGC
--设置年老代为CMS收集器 并发
c、其他设置项:内存压缩,对象晋级等等。
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-- Full GC 5次后,进行内存碎片压缩
-XX:+HeapDumpOnOutOfMemoryError
-- 内存溢出时生成dump文件
d、启用运行期编译
Jit即时编译器
C1 – Client (简单优化
C2 – Server(激进优化 运行在Server模式下会更高效)
根据监控,针对热点代码进行优化(比如方法内联)
Jit的缺点是 编译有耗时,另外,对于一些类装载卸载比较多的场景也不适合。
Server模式启动时要慢一点,运行时效率很高
也可以指定,运行模式 解释模式-Xint,编译模式-XComp
比较理想的情况是,编译成Class,运行时可以动态Server模式;兼顾了效率和可移植性。