1.垃圾收集
程序计数器、虚拟机栈、本地方法栈都是随着方法或者进程的结束而被回收,本地方法区、堆时需要垃圾回收器来来回收的。
堆内存的垃圾回收:
2.判断堆中对象是否需要回收算法:
*引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
*根搜索算法:
在主流的商用程序语言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)判定对象是否存活的。这个算法的基本思路就是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
*引用:
无论计数还是搜索都和引用有关系,在jdk1.2以后,对对象的引用有四种定义,强引用、软引用(在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。)、弱引用(当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。)、虚引用(为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。)。
*对象自救
在根搜索算法中如果对象呗判定为不可达,对象要在finalize()中成功拯救自己—-只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;
package com.shuai.Util;
public class TestGC {
public static TestGC SAVE_HOOK = null;
public void isAlie() {
System.out.println("i am ok ");
}
@Override
public void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method excuted");
TestGC.SAVE_HOOK=this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new TestGC();
// 对象会暂时被判死缓
test();
// 第二次执行此方法会自救失败
test();
}
private static void test() throws InterruptedException {
SAVE_HOOK = null;
// 第一次调用finalize方法对象会被this引用,会进行自救
System.gc();
// finalize方法优先级很低,所以先暂停0.5秒
Thread.sleep(5000);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlie();
} else {
System.out.println(" i am over");
}
}
}
![image.png](https://upload-images.jianshu.io/upload_images/4354067-d48d8072a2bbd0db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
上面的代码运行结果发现,同样的代码,一个可以自救,一个自救不了,这是因为,系统只会执行一次finalize方法。
方法区的垃圾回收
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
废弃常量:
以常量池为例,当常量池中的一个字符串常量没有被任何变量引用时就会被认为是废弃的常量。
无用类:
无用类的定义比较苛刻·:
1.类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收。
3.该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法:
*标记清除算法:
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
*复制算法:
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
*标记整理算法:
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动(美团面试题目,记住是完成标记之后,先不清理,先移动再清理回收对象)
*分代收集算法:
根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
jvm将新生代分为三个区域、一个end区和两个suvivor区。新生代的采用复制算法
新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
垃圾收集器:
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
*serial收集器
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
*ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本。
*Parallel Scavenge收集器
也是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
*Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
*Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
*CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
垃圾回收策略:
- 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
- 大对象直接进入老年代
大对象是指 需要连续内存空间的对象 ,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会 提前触发垃圾收 集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
- 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活, 将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
- 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代, 如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半, 则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
- 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。