GC是什么?为什么要GC?
GC( Garbage Collection ),垃圾回收,是Java与C++的主要区别之一。作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码。这是因为在Java虚拟机中,存在自动内存管理和垃圾清理机制。对JVM中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,保证JVM中的内存空间,防止出现内存泄露和溢出问题。
GC是任意时候都能进行的吗
GC垃圾收集只能在安全点才能进行。在Java虚拟机(JVM)中,安全点(Safe Point)是程序执行的某些特定位置。JVM只能在安全点安全地暂停执行,从而进行垃圾回收(GC)等操作。安全点的设定确保了当线程暂停时,程序的状态是可知和一致的。
如何判断一个对象是否存活?
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
优点:可即刻回收垃圾,当对象计数为0时,会立刻回收;
弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。JVM不用这种算法
可达性分析算法
通过 GC Root 对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到 GC Root没有任何的引用链相连时,说明这个对象是不可用的。
-
JVM中的垃圾回收器通过可达性分析来探索所有存活的对象
-
扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收
GC Root的对象有哪些?
-
虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程被调用的方法栈用到的参数、局部变量或者临时变量等。
-
方法区中类静态属性引用的对象或者说Java类中的引用类型的静态变量。
-
方法区中常量引用的对象或者运行时常量池中的引用类型变量。
-
本地方法栈中JNI(即一般说的Native方法)引用的对象
-
JVM内部的内存数据结构的一些引用、同步的监控对象(被修饰同步锁)。
finalize()方法的作用?
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。(Java 9中已弃用)
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
Java种有哪些引用类型?有什么特点?
-
强引用:gc时不会回收
-
软引用:只有在内存不够用时,gc才会回收
-
弱引用:只要gc就会回收;弱引用对象非常适合于实现 Map 的缓存(weakHashMap),当对象只通过弱引用可达时,可以快速释放内存。
-
虚引用:是否回收都找不到引用的对象,仅用于管理直接内存
什么时候需要安全点?安全点的触发条件?
安全点的作用:
- 垃圾收集: 在进行垃圾收集时,JVM需要暂停所有应用程序线程(GC暂停),以确保不会有线程在操作内存。同时,状态的快照是确定的,以便于GC工作。
- 堆栈遍历: 在执行如线程转储(Thread Dump)等操作时,JVM需要安全地遍历线程栈,这时也需要安全点。
- 性能损耗最小化: 通过在最可能长时间运行的指令设置安全点(例如循环的末端、方法的调用与返回),JVM可以减少程序暂停的频率,从而降低性能损耗。
安全点的触发条件:
- 方法调用:每次方法调用都是一个潜在的安全点。
- 循环回跳:长时间循环中间会插入安全点检查。
- 异常处理:处理异常时,也会检查是否到达安全点。
young Gc、old Gc、full Gc 和 mixed Gc 的区别是什么?
Young GC(Minor GC或 YGC),即年轻代垃圾回收:
- 作用范围:仅针对新生代(Eden和S0/S1)。
- 触发条件:当新生代内存(尤其是 Eden 区)被填满时触发。
- 执行方式:只回收新生代中的对象,老年代不受影响。
- 特点:回收频率较高,回收时间较短,因为新生代中的对象大多数是短命对象,容易被回收。
Old Gc(Major GC或OGC),老年代垃圾回收:
- 作用范围:只针对老年代。
- 触发条件:当老年代空间不足时触发,通常是当从新生代晋升到老年代的对象过多,或者老年代的存活对象数量达到一定阈值时。
- 执行方式:只回收老年代的对象,新生代不受影响。
- 特点:执行时间比 Young GC 长,因为老年代中的对象存活时间更长,且数量较多
Full GC,全堆垃圾回收:
- 作用范围:对整个堆内存(包括新生代和老年代)进行回收。
- 触发条件:当老年代空间不足且无法通过老年代垃圾回收释放足够空间,或其他情况导致系统内存压力较大时触发(如 system.gc()调用)
- 执行方式:回收所有代(新生代、老年代)中的垃圾,并且可能会伴随着元空间的回收。
- 特点:回收时间最长,会触发整个JM 的停顿(Stop-The-World),对性能有较大影响,通常不希望频繁发生
Mixed Gc(仅适用于 G1 GC 的混合垃圾回收):
- 作用范围:同时回收新生代和部分老年代区域
- 触发条件:当 G1垃圾回收器发现老年代区域的垃圾过多时触发。
- 执行方式:混合回收新生代和部分老年代区域,主要目的是减少老年代中的垃圾积压。
- 特点:结合了 YGC 的快速回收和 OGC的深度回收,尽量减少停顿时间,适用于大内存应用
对象在堆中的生命周期?
-
在 JVM 内存模型的堆中,堆被划分为新生代和老年代
- 新生代又被进一步划分为 Eden区Survivor区From SurvivorTo Survivor
-
当创建一个对象时,对象会被优先分配到新生代的 Eden 区
- 此时 JVM 会给对象定义一个对象年轻计数器 -XX:MaxTenuringThreshold
-
当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)
-
JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
-
对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
-
-
如果分配的对象超过了 -XX:PetenureSizeThreshold 直接被分配到老年代
内存的分配策略?
-
对象优先在 Eden 分配: 大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,触发 Minor GC
-
大对象直接进入老年代: 当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代,最典型的大对象有长字符串和大数组。可以设置JVM参数 -XX:PretenureSizeThreshold ,大于此值的对象直接在老年代分配。
-
长期存活的对象进入老年代: 通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在 Survivor 区每经过一次 Minor GC ,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。
-
动态对象年龄判定: 并非对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。
-
空间分配担保: 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 是安全的。如果不成立的话虚拟机会查看HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次Minor GC是有风险的;(也就是说,会把原先新生代的对象挪到老年代中) ;如果小于,或者 HandlePromotionFailure 的值为不允许担保失败,那么就要进行一次 Full GC 。
空间分配担保时的 "冒险"是冒了什么风险?
新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。但前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
Young GC的触发条件
在Java 中,Young GC(Minor GC)是针对新生代(Young Generation)对象的垃圾回收。主要有三种情况会会触发 Young GC:
- Eden 区空间不足:新生代被划分为三个区域,Eden区、S0(Survivor0)区和S1(Survivor 1)区,大部分新创建的对象会先分配到 Eden 区。当 Eden 区的对象填满,无法再为新的对象分配空间时,Young GC 会被触发,回收新生代中不再使用的对象。
- Eden 区+Survivor 区都装满:如果 Eden 区和 Survivor 区的空间都不足以存放新分配的对象时,Young GC 也会被触发,清理空间并将幸存的对象转移到 Survivor 区或老年代
- 部分垃圾回收器在 full gc 之前:有一些收集器的回收实现是在 full gc 前会让先执行以下 young gc。比如 Parallel Scavenge,不过有参数可以调整让其不进行 young gc。
Full GC 的触发条件?
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:•
-
用 System.gc(): 只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
-
老年代空间不足: 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组、注意编码规范避免内存泄露。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
-
空间分配担保失败: 当程序创建一个大对象时,Eden区域放不下大对象,使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
-
JDK 1.7 及以前的永久代空间不足: 在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError 。(JDK 8以后元空间不足)
-
Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。