文章目录
一:什么是GC?
GC:garbage collection (垃圾收集)
那么在什么样的玩意儿算是垃圾呢?
1.垃圾是指在运行程序中没有任何指针指向的对象
2.除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象(尤其是一些大的对象)
二:垃圾回收机制作用域
1.GC主要关注方法区以及堆。
2.频繁在新生代收集,很少在老年代收集,几乎不在方法区(永久代/元空间)收集,其中,堆是垃圾收集器的工作重点。
3.引用类型才需要垃圾收集,基本数据类型不需要回收。
4.Java栈不发生垃圾回收。(不需要回收,结束即出栈)
三:垃圾回收相关算法:
1.标记阶段
a.引用计数算法(了解:JAVA中不采用)
1、引用计数法比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
2、对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再 被使用,可进行回收。
优缺点
优点:
- 实现简单,垃圾对象便于辨别;判定效率高,回收没有延迟性。
缺点:- 1)它需要单独的字段存储计数器,增大了空间开销。
- 2)每次赋值都需要更新计数器,增大了时间的开销。
- 3)==最严重的问题:==无法处理循环引用的情况。(这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法)
内存泄露:
这个对象不再使用,但是GC无法回收。
(p指向null,后面三个对象都不再使用了,但是引用计数都不是0,就没法GC回收。)
b.可达性分析算法(根搜索算法/追踪性垃圾收集)
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
所谓"GCRoots”根集合就是一组必须活跃的引用,其基本思路如下:
- 1、可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 2、使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
- 3、如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 4、在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots可以是哪些元素?(所谓GC Roots,就是一组必须活跃的引用)
- 虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 静态变量引用的对象,除非类卸载,否则他引用的对象一直存在。
- 所有被同步锁synchronized持有的对象(synchronize持有的对象要是被销毁,同步就失效了)
可达性分析算法的注意事项:- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。(这点也是导致GC进行时必须STW的一个重要原因。)
三:对象的finalization机制
首先第一个问题:final、finally、finalize这三者有什么区别?
答:他们的区别就是这三者就是三个不同的东西,叙述各自的作用就行了。
finalize( ):对象销毁前的回调函数。
- Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
- finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
注意事项:
即使重写了这个方法,永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用。
1、在finalize()时可能会导致对象复活。
2、finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
3、因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
四:清除阶段:
目前在JVM中比较常见的三种垃圾收集算法是
1、标记清除算法(Mark-Sweep)
2、标记复制算法(Copying)
3、标记压缩算法(Mark-Compact)
标记-清除收集器(Mark-Sweep):
标记阶段是把所有活动对象(可达对象,reachable)都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
要把用户线程停下来,因为用户线程运行就又会产生垃圾,要保持一致性,就将用户线程先停下来。
缺点:
- 1、标记清除算法的效率不算高 (需要进行遍历)
- 2、在进行GC的时候,需要停止整个应用程序,用户体验较差
- 3、这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。
ps:所以现在新的垃圾收集器没有使用这个算法的了,因为产生碎片
标记-压缩收集器(Mark-Sweep-Compact):
这个算法是对标记-清除算法的改进
1、第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
2、第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间
优点:
- 1、消除了标记-清除算法当中,内存区域分散的缺点,有碎片。
- 2、消除了复制算法当中,内存减半的高额代价。
缺点
- 1、从效率上来说,标记-整理算法要低于其他算法,因为有碎片的整理过程
- 2、移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 3、移动过程中,需要全程暂停用户应用程序,时间要长一些。即:STW
复制收集器
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
ps:没有标记的过程,把可达的对象,直接复制到内存大小一样的另外一个区域中,而且是连续存放, 复制完成后,A区里面的对象就没有用了,下一次从B区复制到A区,这样交换使用。
优点:
- 1.没有标记和清除过程,实现简单,运行高效
- 2.复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
- 需要两倍的内存空间
分代收集器
1.这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。JVM生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。
2.一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
分代收集算法的分代依据:
目前几乎所有的GC都采用分代收集算法执行垃圾回收的
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
1、年轻代(Young Gen)
1、年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
2、这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。
ps:复制算法内存利用率不高的问题,通过两个survivor的设计得到缓解。
2、老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
ps:这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-清除-整理的混合实现。
设计OOM代码
import java.util.ArrayList;
import java.util.Random;
/**
* -Xms600m -Xmx600m
*
*/
public class Sub {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while (true) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture {
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
}