垃圾回收
先抛出 3 个问题
- 什么场景下该使用什么垃圾回收策略?
- 垃圾回收发生在哪些区域?
- 对象在什么时候能够被回收?
什么场景下该使用什么垃圾回收策略?
在对内存要求苛刻的场景,想办法提高对象的回收效率,多回收掉一些对象,腾出更多内存
在CPU使用率高的情况下,降低高并发时垃圾回收的频率,让出更多的 CPU去执行你的业务而不是垃圾回收
垃圾回收发生在哪些区域?
虚拟机栈,本地方法栈,程序计数器,这三个区域都是线程独占的,随着线程的创建而创建,销毁而销毁,所以这三块区域是不需要考虑垃圾回收的
堆和方法区是线程共享的,这里才是需要考虑垃圾回收的区域。堆是垃圾回收的主要区域,主要回收的 是创建的对象,方法区主要回收废弃的常量和不需要使用的类
对象在什么时候能够被回收?
有多种判断方法
- 引用计数法
通过对象的引用计数器来判断该对象是否被引用
如果一个对象被另一个对象引用,计数器就加一,如果这个对象不再被其他对象引用,计数器就减一,直到计数器为 0,计数器为 0 的时候就可以被垃圾回收
但会出现一个问题:循环引用
比如说 A 引用 B,B 引用 C,C 引用 A,那么这个 3 个对象永远都不能回收,所以 java 并没有 用这种算法
- 可达性分析
以根对象( GC Roots )作为起点向下搜索,走过的路径被称为引用链( Reference Chain) ,如果某个对象到根对象没有引用链相连时,就认为这个对象是不可达的,可以回收
GC Roots包括哪些对象?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI (即Native方法)引用的对象
为什么这些对象可以当做 GC Roots???
引用
-
强引用( Strong Reference)
形如Object obj = new Object()的引用,只要强引用在,永远不会回收被引用的对象 -
软引用( Soft Reference)
形如SoftReference sr = new SoftReference< > (“hello”)
是用来描述一些有用但非必需的对象
软引用关联的对象,只有在内存不足的时候才会回收
基于这个特性,可以用来实现缓存 -
弱引用( Weak Reference)
形如WeakReference sr = new WeakReference<> (“hello”)
弱引用也是用来描述非必需对象的
无论内存是否充足,都会回收被弱引用关联的对象 -
虚引用( Phantom Reference)
形如
ReferenceQueue queue = new ReferenceQueue<>
PhantomReference pr = new PhantomReference <>(“hello”, queue);
不影响对象的生命周期,如果一个对象只有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须和引用队列( ReferenceQueue)配合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存波回收之前采取必要的行动
可达性算法注意点
一个对象即使不可达,也不一定会被回收,对象不可达只是给这个对象判了死缓,并不会
finalize()的建议
- 避免使用finalize()方法,操作不当可能会导致问题
- finalize()优先级低,何时会被调用无法确定,因为什么时间发生GC不确定;
- 建议使用try…catch…finally来替代finalize()
垃圾回收算法
- 标记-清除( Mark-Sweep )
标记需要回收的对象(这个就是根据可达性分析标记需要回收的对象)
清理掉要回收的对象
标记清除算法清理的时候可能会产生内存碎片,下次再来一个大对象的时候,可能就没法存了
所以就有了下面的垃圾回收算法
-
标记-整理( Mark-Compact )
标记需要回收的对象
把所有的存活对象压缩到内存的一端
清理掉边界外的所有空间 -
复制( Copy )
把内存分为两块,每次只使用一块
将正在使用的内存中的存活对象复制到未使用的内存中去,然后清除掉正在使用的内存中的所有对象,交换两个内存的角色,等待下次回收
三种算法对比
回收算法 | 优点 | 缺点 |
---|---|---|
标记-清除 | 实现简单 | 存在内存碎片、分配内存速度会受影响 |
标记-整理 | 无碎片 | 整理存在开销 |
复制 | 性能好、无碎片 | 内存利用率低 |
- 分代收集算法
把内存分成多个区域,不同区域使用不同的回收算法回收对象
如果需要回收的内存非常大,一次回收所有垃圾,耗费的时间会非常长,可能会造成系统长时间的停顿
- 增量算法
每次只收集一小片区域的内存空间的垃圾,这样可以减少系统的停顿
分代收集算法
各种商业虚拟机堆内存的垃圾收集基本上都采用了分代收集
根据对象的存活周期,把内存分成多个区域,不同区域使用不同的回收算法回收对象
回收类型
- 新生代回收(Minor GC | Young GC)
- 老年代回收( Major GC )
- 清理整个堆( Full GC )
- Major GC≈Full GC
对象创建的时候会先存放到 Eden,Eden满了就会触发垃圾回收,这个回收的过程是,把 eden 存活的对象拷贝到存活区的 From Survivor或to Survivor,比如这次拷贝到 From Survivor,下次回收的时候就会拷贝到 to Survivor,如此来回多次,默认是 15 次之后,会晋升到老年代,老年代一般使用标记清除,或标记整理回收,因为老年代的对象一般都存活的比较长,只有少部分对象需要清理
新建的对象不一定分配到伊甸园
对象大于XX:PretenureSizeThreshold , 就会直接分配到老年代
新生代空间不够
对象不一定要达到年龄才进入老年代
动态年龄:如果Survivor空间中所有相同年龄对象大小的总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就可以直接进老年代
触发垃圾回收的条件新生代( Minor GC )
伊甸园空间不足
触发垃圾回收的条件-老年代( Full GC )
- 老年代空间不足(1.垃圾碎片引起的不足 2.本身空间不足 )
- 元空间不足
- 要晋升到老年代的的对象所 占用的空间大于老年代的剩余空间
- 显式调用System.gc()
建议垃圾回收器执行垃圾回收
-XX:+DisableExplicitGC参数,忽略掉System.gc()的调用
分代的好处
更有效的清除不再需要的对象
提升了垃圾回收的效率
分代收集算法调优原则
合理设置Survivor区域的大小,避免内存浪费
让GC尽量发生在新生代,尽量减少Full GC的发生