一个优秀 Java 程序员, 必须了解 Java 内存模型, GC 工作原理, 以及如何优化 GC 的性能, 与 GC 进行有限的交互, 有一些应用程序对性能要求较高, 例如嵌入式系统, 实时系统等, 只有全面提升内存的管理效率, 才能提高整个应用程序的性能.
本文将从 JVM 内存模型, GC 工作原理, 以及 GC 的几个关键问题进行探讨, 从 GC 角度提高 Java 程序的性能.
一, Java 内存模型
按照官方的说法: Java 虚拟机具有一个堆, 堆是运行时数据区域, 所有类实例和数组的内存均从此处分配.
JVM 主要管理两种类型内存: 堆和非堆, 堆内存 (Heap Memory) 是在 Java 虚拟机启动时创建, 非堆内存 (Non-heap Memory) 是在 JVM 堆之外的内存.
简单来说, 堆是 Java 代码可及的内存, 留给开发人员使用的; 非堆是 JVM 留给自己用的, 包含方法区, JVM 内部处理或优化所需的内存 (如 JITCompiler,Just-in-time Compiler, 即时编译后的代码缓存), 每个类结构(如运行时常数池, 字段和方法数据) 以及方法和构造方法的代码.
JVM 内存包含如下几个部分:
● 堆内存(Heap Memory): 存放 Java 对象
● 非堆内存(Non-Heap Memory): 存放类加载信息和其它 meta-data
● 其它(Other): 存放 JVM 自身代码等
在 JVM 启动时, 就已经保留了固定的内存空间给 Heap 内存, 这部分内存并不一定都会被 JVM 使用, 但是可以确定的是这部分保留的内存不会被其他进程使用, 这部分内存大小由 - Xmx 参数指定. 而另一部分内存在 JVM 启动时就分配给 JVM, 作为 JVM 的初始 Heap 内存使用, 这部分内存是由 -Xms 参数指定.
详细配置文件目录: eclipse/eclipse.INI
默认空余堆内存小于 40% 时, JVM 就会增大堆直到 - Xmx 的最大限制, 可以由 -XX:MinHeapFreeRatio 指定.
默认空余堆内存大于 70% 时, JVM 会减少堆直到 - Xms 的最小限制, 可以由 -XX:MaxHeapFreeRatio 指定, 详见
可以通过 -XX:MaxPermSize 设置 Non-Heap 大小, 详细参见我的百度博客
二, Java 内存分配
Java 的内存管理实际上就是变量和对象的管理, 其中包括对象的分配和释放.
JVM 内存申请过程如下:
● JVM 会试图为相关 Java 对象在 Eden 中初始化一块内存区域
● 当 Eden 空间足够时, 内存申请结束; 否则到下一步
● JVM 试图释放在 Eden 中所有不活跃的对象(这属于 1 或更高级的垃圾回收), 释放后若 Eden 空间仍然不足以放入新对象, 则试图将部分 Eden 中活跃对象放入 Survivor 区
● Survivor 区被用来作为 Eden 及 OLD 的中间交换区域, 当 OLD 区空间足够时, Survivor 区的对象会被移到 Old 区, 否则会被保留在 Survivor 区
● 当 OLD 区空间不够时, JVM 会在 OLD 区进行完全的垃圾收集(0 级)
● 完全垃圾收集后, 若 Survivor 及 OLD 区仍然无法存放从 Eden 复制过来的部分对象, 导致 JVM 无法在 Eden 区为新对象创建内存区域, 则出现 "out of memory" 错误
三, GC 基本原理
GC(Garbage Collection), 是 JAVA/.NET 中的垃圾收集器.
Java 是由 C++ 发展来的, 它摈弃了 C++ 中一些繁琐容易出错的东西, 引入了计数器的概念, 其中有一条就是这个 GC 机制(C# 借鉴了 JAVA)
编程人员容易出现问题的地方, 忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃, Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的, Java 语言没有提供释放已分配内存的显示操作方法. 所以, Java 的内存管理实际上就是对象的管理, 其中包括对象的分配和释放.
对于程序员来说, 分配对象使用 new 关键字; 释放对象时, 只要将对象所有引用赋值为 null, 让程序不能够再访问到这个对象, 我们称该对象为 "不可达的".GC 将负责回收所有 "不可达" 对象的内存空间.
对于 GC 来说, 当程序员创建对象时, GC 就开始监控这个对象的地址, 大小以及使用情况. 通常, GC 采用有向图的方式记录和管理堆 (heap) 中的所有对象. 通过这种方式确定哪些对象是 "可达的", 哪些对象是 "不可达的". 当 GC 确定一些对象为 "不可达" 时, GC 就有责任回收这些内存空间. 但是, 为了保证 GC 能够在不同平台实现的问题, Java 规范对 GC 的很多行为都没有进行严格的规定. 例如, 对于采用什么类型的回收算法, 什么时候进行回收等重要问题都没有明确的规定. 因此, 不同的 JVM 的实现者往往有不同的实现算法. 这也给 Java 程序员的开发带来行多不确定性. 本文研究了几个与 GC 工作相关的问题, 努力减少这种不确定性给 Java 程序带来的负面影响.
四, GC 分代划分
JVM 内存模型中 Heap 区分两大块, 一块是 Young Generation, 另一块是 Old Generation
1) 在 Young Generation 中, 有一个叫 Eden Space 的空间, 主要是用来存放新生的对象, 还有两个 Survivor Spaces(from,to), 它们的大小总是一样, 它们用来存放每次垃圾回收后存活下来的对象.
2) 在 Old Generation 中, 主要存放应用程序中生命周期长的内存对象.
3) 在 Young Generation 块中, 垃圾回收一般用 Copying 的算法, 速度快. 每次 GC 的时候, 存活下来的对象首先由 Eden 拷贝到某个 SurvivorSpace, 当 Survivor Space 空间满了后, 剩下的 live 对象就被直接拷贝到 OldGeneration 中去. 因此, 每次 GC 后, Eden 内存块会被清空.
4) 在 Old Generation 块中, 垃圾回收一般用 mark-compact 的算法, 速度慢些, 但减少内存要求.
5) 垃圾回收分多级, 0 级为全部 (Full) 的垃圾回收, 会回收 OLD 段中的垃圾; 1 级或以上为部分垃圾回收, 只会回收 Young 中的垃圾, 内存溢出通常发生于 OLD 段或 Perm 段垃圾回收后, 仍然无内存空间容纳新的 Java 对象的情况.
五, 增量式 GC
增量式 GC(Incremental GC), 是 GC 在 JVM 中通常是由一个或一组进程来实现的, 它本身也和用户程序一样占用 heap 空间, 运行时也占用 CPU.
当 GC 进程运行时, 应用程序停止运行. 因此, 当 GC 运行时间较长时, 用户能够感到 Java 程序的停顿, 另外一方面, 如果 GC 运行时间太短, 则可能对象回收率太低, 这意味着还有很多应该回收的对象没有被回收, 仍然占用大量内存. 因此, 在设计 GC 的时候, 就必须在停顿时间和回收率之间进行权衡. 一个好的 GC 实现允许用户定义自己所需要的设置, 例如有些内存有限的设备, 对内存的使用量非常敏感, 希望 GC 能够准确的回收内存, 它并不在意程序速度的快慢. 另外一些实时网络游戏, 就不能够允许程序有长时间的中断.
增量式 GC 就是通过一定的回收算法, 把一个长时间的中断, 划分为很多个小的中断, 通过这种方式减少 GC 对用户程序的影响. 虽然, 增量式 GC 在整体性能上可能不如普通 GC 的效率高, 但是它能够减少程序的最长停顿时间.
Sun JDK 提供的 HotSpot JVM 就能支持增量式 GC.HotSpot JVM 缺省 GC 方式为不使用增量 GC, 为了启动增量 GC, 我们必须在运行 Java 程序时增加 - Xincgc 的参数.
HotSpot JVM 增量式 GC 的实现是采用 Train GC 算法, 它的基本想法就是: 将堆中的所有对象按照创建和使用情况进行分组 (分层), 将使用频繁高和具有相关性的对象放在一队中, 随着程序的运行, 不断对组进行调整. 当 GC 运行时, 它总是先回收最老的(最近很少访问的) 的对象, 如果整组都为可回收对象, GC 将整组回收. 这样, 每次 GC 运行只回收一定比例的不可达对象, 保证程序的顺畅运行.
六, 详解函数 finalize
finalize 是位于 Object 类的一个方法, 详见我的开源项目: src-jdk1.7.0_02
protected void finalize() throws Throwable { }
该方法的访问修饰符为 protected, 由于所有类为 Object 的子类, 因此用户类很容易访问到这个方法.
由于, finalize 函数没有自动实现链式调用, 我们必须手动的实现, 因此 finalize 函数的最后一个语句通常是 super.finalize(). 通过这种方式, 我们可以实现从下到上实现 finalize 的调用, 即先释放自己的资源, 然后再释放父类的资源. 根据 Java 语言规范, JVM 保证调用 finalize 函数之前, 这个对象是不可达的, 但是 JVM 不保证这个函数一定会被调用. 另外, 规范还保证 finalize 函数最多运行一次.
很多 Java 初学者会认为这个方法类似与 C++ 中的析构函数, 将很多对象, 资源的释放都放在这一函数里面. 其实, 这不是一种很好的方式, 原因有三:
其一, GC 为了能够支持 finalize 函数, 要对覆盖这个函数的对象作很多附加的工作.
其二, 在 finalize 运行完成之后, 该对象可能变成可达的, GC 还要再检查一次该对象是否是可达的. 因此, 使用 finalize 会降低 GC 的运行性能.
其三, 由于 GC 调用 finalize 的时间是不确定的, 因此通过这种方式释放资源也是不确定的.
通常, finalize 用于一些不容易控制, 并且非常重要资源的释放, 例如一些 I/O 的操作, 数据的连接. 这些资源的释放对整个应用程序是非常关键的. 在这种情况下, 程序员应该以通过程序本身管理 (包括释放) 这些资源为主, 以 finalize 函数释放资源方式为辅, 形成一种双保险的管理机制, 而不应该仅仅依靠 finalize 来释放资源.
下面给出一个例子说明, finalize 函数被调用以后, 仍然可能是可达的, 同时也可说明一个对象的 finalize 只可能运行一次.
- class MyObject {
- Test main; // 记录 Test 对象, 在 finalize 中时用于恢复可达性
- public MyObject(Test t) {
- protected void finalize() {
- main = t;// 保存 Test 对象
- }
- System.out.println("This is finalize");// 用于测试 finalize 只运行一次
- main.ref = this;// 恢复本对象, 让本对象可达
- }
- }
- class Test {
- Test test = new Test();
- MyObject ref;
- public static void main(String[] args) {
- test.ref = null; // MyObject 对象为不可达对象, finalize 将被调用
- test.ref = new MyObject(test);
- System.gc();
- if (test.ref != null)
- }
- System.out.println("My Object 还活着");
- }
运行结果:
This is finalize
My Object 还活着
此例子中需要注意, 虽然 MyObject 对象在 finalize 中变成可达对象, 但是下次回收时候, finalize 却不再被调用, 因为 finalize 函数最多只调用一次.
七, GC 程序交互
程序如何与 GC 进行交互呢? Java2 增强了内存管理功能, 增加了一个 java.lang.ref 包, 详见我的开源项目: src-jdk1.7.0_02
其中定义了三种引用类. 这三种引用类分别为: SoftReference, WeakReference, PhantomReference
通过使用这些引用类, 程序员可以在一定程度与 GC 进行交互, 以便改善 GC 的工作效率, 这些引用类的引用强度介于可达对象和不可达对象之间.
创建一个引用对象也非常容易, 例如: 如果你需要创建一个 Soft Reference 对象, 那么首先创建一个对象, 并采用普通引用方式(可达对象); 然后再创建一个 SoftReference 引用该对象; 最后将普通引用设置为 null. 通过这种方式, 这个对象就只有一个 Soft Reference 引用. 同时, 我们称这个对象为 Soft Reference 对象.
Soft Reference 的主要特点是据有较强的引用功能. 只有当内存不够的时候, 才进行回收这类内存, 因此在内存足够的时候, 它们通常不被回收. 另外, 这些引用对象还能保证在 Java 抛出 OutOfMemory 异常之前, 被设置为 null. 它可以用于实现一些常用图片的缓存, 实现 Cache 的功能, 保证最大限度的使用内存而不引起 OutOfMemory. 以下给出这种引用类型的使用伪代码:
- // 申请一个图像对象
- Image image=new Image();// 创建 Image 对象
- ...
- // 使用 image
- ...
- SoftReference sr=new SoftReference(image);
- // 使用完了 image, 将它设置为 soft 引用类型, 并且释放强引用;
- image=null;
- ...
- image=new Image();// 由于 GC 由于低内存, 已释放 image, 因此需要重新装载;
- // 下次使用时
- if (sr!=null)image=sr.get();
- else{
- }
- sr=new SoftReference(image);
Weak 引用对象与 Soft 引用对象的最大不同就在于: GC 在进行回收时, 需要通过算法检查是否回收 Soft 引用对象, 而对于 Weak 引用对象, GC 总是进行回收. Weak 引用对象更容易, 更快被 GC 回收. 虽然, GC 在运行时一定回收 Weak 对象, 但是复杂关系的 Weak 对象群常常需要好几次 GC 的运行才能完成. Weak 引用对象常常用于 Map 结构中, 引用数据量较大的对象, 一旦该对象的强引用为 null 时, GC 能够快速地回收该对象空间.
Phantom 引用的用途较少, 主要用于辅助 finalize 函数的使用. Phantom 对象指一些对象, 它们执行完了 finalize 函数, 并为不可达对象, 但是它们还没有被 GC 回收. 这种对象可以辅助 finalize 进行一些后期的回收工作, 我们通过覆盖 Reference 的 clear()方法, 增强资源回收机制的灵活性.
八, Java 编程建议
根据 GC 的工作原理, 我们可以通过一些技巧和方式, 让 GC 运行更加有效率, 更加符合应用程序的要求. 一些关于程序设计的几点建议:
1)最基本的建议就是尽早释放无用对象的引用. 大多数程序员在使用临时变量的时候, 都是让引用变量在退出活动域 (scope) 后, 自动设置为 null. 我们在使用这种方式时候, 必须特别注意一些复杂的对象图, 例如数组, 队列, 树, 图等, 这些对象之间有相互引用关系较为复杂. 对于这类对象, GC 回收它们一般效率较低. 如果程序允许, 尽早将不用的引用对象赋为 null, 这样可以加速 GC 的工作.
2)尽量少用 finalize 函数. finalize 函数是 Java 提供给程序员一个释放对象或资源的机会. 但是, 它会加大 GC 的工作量, 因此尽量少采用 finalize 方式回收资源.
3)如果需要使用经常使用的图片, 可以使用 soft 应用类型. 它可以尽可能将图片保存在内存中, 供程序调用, 而不引起 OutOfMemory.
4)注意集合数据类型, 包括数组, 树, 图, 链表等数据结构, 这些数据结构对 GC 来说, 回收更为复杂. 另外, 注意一些全局的变量, 以及一些静态变量. 这些变量往往容易引起悬挂对象(dangling reference), 造成内存浪费.
5)当程序有一定的等待时间, 程序员可以手动执行 System.gc(), 通知 GC 运行, 但是 Java 语言规范并不保证 GC 一定会执行. 使用增量式 GC 可以缩短 Java 程序的暂停时间.
来源: https://yq.aliyun.com/articles/653563