Java中的垃圾回收

如何确定哪些东西是垃圾?

	·引用计数法:Java中引用和对象是有关联的,如果我们想要操作一个对象,一定要只有这个对象
	的引用,那么这个时候我们就可以根据对象被引用的次数来判断这个对象是否是可回收的对象。
		·缺点:引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,
		对象B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回
		收,所以主流的虚拟机都没有采用这种算法。
	
	·可达性分析法(引用链法):为了解决引用计数法的循环引用问题,Java 使用了可达性分析的
	方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路
	径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可
	回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

		·如果对象在可达性分析中没有不GC Root 的引用链,那么此时就会被第一次标记并且进行第
		一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已
		被虚拟机调用过,那么就认为是没必要的。如果该对象有必要执行 finalize()方法,那么这个对
		象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程
		是高优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者
		发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 
		F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。所
		以我们尽量不要使用finalize,不然可能会导致OOM,而且finalize中的代码由另外一个守护线
		程来执行,可能主程序执行结束了这个守护线程还没有执行结束,那也不会执行的。那么就会
		出现问题了。所以这个finalize再Java9开始就被标记为已过时的了,我们尽量别用。

		·哪些对象可以被当作GC roots?
			·虚拟机栈中引用的对象
			·方法区中类的静态属性引用的常量
			·方法区常量池引用的对象
			·本地方法栈JNI中引用的常量

确定了垃圾之后,如何进行垃圾清理?

·标记清除法:最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收
的对象,清除阶段回收被标记的对象所占用的空间。
		·缺点:内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。


·复制算法:为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用
其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一
块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪
费一半的内存。

	·虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一
	半。且存活对象增多的话,Copying 算法的效率会大大降低。


·标记整理算法:是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决
了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移劢到一端,然后清
除掉端边界以外的对象,这样就不会产生内存碎片了。
	
·分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存		
划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young 
Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾
回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。在新生代中,由亍对象
生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较
高,没有额外的空间迚行分配担保,所以可以使用标记-整理 戒者 标记-清除。

java 内存分配与回收策率以及 Minor GC 和 Major GC

  1. 对象优先在堆的 Eden 区分配。
  2. 大对象直接迚入老年代.
  3. 长期存活的对象将直接放入老年代
    当 Eden 区没有足够的空间迚行分配时,虚拟机会执行一次 Minor GC,Minor Gc 通 常发生在新
    生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高, 回收速度比较快;
    Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候还会触发 Minor GC,但是
    通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。

垃圾收集回收器的种类

1:Serial:最基本的垃圾收集器,采用复制算法,是jdk1.3之前唯一垃圾收集器,serial是一个单线程的垃圾收集器,并且在进行垃圾收集的同时必须暂停其他线程,知道垃圾回收结束。回收效率高,没有线程交互的开销,是Java虚拟机在Client模式下默认的新生代垃圾收集器
2:ParNew:实际上是Serial垃圾收集器的多线程版本,也是采用了复制算法,除了利用多线程进行垃圾回收之外,其他和Serial垃圾收集器一样。ParNew在垃圾收集时候也要暂停其他所有工作的线程。默认开启和CPU数目相同的线程数目,可以通过-XX:ParallelGCThreads参数来限定垃圾收集器的线程数,是Java虚拟机在Server模式下新生代默认的垃圾收集器
3:Parallel Scavenge:多线程复制算法,也是新生代的垃圾收集器,重点关注程序可以达到的一个可控制的吞吐量。包含自适应调节策略。
4:Serial Old:单线程标记整理算法,运行在Client模式下Java虚拟机默认的老年代垃圾收集器
5:Parallel Old:多线程标记整理算法
6:CMS:多线程标记清除算法,一种老年代垃圾收集器
7:G1:标记整理算法,不产生内存碎片。

CMS垃圾收集器
这是一个老年代的垃圾收集器,采用的是多线程标记清除算法,标记过程分为四个阶段,

  • 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
  • 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
  • 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
  • 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。

由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

优点:处理速度快
缺点:会产生内存碎片

CMS在并发标记的过程中,会产生对象丢失的问题,产生对象丢失的条件有两个
(参考文章,这个写的很清楚。其他文章)

  • 是在三色标记的过程中有灰色到白色的引用被取消
  • 被灰色取消引用的白色对象被黑色对象引用了

解决的方式有两种,分别对应的是破坏上面两个条件

  • 增量更新:当发生第一种情况时候把这个对象记录下来,并发标记完成后再把对象重新扫描一遍。这个也是CMS中使用的方式。
  • 原始快照:发生第二种情况时候也把这个对象记录下来,并发标记结束之后再重新扫描一遍。(这个是G1使用的方式)

G1垃圾收集器

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收
集器两个最突出的改进是:

  1. 基于标记-整理算法,不产生内存碎片。
  2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
    G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

ZGC(参考文章)
ZGC是JDK111中推出的一款低停顿的垃圾收集器,它的设计目标如下:

  • 停顿是时间不超过10ms(这个在jdk15的时候已经实现了)
  • 停顿的时间不会随着堆的增大而增长
  • 支持8M-4T级别的堆(未来支持16TB,这个在jdk15的时候已经实现了)

染色指针可以说就是指的是对象内存地址的44-47位的颜色标志,这样就可以快速地判断这个对象是否是标记或者转移阶段,或者是否是只有finalize方法才能调用。

垃圾收集的安全点
GC并不是在任何时候都会发生的,因为我们知道垃圾回收的时候判断是否可达,不可达的对象才会被定义成垃圾,这样在标记的时候就要stop the world,防止在标记过程中产生的新的垃圾对象。HotSpot虚拟机维护了一个映射列表oopmap用来记录哪些位置存放了对象的引用,以便于快速判断是否是垃圾,但是由于对于每一个操作都维护一个oopmap未免有点过于占用空间,得不偿失。因此只有在特点的地方生成oopmap,这样的位置就是安全点。

不过安全点的设置也不是越多越好,多了反而导致垃圾收集效率很低,太少导致一些线程等待其他线程到达安全点的时间很久,导致性能变差。一般安全点会设置在下面几个地方:

  • 方法临返回前
  • 刚调用方法的时候
  • 循环体结束前
  • 发生异常的地方

强烈推荐,推荐文章
这些位置有助于使线程不会长时间运行导致无法到达safePoint,避免其他线程都停顿等待本线程。

线程中断的方式

  • 抢占式中断:直接中断所有的线程,如果有的线程没有到达安全点,那就让它继续运行到安全点。
  • 主动式中断:不对线程直接进行操作,而是设置一个中断标志,线程运行的时候自己检测这个标志,如果需要进行垃圾回收,则运行到最近的安全点的时候自己主动暂停。

经典的停顿导致主线程长时间等待代码:

public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=()->{
        //i定义成int类型会被当成可数循环导致jvm不会在这里设置安全点safepoint
            for (int i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            System.out.println(Thread.currentThread().getName()+"执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        //由于sleep方法是底层用到了native,回来的时候就会进入安全点等待其他线程也进入安全点进行
        //gc,但是由于int的循环没有添加安全点导致了主线程只能等待其他线程的循环体执行完才能进入
        //安全点,所以下面的打印会很慢,如果改成long类型的i就可以解决问题,或者添加一个
        //sleep(0)的代码也可以有native方法执行完之后进入安全点达到解决长时间停顿的问题。
        Thread.sleep(1000);
        System.out.println("num = " + num);
    }

安全区域

思考一下前面的安全点会不会有一些问题?
比如线程阻塞获取不到时间片,或者sleep了,无法到达安全点怎么办?总不能一直不进行垃圾回收吧。

安全区域的出现就是为了解决上面的问题,所谓的安全区域就是在某一部分代码执行的时候对象的引用状态不会发生变化,这样GCROOT就不会变化,那么在这一块代码执行的时候是可以直接进行垃圾回收的而不必非要等到线程到达安全点。

那么这里又会出现一个问题,假如我在垃圾回收的时候,线程从安全点跑出去了导致对象的引用状态发生了变化那不就完蛋了???所以这里在线程执行完安全区的代码的时候也不会直接跑出安全区,而是先看这个垃圾回收是否进行完,回收结束了才会让你跑出去。不然就等着吧你。

java8默认是垃圾回收算法是什么?

使用命令查看

java -XX:+PrintCommandLineFlags -version

在这里插入图片描述
JDK8中默认的选择是”-XX:+UseParallelGC",是 Parallel Scavenge + Parallel Old组合。

你可能感兴趣的:(JVM内存模型,java)