七、堆内存最佳实践

这些工具能帮助我们理解堆内正在发生什么


1 堆分析

1.1 堆直方图

具体使用了 JCMD 命令,使用 jcmd -l 查看当前运行的 JVM 的所有进程,进而获取 PID。
jcmd

jcmd pid GC.class_histogram:查看系统中类统计信息。
GC.class_histogram

1.2 堆转储

jcm pid GC.heap_dump /path/fileName.hprof
GC.heap_dump
接着使用 MAT 来进行分析

1.3 内存溢出错误

OOM的几种原因:

  1. JVM 没有原生内存可用。
  2. 永久代/元空间内存不足。
  3. Java 堆本身内存不足,对于给定的堆空间而言,应用中活跃对象太多。
  4. 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
这个条件比较苛刻,需要满足以下条件:

  1. Full GC 时间超过了 98% 的时间。
  2. Full GC 回收的内存量不足堆 2%。
  3. 上面两个条件连续 5次 Full GC 都成立。
  4. -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 对象池

  1. 原生 NIO 缓存区(Netty 使用了池)
  2. 线程池
  3. 大数组
  4. 安全相关类(Signature)
  5. StringBuilder

但是对象池会对 GC 有很大影响,同时也有同步问题。

3.1.2 线程局部变量

  1. 好处在于不需要考虑同步的问题。
  2. 但是一般线程局部变量和其线程有对应关系。

3.2 弱引用、软引用和其它引用

4 小结