这些工具能帮助我们理解堆内正在发生什么
1 堆分析
1.1 堆直方图
具体使用了 JCMD 命令,使用 jcmd -l
查看当前运行的 JVM 的所有进程,进而获取 PID。
jcmd pid GC.class_histogram
:查看系统中类统计信息。
1.2 堆转储
jcm pid GC.heap_dump /path/fileName.hprof
接着使用 MAT 来进行分析
1.3 内存溢出错误
OOM的几种原因:
- JVM 没有原生内存可用。
- 永久代/元空间内存不足。
- Java 堆本身内存不足,对于给定的堆空间而言,应用中活跃对象太多。
- JVM 执行 GC 耗时太多。
1.3.1 原生内存不足
例如系统最大内存是 4G,然而 JVM 超过了这个限制就会出现这个异常:unable to create new native thread
1.3.2 永久代/元空间不足
第一种可能是使用的类太多,这时候可以增加元空间的最大大小来解决这个问题。
第二种是类加载器没有被卸载,一般在服务器环境中易出现(Tomcat)。java.lang.OutOfMemoryError: PermGen space
1.3.3 堆内存不足
可以增加堆最大内存大小,但是如果是出现内存泄漏,仍然在以后的某个时间点会发生。java.lang.OutOfMemoryError: Java heap sapce
-XX:HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=
-XX:HeapDumpAfterFullGC
-XX:HeapDumpBeforeFullGC
1.3.4 达到 GC 的开销
java.lang.OutOfMemoryError: GC overhead limit exceeded
这个条件比较苛刻,需要满足以下条件:
- Full GC 时间超过了 98% 的时间。
- Full GC 回收的内存量不足堆 2%。
- 上面两个条件连续 5次 Full GC 都成立。
- -XX:UseGCOverhead-Limit 标志位为 true(默认值)。
2 减少内存使用
2.1 减少对象大小
常见的八大基本类型大小
但是 JVM 会对类的大小自动对齐,使其为 8字节 的倍数。
2.2 延迟初始化
常见的集合类,在初始化时就使用了部门变量延迟初始化,只有你在使用时才会进行初始。
但是如果是线程安全的类,最好不要使用延迟初始化,不然要使用双重锁检查,例如单例模式的双重锁实现例子。
2.3 不可变对象和标准对象
例如 Boolean 变量,其实只需要 FALSE 和 TRUE,但是实际开发会在各个地方进行初始化,优化的手段是,将其变成两个不可变对象,每个使用的地方都可以引用。
2.4 字符串的保留
例如 String 是不可变对象,如果有很多都是相同的 String 字符串,通过保留字符串可以节省内存。例如 intern()
注意:保留字符串的表时保存在原生内存中,是个固定的 hashtable,JDK7 前默认 1009,之后是 60013。它是在 JVM 就创建了固定的大小。
3 对象生命周期管理
3.1 对象重用
3.1.1 对象池
- 原生 NIO 缓存区(Netty 使用了池)
- 线程池
- 大数组
- 安全相关类(Signature)
- StringBuilder
但是对象池会对 GC 有很大影响,同时也有同步问题。
3.1.2 线程局部变量
- 好处在于不需要考虑同步的问题。
- 但是一般线程局部变量和其线程有对应关系。