基础知识
什么是垃圾
简单说就是没有被任何引用指向的对象就是垃圾。后面会有详细说明。
和C++的区别
java:GC处理垃圾,开发效率高,执行效率低
C++:手工处理垃圾,如果忘记回收,会导致内存泄漏问题。如果回收多次,则会导致非法访问问题。开发效率低,执行效率高
如何定位垃圾
引用计数法
英文:reference count,当有引用指向对象时,给引用的数量计数,当引用数量等于0时,对象就被判定为垃圾。
这种方法无法解决的问题:循环引用。例如:A > B > C > A,彼此互相引用,引用计数都是1,但其实没有任何其他引用指向他们。使用引用计数法,这些对象将永远无法垃圾回收。所以需要其他算法。Python用的是这种算法。
根可达算法
英文:Root Searching,这是JVM实际使用的算法。首先定义好一系列的根对象(GC Root),如果一个对象向上追溯没有被GC Root引用就是垃圾。那么GC Root有哪些?
线程栈变量
从main方法开始,会启动一个主线程,然后会有一个main的栈帧,这里面中的对象就是线程栈变量。
静态变量
静态变量一个类只有一个,当类初始化时,马上就会引用到静态变量,所以静态变量也是Root对象。被静态变量引用的对象不能被回收。
常量池
Runtime constant pool,常量池中引用指向的对象,类似静态变量。
JNI指针
本地方法(native)用到的类或者对象。
垃圾回收算法
标记清除(mark sweep)
第一遍:扫描整个堆,标记出可回收的垃圾。
第二遍:扫描整个堆,将刚刚标记出来的垃圾清除。
缺点:
对象清除以后内存不连续会产生空间碎片
标记和清除比较耗时,效率比较低。
拷贝算法 (copying)
也叫复制算法,将内存划分为两块相等的区域,每次只使用其中一块,当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
缺点:空间浪费,复制操作会增加额外开销。
优点:空间连续,没有碎片
适用场景:少量对象存活的场景
标记压缩(mark compact)
复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。
标记过程与"标记-清除"算法类似,但是后续步骤不是直接对可回收对象进行清理,而是将所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
没有碎片,效率偏低(两遍扫描,指针需要调整)
内存分代模型
内存分代模型也有另一个名字:分代收集算法。这种算法整合了以上所有算法的优点,最大程度避免了它们的缺点。
与其说它是算法,倒不是说它是一种策略,因为它是把上述几种算法整合在了一起。
内存分代模型是部分垃圾回收器使用的模型,也有新的垃圾回收器是不分代的。例如:Epsilon,ZGC,Shenandoah。G1 是逻辑分代,物理不分代。
根据实际场景,其实大部分的对象都很短命,一般来说,98% 的对象都是朝生夕死的,所以分代收集算法根据对象存活周期的不同将堆分成新生代和老年代。
如下图:新生代和老年代的默认比例为 1 : 2,新生代又分为 Eden 区, from Survivor 区(简称 S0 ),to Survivor 区(简称 S1 ),三者的比例为 8: 1 : 1。
一个对象从出生到死亡的过程
对象分配过程
1. 刚刚诞生的新对象首先会尝试在栈上分配。什么样的对象会分配到栈上?
-
线程私有小对象。
-
无逃逸。(就在某1个方法中使用的对象,没有其他地方引用)
-
支持标量替换(例如:某些对象就2个int属性,那就可以用2个int来代替整个对象)
-
(一般无需调整)
-
如果在栈上分配不下且这个对象又很大,则会直接进入老年代。
- 多大算大,是由一个参数控制的。
-
如果栈分配不下且对象不够大,默认优先放到eden区的TLAB(Thread Local Allocation Buffer)
- 每个线程独有区域,默认占用eden的1%
- 避免多线程的时候eden空间的竞争,提高效率
- 小对象(证明TLAB的案例)
- 通过
-XX:-UseTLAB
可以关闭(一般无需调整)
-
eden区的对象经过一次垃圾回收之后,能回收的直接回收了。不能回收的,会移动到S1区。
-
再回收1次就会连同eden区的某些存活对象一起进入S2区。再回收又会进入S1区,以此往复。每次回收都会让年龄+1。
-
当对象的年领大于设置的值时,对象就会进入到老年代。具体要符合下面条件的任意一个。
- 年龄大于XX:MaxTenuringThreshold中配置的值。不同的垃圾回收器,默认值不同。
- Parallel Scavenge:15
- CMS:6
- G1:15
- 由于JVM对象结构中GC年龄是4位,所以年龄最大就是15。
- 动态年龄
- 当S1把符合条件的对象拷贝到S2时(反过来也一样)
- 如果此时S2的空间占用超过50%,就会把年龄最大的对象放入老年代
- 分配担保
- YGC期间 survivor区空间不够了 空间担保直接进入老年代
- 参考:https://cloud.tencent.com/developer/article/1082730
- 年龄大于XX:MaxTenuringThreshold中配置的值。不同的垃圾回收器,默认值不同。
-
老年代
- 顽固分子
- 老年代满了FGC Full GC
证明TLAB的案例
下面的代码,大家可以使用idea和jvm的默认参数执行一下,然后记录一下平均执行时间。
然后加上这些jvm参数后在执行一下:-XX:-DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB
,应该会发现执行时间翻了接近一倍。
这些参数的含义。-XX:-DoEscapeAnalysis 关闭逃逸分析,-XX:-EliminateAllocations关闭标量替换,-XX:-UseTLAB关闭TLAB。关闭这些功能后会导致所有的对象都直接在eden区创建而不是栈中或者tlab,从而导致程序性能下降。
public class TestTLAB {
class User {
int id;
String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
void alloc(int i) {
new User(i, "name " + i);
}
public static void main(String[] args) {
TestTLAB t = new TestTLAB();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000_0000; i++) {
t.alloc(i);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
GC特点
当新生代空间不足时发生的 GC 称为 Young GC(YGC,也叫 Minor GC )
当老年代空间不足时发生的 GC 称为 MajorGC(也称为 Full GC )。此时新生代和老年代同时GC。
Minor GC 非常频繁,一般回收速度也比较快;出现了 Full GC,经常会伴随至少一次的 Minor GC,Full GC 的速度一般会比 Minor GC 慢10倍以上。