内存溢出与内存泄漏
- 堆内存溢出(OutOfMemoryError: Java heap space)
内存溢出是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。
会导致 JVM 内存溢出的一些场景:
- JVM 启动参数堆内存值设定的过小
- 内存中加载的数据量过于庞大(一次性从 Mysql、Redis 取出过多数据)
- 对象的引用没有及时释放,使得JVM不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
内存溢出问题一般分为两种,一种是由于大峰值下瞬间创建大量对象而导致的内存溢出;另一种则是由于内存泄漏而导致的内存溢出。第一种问题可通过限流来处理,第二种问题需要分析程序是否存在 Bug
- 内存泄漏(Memory Leak)
内存泄漏是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
- 过度使用静态成员属性(static fields)
- 忘记关闭已打开的资源链接(unclosed Resources)
- 没有正确的重写 equals 和 hashcode 方法(HashMap HashSet)
哪些对象该被回收?一般有可达性分析和引用计数两种算法来识别可回收的对象:
- 可达性分析
目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
- 引用计数的循环引用问题
可达性分析可以解决引用计数法所不能解决的循环引用问题。举例来说,即便对象 a 和 b 相互引用,只要从 GC Roots 出发无法到达 a 或者 b,那么可达性分析便不会将它们加入存活对象合集之中。
GC Roots 我们可以暂时理解为由堆外指向堆内的引用
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。它从一系列 GC Roots 出发,边标记边探索所有被引用的对象。
一般而言,GC Roots 包括(但不限于)如下几种:
- Java 方法栈桢中的局部变量;
- 已加载类的静态变量;
- JNI handles;
- 已启动且未停止的 Java 线程。
jmap 命令
- jmap -heap
查看堆内存初始化配置信息已经堆内存使用情况,垃圾收集器类型
jmap -heap pid
- jmap -histo
统计各个类的实例数目以及占用内存,并按照内存使用量从多至少的顺序排列
jmap -histo:live pid
- jmap -dump
把堆内存的使用情况 dump 到文件中,live 只保存堆中的存活对象
jmap -dump:live,format=b,file=dump.bin pid
Heap dump 是Java进程在特定时间点的一个内存快照,快照包含在dump时间点java堆中的对象和类的信息。 通常在dump时, 会触发一个完整的GC, 故而Heap Dump文件中只包含哪些未被回收的对象的信息
- jmap 参数详细说明
输出 GC 日志
- 当堆内存空间溢出时输出堆的内存快照
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/java/logs/
- 输出 GC 详细日志并指定日志路径
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 以基准时间的形式输出GC的时间戳(JVM 启动时间为起点的相对时间)
-XX:+PrintGCDateStamps 以日期的形式输出GC的时间戳(2013-05-04 T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:/data/java/logs/usercenter/gc.log 日志文件的输出路径
Eclipse MAT
Eclipse MAT(Memory Analyzer Tool)是一个强大的基于Eclipse的内存分析工具,可以帮助我们找到内存泄露,减少内存消耗
MAT 计算对象占据内存分为 Shallow Heap 和 Retained Heap 两种方式
- Shallow heap
对象自身所占据的内存
- Retained heap
当对象不再被引用时,垃圾回收器所能回收的总内存,包括对象自身所占据的内存,以及仅能够通过该对象引用到的其他对象所占据的内存
MAT 包含了两个比较重要的视图 直方图(histogram)和支配树(Dominator Tree)
- 直方图
MAT 的直方图类似 jmap -histo 命令输出的结果,能够展示各个类的实例数目以及这些实例的 Shallow heap 总和,并且可以根据 Shallow 或 Retained heap 从大到小排序
- 支配树
支配树是由一些对象组成的图,在支配树中每个对象都是它子节点的直接支配者
- 支配者
如果从入口节点到 b 节点的所有路径都要经过 a 节点,那么 a 支配(dominate)b
如果从 a 节点到 b 节点的所有路径中不存在支配 b 的其他节点,那么 a 直接支配(immediate dominate)b
注意点:对象的引用型字段未必对应支配树中的父子节点关系。假设对象 a 拥有两个引用型字段,分别指向 b 和 c。而 b 和 c 各自拥有一个引用型字段,但都指向 d。如果没有其他引用指向 b、c 或 d,那么 a 直接支配 b、c 和 d,而 b(或 c)和 d 之间不存在支配关系
使用 List objects 查看引用关系图
- with outgoing references
查看当前对象持有的外部对象引用(在对象关系图中为从当前对象指向外的箭头)
- with incoming references
查看当前对象被哪些外部对象所引用(在对象关系图中为指向当前对象的箭头)
- Paths to GC Roots
可以通过该功能反向列出该对象到 GC Roots 的引用路径,这个路径解释了为什么当前对象还能存活,对分析内存泄露很有帮助,这个查询只能针对单个对象使用
- Merge Shortest Paths to GC roots
通过下图我们可以看到 Context 直接支配 CtEntry,只要释放 Context 的引用,CtEntry 占用的内存就可以被回收
- 线程视图(Thread Overview)
可以看到线程对象/线程栈信息、线程名、Shallow Heap、Retained Heap、类加载器、是否Daemon线程等信息
Sentinel 内存泄漏 bug 导致 full GC
sentinel-apache-dubbo-adapter Entry leak cause full GC #1416
这个问题首次出现在 Sentinel 1.7.1 版本,且只在如下场景发生:
A,B,C 三个应用,A 是服务消费者,B 是服务提供者并且是服务消费者,C 是服务提供者
A 请求 B 提供的服务,B 在处理 A 请求的同时调用 C 提供的服务
A(consumer) -> B(provider and consumer) -> C(provider)
B 应用 dubbo 业务线程池配置如下:
<dubbo:protocol name="dubbo" port="20077" threadpool="cached" threads="200"/>
cached 类型代表缓存线程池,如果空闲一分钟会自动删除,需要时重建(如果不间断每分钟都有请求需要处理,那么部分线程会一直存活)
- RpcContext 和 Context
这里涉及到的两个 context 分别是 Dubbo 框架的 rcpContext,另外一个是 Sentinel 的 context
RpcContext 用来存储一些临时状态,这些状态会随着每次 request 的发送或者接收而改变
Sentinel context 也是用来存储一些调用的元数据
- Entry 和 CtEntry
SphU#entry() 方法每次调用都会返回一个 Entry,Entry 存储着当前的调用信息
CtEntry 在 Entry 的基础上增加了 parent 和 child 的链式关联
- Context 和 CtEntry 相互引用,应该释放哪个引用才会释放内存 ?
- 为什么会存在 CtEntry 递归引用堆积的问题,并最终指向了同一个 Context ?
- 为什么 providerFilter 没有正常执行 ContextUtil.exit() ?
- 为什么 consumerFilter 不需要执行 ContextUtil.exit() ?
public class SentinelDubboConsumerFilter extends BaseSentinelDubboFilter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Entry interfaceEntry = null;
Entry methodEntry = null;
RpcContext rpcContext = RpcContext.getContext();
try {
String methodResourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix());
String interfaceResourceName = DubboConfig.getDubboInterfaceGroupAndVersionEnabled() ? invoker.getUrl().getColonSeparatedKey()
: invoker.getInterface().getName();
InvokeMode invokeMode = RpcUtils.getInvokeMode(invoker.getUrl(), invocation);
if (InvokeMode.SYNC == invokeMode) {
interfaceEntry = SphU.entry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT);
rpcContext.set(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY, interfaceEntry);
methodEntry = SphU.entry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT, invocation.getArguments());
} else {
// should generate the AsyncEntry when the invoke model in future or async
interfaceEntry = SphU.asyncEntry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT);
rpcContext.set(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY, interfaceEntry);
methodEntry = SphU.asyncEntry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT, 1, invocation.getArguments());
}
rpcContext.set(DubboUtils.DUBBO_METHOD_ENTRY_KEY, methodEntry);
return invoker.invoke(invocation);
} catch (BlockException e) {...}
}
}
public class SentinelDubboProviderFilter extends BaseSentinelDubboFilter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// Get origin caller.
String application = DubboUtils.getApplication(invocation, "");
RpcContext rpcContext = RpcContext.getContext();
Entry interfaceEntry = null;
Entry methodEntry = null;
try {
String methodResourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix());
String interfaceResourceName = DubboConfig.getDubboInterfaceGroupAndVersionEnabled() ? invoker.getUrl().getColonSeparatedKey()
: invoker.getInterface().getName();
// Only need to create entrance context at provider side, as context will take effect
// at entrance of invocation chain only (for inbound traffic).
ContextUtil.enter(methodResourceName, application);
interfaceEntry = SphU.entry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN);
rpcContext.set(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY, interfaceEntry);
methodEntry = SphU.entry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN, invocation.getArguments());
rpcContext.set(DubboUtils.DUBBO_METHOD_ENTRY_KEY, methodEntry);
return invoker.invoke(invocation);
} catch (BlockException e) {...}
}
}
public abstract class BaseSentinelDubboFilter extends ListenableFilter {
static void traceAndExit(Throwable throwable, URL url) {
Entry interfaceEntry = (Entry) RpcContext.getContext().get(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY);
Entry methodEntry = (Entry) RpcContext.getContext().get(DubboUtils.DUBBO_METHOD_ENTRY_KEY);
if (methodEntry != null) {
Tracer.traceEntry(throwable, methodEntry);
methodEntry.exit();
RpcContext.getContext().remove(DubboUtils.DUBBO_METHOD_ENTRY_KEY);
}
if (interfaceEntry != null) {
Tracer.traceEntry(throwable, interfaceEntry);
interfaceEntry.exit();
RpcContext.getContext().remove(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY);
}
if (CommonConstants.PROVIDER_SIDE.equals(url.getParameter(CommonConstants.SIDE_KEY))) {
ContextUtil.exit();
}
}
}
其他 JVM 监控和诊断工具
其他 JVM 监控和诊断指令
- jstat
可用来打印目标 Java 进程的性能数据,以-gc为前缀的子命令,它们将打印垃圾回收相关的数据
- jstack
可以用来打印目标 Java 进程中各个线程的栈轨迹,以及这些线程所持有的锁
- jcmd
参考资料:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。