JVM GC(1) | 内存结构与GC基础
(下文内容都是针对HotSpot VM)
JVM内存结构
图示
- 线程间共享(绿)
- 线程间独立(蓝)
介绍
线程共享
1 方法区(永生代)
- 存储已经被加载的类信息、static变量.
- 1.1 同时放置运行时常量,运行期间动态产生的常量也放在这
- 参数
-XX:MaxPermSize
SE8后改为MetaSpace
2 堆
较复杂,见下文
线程独立
- 栈
Sun Hotspot不区分虚拟机栈和方法栈- 3 虚拟机栈 (Java Virtual Machine Stacks)
- Java方法执行的内存模型,类似C++,每个方法调用都会在栈上留下局部变量表,操作数栈,动态链接,方法出口等, 调用完成就出栈
- 4 本地方法栈 (Native Method Stacks)
- 作用类似,区别在于为虚拟机执行native方法
- 3 虚拟机栈 (Java Virtual Machine Stacks)
- 5 程序计数器 (Program Counter Register)
- 记录线程切换时原来代码段的地址
- 占空间很小
- 6 直接内存
- 不在JVM的范围内,就是不用经过JVM管理、映射的直接内存
GC过程
四种引用类型
强引用(Strong Reference)
1
2Object object = new Object();
// 只要强引用存在,垃圾回收就不会回收该对象,内存不足时会抛出OOM。软引用(Soft Reference)
1
2SoftReference<Object> softReference = new SoftReference<>(new Object());
//定义:非必须,但仍有用的对象。内存不足时才会回收。further reading: 怎么定义内存不足
Interval(该引用多少MS没访问过) = clock - timesatmp > heap_free_space_after_last_gc(MB) * SoftRefLRUPolicyMSPerMB(JVM paramrter, default value is 1000ms))
Related Code1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// https://github.com/AdoptOpenJDK/openjdk-jdk12u/blob/master/src/java.base/share/classes/java/lang/ref/SoftReference.java
public class SoftReference<T> extends Reference<T> {
private static long clock; //Timestamp clock, updated by the garbage collector
private long timestamp; //Timestamp updated by each invocation of the get method.
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
//https://code.googlesource.com/edge/openjdk/+/refs/heads/jdk8u111-b09/hotspot/src/share/vm/memory/referencePolicy.cpp
bool LRUCurrentHeapPolicy::should_clear_reference(...) {
jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
assert(interval >= 0, "Sanity check");
// The interval will be zero if the ref was accessed since the last scavenge/gc.
if(interval <= _max_interval) { return false; }
}弱引用(Weak Reference)
1
2WeakReference<Object> weakReference = new WeakReference<>(new Object());
//定义:发生GC时就会被回收虚引用(Phantom Reference)
1
2
3ReferenceQueue queue = new ReferenceQueue();
PhantomReference<Object> pReference = new PhantomReference<>(new Object(),queue);
//不影响对象的生命周期,初始化必须有队列 只是在对象释放后,虚引用会进ReferenceQueue队列。
对于三种非强引用,一般都要结合referenceQueue使用,用于捕获哪些reference被释放了。如无必要,自己写代码请只用强引用。对该部分不理解也不影响对GC过程的理解。
垃圾标记算法
引用计数 Reference Counting Collector
python默认采用的策略
- 优:
- 简单,就是类似C++的shared pointer。 早期做法。
- 缺:
- 无法鉴别循环引用,即
- 效率变慢,每次指针操作可能修改计数。
1
2
3a.next = b;
b.next = a;
a = b = null;
根搜索算法 Root Tracing Collector
Java和C#默认采用的策略
过程
- 通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
- 找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
- 重复(2)。
- 搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
根集(Root Set)
就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),包含
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
标记过程
关键点: 高优先级别,全局暂停标记; 低优先级异步释放
注意点
- 标记只能在静态内存结构上运行,JVM需要首先达到safepoint,然后标记过程全局暂停Stop the World(STW),
- 执行(暂停)时间是看存活对象的个数,与堆大小无关,对象大小无关。
- 至于待释放对象个数,一般不相关,只有标记-清理算法 时间复杂度和待释放对象个数有关。
safepoint 是一个big topic,以后有空再另外写一篇吧。
从标记到删除
至少需要两次标记
是否需要finalize的标记: 根未搜索、第一次标记、到且有必要执行finalize的对象打上标记放入F-Queue等待释放队列。
- 未执行过finalize
- finalize未被重载
虚拟机自动建立的、低优先级的Finalizer线程去执行对象的finalize()方法
对象执行finalize()如果仍旧没有被引用,就开始执行回收流程
为什么需要finalize()而不直接回收
因为不是所有内存都是JVM自己管理的,比如开辟的直接内存,就需要自己显式的申请和删除
回收算法
标记-清除 mark-sweep
标记后直接在内存中原地清除对象
- 优:
- 对象不需要移动
- 只操作不存活对象,存活对象多也不影响效率
- 劣:
- 标记清除效率低,因为需要维护一个空闲表
- 清除产生大量不连续的内存碎片
- 标记清除效率低,因为需要维护一个空闲表
- 适合:
- 希望暂停时间尽可能短的
- 存活比例高的
标记-整理 mark-compact
不直接对回收对象进行清理,而是所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 优:
- 整理后无碎片=>内存分配更加快速, 空闲大小和位置便于统计
- 劣:
- 因为要移动存活的变量,和重新引用赋值, 就需要执行暂停,使得GC时间延长
- 适合:
- 存活比例高
- 回收不频繁
停止-拷贝 copying
拿空间换时间,将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面)。
只有对象区与空闲区的切换过程中,程序暂停执行。
- 优:
- 无碎片同mark-compact
- 标记和复制可以同时执行
- 每次只对一块内存进行回收,高效
- 实现简单
- 劣:
- 额外空间,空间利用率降低
- 也需要暂停
- 适合:
- 存活比例少
- 内存申请次数多
- 回收不频繁
其他
Adaptive 算法:垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。
堆
所有new创建的对象都在堆中分配
分代示意图
对象分配原则
- 对象优先在Eden分配。
- 大对象直接进入老年代。
- S1满时,S1所有数据进入老年代。
- 长期存活(年龄超过15)的对象将进入老年代: 每次Minor GC使得Survivor区内的对象年龄+1
Minor GC 和 Full GC
- 新生、年轻代 Young Generation: 对象死亡率高,少量存活, 适合
停止-拷贝
- Eden区 80%: 大部分对象在此生成
- 一般满时触发 Minor GC(Scavenge GC) : 从Eden
停止-拷贝
到Survivor0,直到Eden清空位置
- 一般满时触发 Minor GC(Scavenge GC) : 从Eden
- Survivor0 10%
- 满时 交换S0和S1,(本质就是就是把数据从0转到1),完成后S0和Eden都为空
- Survivor1 10%
- 满时将数据转移到 老年代
- Eden区 80%: 大部分对象在此生成
- 年老: 对象存活率高,适合
标记-整理
,如果希望暂停时间减少,也可考虑标记-删除
- 一般满时 触发 Full GC : 新生代、老年代、永生代都进行回收
- 永生: 不怎么需要管理
- 写满时也会触发 Full GC
这个永生在JAVA SE8后被移除,改为MetaSpace,毕竟从意义上就是记录了程序的元信息。
各代各部分比例都可以通过参数调整
另外 Full GC可以通过System.gc()显示调用;
一些经验
减少GC开销的技巧
- 没经验不要显式调用System.gc(),因为增加了暂停次数
- 减少临时对象的使用
- 对象不用时最好显式置为Null,手动悬空,加快GC
- 使用StringBuffer,而不用String来累加字符串
- 能用基本类型如Int,Long,就不用Integer,Long对象
- 尽量少用静态对象变量: 静态变量属于全局变量,不会被GC回收,它们会一直占用内存
- 散对象创建或删除的时间: 集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,也是类似的。
Java 可能出现内存泄露的情况
即使根搜索看起来天衣无缝,依旧存在问题,如下:
- 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
- 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。
GC性能调优 和 主流GC对比分析
建议看我的下一篇