垃圾收集 (Garbage Collection) 机制是 Java 的一大优势特性, 为充分榨取 JVM 性能, 避免系统因垃圾收集不及时导致的 OOM (OutOfMemory, 内存溢出)问题, 或内存饱和出现无法响应用户请求的情况, 就需要根据服务器配置及应用复杂度对 GC 策略进行优化, 以确保系统正常运行.
1 JVM 中 Java 对象的分类JVM根据运行于其中的对象的生存时间, 将它们分为3种, 并分别存放在JVM的不同内存区域中. 这种对象存放空间的管理方式叫做 Generation管理方式.
Young Generation (新生代, 又称年轻代): 用于存放"早逝"对象(即瞬时对象), 一般的 Java 应用中, 80%的对象都是"朝生息灭"的, 比如在创建对象或调用方法时使用的临时对象或局部变量.
Tenured Generation (老年代): 用于存放"驻留"对象(即被引用较长时间的对象). 往往体现为一个大型程序中的全局对象或长时间被使用的对象.
Perm Generation (永久代): 用于存放"永久"对象. 这些对象管理着运行于 JVM 中的类和方法.
2 JVM 的 GC 类型及触发条件 2.1 Young GC又叫 Minor GC(次收集), Young GC 经常发生, 且其每次消耗的时间较短 —— 它只对Young Generation 中的对象进行垃圾收集.
触发条件:
在 Young Generation(新生代)的 Edne 区的空间不足以容纳新生成的对象时执行, 同时会将 Eden 区与 From Survivor 区中尚且存活的对象移动至空闲的 To Survivor 区中.
—— 程序运行过程中, 始终有一个 Survivor 区是完全处于空闲状态的, 如果不是, 说明应用程序出现故障了.
2.2 Full GC又叫 Major GC(主收集), 是对整个 Java Heap 中的对象(不包括永久代/元空间)进行垃圾收集, 此 GC 操作耗时久, 对系统的性能影响较大, 因此在 JVM 的调优中, 很多工作是针对 Full GC 的调优 —— 要尽可能减少 Full GC 的频率.
Full GC 是一种"昂贵"的垃圾收集方式, 它要对整个Heap 进行垃圾收集, 并做一定的空间整理, 这会使 Stop-The-World 的时间变长.
Full GC 的触发条件:
1) 年老代(Tenured)空间不足:
通过 Minor GC 后进入老年代的对象的体积大于老年代的可用空间;
由Eden块、From Space 块向 To Space 复制存活对象时, 它们的体积大于 To Space 的大小, 系统就会把这些对象转存到老年代, 而老年代的可用空间小于这些对象的体积.
2) System.gc() 被显式调用, 系统建议执行 Full GC, 但并不会立即执行 —— 非常影响程序性能, 建议禁止使用;
3) 上一次 GC 之后 Heap 各个区域空间的动态变化.
3 Java 对象生成时的内存申请过程1) JVM 会试图为相关 Java 对象在年轻代的 Eden 区中初始化一块内存区域;
2) 当 Eden 区空间足够时, 内存申请结束. 否则执行下一步;
3) JVM 试图释放在 Eden 区中所有不活跃的对象(即 出发Young GC), 释放后若Eden空间仍然不足以放入新对象时, JVM 会试图将部分 Eden 区中活跃的对象迁移至 Survivor 区;
4) Survivor 区被用来作为 Eden 区及老年代的中间交换区域, 当老年代空间足够时, Survivor 区中存活了一定次数的对象会被迁移到老年代;
5) 当年老代空间不够时, JVM会在老年代进行完全的垃圾回收(Full GC);
6) Full GC 后, 若 Survivor 区及老年代仍然无法存放从 Eden 区复制过来的对象, 则会导致 JVM 无法在 Eden 区为新生成的对象申请内存, 即出现 "Out of Memory".
OOM(Out of Memory)异常一般主要有如下2种原因:
1) 老年代溢出, 表现为: java.lang.OutOfMemoryError:Javaheapspace
这是最常见的情况, 产生的原因可能是: 设置的内存参数-Xmx过小或程序的内存泄露及使用不当问题.
2) 持久代溢出,表现为: java.lang.OutOfMemoryError:PermGenspace
通常由于持久代设置过小, 动态加载了大量 Java 类而导致溢出, 解决办法唯有将参数 -XX:MaxPermSize 调大(一般256m能满足绝大多数应用程序需求).
3 Oracle JDK 中的垃圾收集器 3.1 串行收集器(Serial Collector)只有一条GC线程, 运行时需要暂停用户程序(Stop-The-World).
实现: Serial(用于新生代, 采用复制算法)、serial old(用于老年代, 采用标记-整理算法).
3.2 并行收集器(Parallel Collector)有多条GC线程, 运行时也需要暂停用户程序(Stop-The-World).
实现: ParNew(用于新生代, 采用复制算法)、Parallel Scavenge(用于新生代, 采用复制算法)、Parallel Old(用于老年代, 采用标记-整理算法).
3.3 并发收集器(Concurrent Collector)有一条或多条GC线程, 且需要在部分阶段暂停用户程序(Stop-The-World), 部分阶段与用户程序并发执行.
实现: Concurrent Mark Sweep(CMS, 用于老年代, 采用标记-清除算法).
并发(concurrent)与并行(parallel)的比较:
并发就是两个任务(A和B)需要独立运行, 在任务A结束之前, 任务B开始执行 --- 即表面上多个任务同时执行.