java相较于c、c++语言的优势之一是自带垃圾回收器,垃圾回收是指不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null,可以通过System.gc方法进行回收处理
我们一般声明对象时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,就说明他不是垃圾,则不会被垃圾回收。
软引用
软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。
弱引用
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉该类对象。弱引用对象在回收时会被放入引用队列(ReferenceQueue)。
虚引用
虚引用被称为幽灵引用或幻象引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得对象实例。任何时候都可能被回收,一般用来跟踪对象被垃圾收集器回收的活动,起哨兵作用。必须和引用队列(ReferenceQueue)联合使用。
这些概念可能有点抽象,不过这四种引用具有不同的垃圾回收时机应该是很清楚的。我们会发现,引用的强度从强、软、弱、虚依次递减,越往后的引用所引用的对象越容易被垃圾回收。
引用计数算法是判断对象是否存活的算法之一:它给每一个对象加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被使用的,即将被垃圾回收器回收。
缺点:
public class GcTest {
public static void main(String[] args) {
MyObject myObject_1 = new MyObject();
MyObject myObject_2 = new MyObject();
myObject_1.instance = myObject_2;
myObject_2.instance = myObject_1;
myObject_1 = null;
myObject_2 = null;
System.gc();
}
// 对象循环引用,用引用计数算法时,无法回收这两个对象
目前主流使用的都是可达性分析算法来判断对象是否存活。算法基本思路:以“GC Roots”作为对象的起点,从此节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(Native方法)引用的对象;
活跃线程的引用对象。
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
在可达性分析算法中被标记为不可达的对象,也不一定是一定会被回收,它还有第二次重生的机会。每一个对象在被回收之前要进行两次标记,一次是没有关联引用链会被标记一次,第二次是判断该对象是否覆盖finalize()方法,如果没有覆盖则真正的被定了“死刑”。
代码实现
public class FinalizeTest {
public static FinalizeTest ft;
/**
* 用于判断对象是死亡还是存活
*/
public static void judge(){
if(ft == null){
System.out.println("i am dead");
}else{
System.out.println("i am alive");
}
}
public static void main(String[] args) throws InterruptedException {
ft = new FinalizeTest();
// 将引用指向null,那么对象就没有任何关联了
ft = null;
// 触发一次gc
System.gc();
// 因为Finalizer线程的优先级低,因此sleep 1秒后再看结果
Thread.sleep(1000);
//因为FinalizeTest对象覆盖了finalize方法,并在该方法中重新建立与引用的关联,所以对象会复活
judge();
//下面的代码和上面的一模一样,但是对象不会再复活了,因为finalize方法最多执行一次
ft = null;
System.gc();
Thread.sleep(1000);
judge();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("执行finalize方法");
// 对象复活的关键:重新建立与引用的关联
ft = this;
}
}
确认完垃圾之后肯定要想办法回收垃圾。回收垃圾主要有下面四种方法:标记清除算法、标记整理算法、复制算法、分代收集算法。
算法思路:算法分为“标记”和“清理”两个步骤,首先标记处所有需要回收的对象,在标记完成后再统一回收所有被标记的对象。
缺陷:
适用于存活对象占多数的情况。
算法思路:标记过程和标记-清理算法一样,而后面的不一样,它是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
算法思路:将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完后,就将还存活的对象复制到另一块去,然后再把已使用过的内存空间一次清理掉。
缺陷:
可用内存缩小为了原来的一半
算法执行效率高,适用于存活对象占少数的情况。
当前大多数垃圾收集都采用的分代收集算法,这种算法并没有什么新的思路,只是根据对象存活周期的不同将内存划分为几块,每一块使用不同的上述算法去收集。在jdk8以前分为三代:年轻代、老年代、永久代。在jdk8以后取消了永久代的说法,而是元空间取而代之。
在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。虽然对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止没有出现一个最用的的垃圾收集器,更加没有万能的垃圾收集器,能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一任何场景下都适用的完美收集器存在,那么Java虚拟机就不会实现那么多不同的垃圾收集器了。
经典的一些收集器(serial,parnew, CMS,G1)
主要面向服务端的,
服务端, 延迟小的好 . 但当功能为计算数据时,等个30秒再算也没毛病,这时吞吐量优先更合适.客户端,一般要给人用的,自然是低延迟好
*G1整体是标记-整理算法,局部是标记-复制算法, 意味着不会产生空间碎片
可预测停顿时间垃圾收集器
是Region的内存布局形式
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
根据不同类型的Region,选择不同的策略去进行收集管理。
也是分代理论,但是不再是划分为老年代,新生代了
可以管理全堆内存
四个步骤 世界停止的时间比较短
*G1整体是标记-整理算法,局部是标记-复制算法, 意味着不会产生空间碎片
https://blog.csdn.net/m0_59879385/article/details/127516655