1.GC 是什么?GC 的作用是什么?
背景:
程序员在内存处理方面出现问题,例如忘记、错误回收内存,导致程序或系统的不稳定甚至崩溃。
GC (GabageCollection)是指垃圾回收。Java 提供 GC 功能,可以自动监控内存区域是否超过作用域,实现自动回收内存的目的。
说明:
Java 语言没有提供显式方法来分配和释放内存。
2.简述 Java 垃圾回收机制及其优点
Java 垃圾回收机制实现了自动回收内存的功能。JVM 有垃圾回收线程(守护线程),其优先级较低,在正常情况下是不会执行的。当 JVM 空闲、heap 内存、Metaspace 内存不足时,JVM 会执行垃圾回收线程。
Java 垃圾回收的步骤:
① 判断对象 A 是否存活。采用引用计数法或可达性分析,例如后者从 GC Roots 开始搜索,当没有任何的 GC Roots 与对象 A 相连时,则对象 A 是不可达对象,可以判定为可回收对象。(GC Roots 是堆外指向堆内的引用)
② 触发 GC 的时机。Minor GC 发生在 Heap 新生代,例如 Eden、FromSuv、ToSuv 满了都会触发 Minor GC。Full GC 发生在 Heap 老年代和 Metaspace,例如调用System.gc、Heap 老年代空间不足、Metaspace 空间不足等。
③ 进行 GC。GC 算法是内存回收的理论方法,GC 垃圾收集器则是具体实现。GC 算法有标记清除法、复制算法、标记压缩算法。
JVM 是采用分代垃圾回收机制,本文通过某对象的完整 GC 过程,理解 Java 垃圾回收机制,可参考《Java 虚拟机原理》5.1 GC垃圾收集及案例分析 — GC案例分析
Java 垃圾回收机制的优点
① 不需要考虑内存管理;
② 有效地防止内存泄漏;
③ 有效地利用内存;
④ Java 中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。
Java 垃圾回收机制的缺点
① 垃圾回收机制的目标是在 JVM Heap 和 Metaspace 内存中,回收无用对象的内存空间,因此无法回收数据库连接、Socket、I/O 等物理资源;
② 垃圾回收机制具有不可预知性,通过 System.gc()、对象引用设置为 null 等手段促进垃圾回收,但不能精确控制垃圾回收机制的运行;
③ 垃圾回收机制的潜在缺点是它的开销会影响性能;
3.如何判断一个对象是否存活?
(1)判断对象存活的方法
引用计数法
每个对象都有一个引用的计数值,当该对象新增一个引用时,该计数值加 1;反之,当该对象减少一个引用时,该计数值减 1。如果计数值为 0,则回收该对象。引用计数法的缺点是不能处理对象的相互循环引用
![](https://img.haomeiwen.com/i21744606/eefb0726cffc7e2e.png)
可达性分析
从 GC Roots 开始搜索,当没有任何的 GC Roots 与对象 A 相连时,则对象 A 是不可达对象,可以判定为可回收对象。(GC Roots 是堆外指向堆内的引用)
![](https://img.haomeiwen.com/i21744606/6b812eebee568479.png)
GC Roots:
由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)下列几种,Java 方法栈桢中的局部变量、已加载类的静态变量、JNI handles、已启动且未停止的 Java 线程等。因此,GC Roots 是 GC 对象的引用。
(2)强引用、软引用、弱引用、虚引用的区别
强引用 StrongReference
例如,new 出来的对象,JVM 即使出现 OutOfMemory 错误,也不会垃圾回收该对象。
Object obj = new Object();
软引用 SoftReference
非必须引用,内存溢出之前回收。
Object obj = new Object(); //强引用
ReferenceQueue<Object> referenceQueuee = new ReferenceQueue<>(); //引用队列
// 软引用和引用队列联合使用,如果软引用所引用的对象被垃圾回收器回收,JVM就会把这个软引用加入到引用队列中
SoftReference softReference = new SoftReference(str, referenceQueuee);
注意:一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,同时维护系统的运行安全,防止 OutOfMemory 等问题。
弱引用 WeakReference
第二次垃圾回收时回收。
@Test
public void testWeakReference() throws InterruptedException {
ReferenceQueue<Object> referenceQueuee = new ReferenceQueue<>();
Object weakObject = new Object();
//弱引用
WeakReference weakReference = new WeakReference(weakObject, referenceQueuee);
System.out.println("WeakReference:" + weakReference.get());
System.out.println("referenceQueuee:" + referenceQueuee.poll());
weakObject = null;
System.gc();
Thread.sleep(2000);
System.out.println("WeakReference:" + weakReference.get());
System.out.println("referenceQueuee:" + referenceQueuee.poll());
}
// 测试结果
WeakReference:java.lang.Object@694f9431
referenceQueuee:null
WeakReference:null
referenceQueuee:java.lang.ref.WeakReference@f2a0b8e
虚引用 PhantomReference
虚引用是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
4.简述 Java 中的内存泄漏
内存泄露是指一个不再被程序使用的对象或变量一直占据内存。
在 Java 垃圾回收机制中,如果对象不再被引用(GC Roots 的可达性分析),则垃圾收集器会从内存中清除该对象。
(1)长生命周期的对象持有短生命周期对象的引用
由于短生命周期对象已经不再需要,而长生命周期对象持有它的引用而导致不能被回收。通俗地说,程序创建了一个对象,之后一直不再该对象,但这个对象却一直被引用,导致无法被垃圾收集器回收。
① static 字段引起的内存泄露
在 Java 中,静态字段与整个程序的生命周期一致。如果静态对象是不断创建、增大,可能导致 OutOfMemory。
静态集合类:HashMap、LinkedList 等,容器内的对象在程序结束前都不能被回收。
单例模式:如果单例对象 A 持有外部对象 B 的引用,那么对象 B 将不能被 JVM 正常回收,导致内存泄露。
② 引用了外部类的内部类
一个外部类实例对象 B 的方法返回了一个内部类 B.Inner 的实例对象。如果 B.Inner 被长期引用,即使对象 B 不再被使用。因为 B.Inner 持有对象 B 的引用,所以对象 B 将不会被垃圾回收,导致内存泄露。解决方法:采用静态内部类。
为什么非静态内部类持有外部类引用,静态内部类不持有外部引用?
static 调用 static,非 static 调用非 static。即 static --> 针对 class, 非static -> 针对 对象。
(2)长生命周期的对象本身不释放
数据库连接、网络连接和 IO 连接等
(3)改变哈希值
不正确地重写 equals() 和 hashCode(),在 HashMap、HashSet 等种集合中,常常用到 equal() 和 hashCode() 来比较对象,如果重写不合理,将会成为潜在的内存泄露问题。
排除内存泄露的样例
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
解答:当进行大量的 pop 操作时,引用未置空使得 GC 是不会回收数组的对象。
![](https://img.haomeiwen.com/i21744606/cb7457b353863d48.png)
解决方法:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
![](https://img.haomeiwen.com/i21744606/0904814570cbdc12.png)
5.简述 JVM 的一次完整 GC 流程
(1)Java 对象的创建及初始化
![](https://img.haomeiwen.com/i21744606/660eafe2b0ee9524.png)
① 类加载检查。JVM 遇到 new 指令,首先去检查该类是否在常量池中有符合引用,并且检查该类是否被加载、解析和初始化。如果没有,则执行类加载流程。
② 内存分配。在类加载检查后,可知对象所需要的内存大小。JVM 采用“指针碰撞”或者“空闲列表”,为对象进行内存分配。
![](https://img.haomeiwen.com/i21744606/e4cc260980d337d4.png)
③ 初始化零值。JVM 为分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,例如,Object 对象的零值为 null,int 的零值为 0 等。
④ 设置对象头。JVM 为对象设置对象头,包括该对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等。
⑤ 执行 init 方法。init 方法即程序中对象的构造函数显示的内容。
网友评论