目录:

  1. 什么是垃圾
  2. 怎么判定垃圾
  3. 什么时候回收垃圾
  4. 怎么回收
  5. 垃圾回收器介绍

1.什么是垃圾

在JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程生而生,随线程灭而灭(不需要管理);

栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理(不需要管理);

常说的垃圾回收主要集中在堆和方法区,这部分内存是随着程序运行动态分配的(回收对象,常量,类)。

 

2.怎么判定垃圾

2.1对象:

产生位置:堆

Java 的自动内存管理主要是针对对象内存的回收对象内存的分配
Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。关于Java 自动内存管理真的是‘’城外的人想进来,城里的人想出去‘’。

2.1.1引用计数法(非主流算法)

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;

当引用失效,计数器就减1;

任何时候计数器为0的对象就是不可能再被使用的。

优点:实现简单,效率高

缺点:很难解决对象之间相互循环引用的问题

2.1.2可达性分析法(主流算法)

“GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

 

补充:谈谈引用

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

 

补充知识:引用队列可以很容易地实现跟踪不需要的引用。当你在构造WeakReference时传入一个ReferenceQueue对象,当该引用指向的对象被标记为垃圾的时候,这个引用对象会自动地加入到引用队列里面。接下来,你就可以在固定的周期,处理传入的引用队列,比如做一些清理工作来处理这些没有用的引用对象。

 

1.强引用(new)
当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

 

实现强引用代码:

Widget w = new Widget();//w是新建的Widget的强引用

 

 

2.软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

 

实现软引用代码:

Widget w = new Widget();//w是新建的Widget的强引用
SoftReference<Widget> widgetWeakReference = new SoftReference<Widget>(w);//将一个软引用指向对象,此时Widget对象有两个引用
w = null;//去除对象的强引用
System.gc();//gc只有在内存不足是才会回收软引用对象

 

 

3.弱引用(WeakReference)
在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

实现弱引用代码:

Widget w = new Widget();//w是新建的Widget的强引用
WeakReference<Widget> widgetWeakReference = new WeakReference<Widget>(w);//将一个软引用指向对象,此时Widget对象有两个引用
w = null;//去除对象的强引用
System.gc();//gc对弱引用对象进行回收

 

 

4.虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

 

实现虚引用代码:

Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, queue);
obj=null;
System.gc();

 

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

 

学习到这里我其实是有个疑问的,当我们强引用了一个对象例如Student student=new student()的时候,难道JVM就真的不回收了吗。其实不是的,我们在栈帧中局部变量表中为对象引用分配了空间,当栈帧出栈时,引用内存被销毁。堆中的对象就会被判定层没有引用的垃圾进而回收。
可以看出引用的回收是指在方法进行中的垃圾回收,是‘动态’的过程;而对内存的垃圾回收是在方法执行完成后进行的回收,是个‘静态’的过程

 

2.2常量

产生位置:运行时常量池(JDK1.7及之后版本的 JVM 已经将其从方法区中移动到 Java 堆(Heap)中)

假如在常量池中存在字符串 "abc",如果当前没有任何String对象引用(栈帧中的局部变量表,出栈后清空栈帧)该字符串常量的话,就说明常量 "abc" 就是废弃常量。

 

2.3类

产生位置:方法区

主要回收的是无用的类。

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

同时满足上述三个条件即可判定为垃圾,但是并一定会像对象一样必定会被回收

 

3.什么时候回收垃圾(对象)

不可达的对象并非“非死不可”
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;

可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。
当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

 

 

4.怎么回收垃圾(对象)

垃圾回收算法有四种,分别是标记清理,复制回收,标记整理,分代回收

 

4.1标记-清除算法

标记:标记出所有需要回收的对象(应用计数法,可达性分析均可)

清除:标记完成后统一回收所有被标记的对象

 

优点:算法简单,效率高
缺点:标记清除后会产生大量不连续的碎片,为后续对象分配内存提出更高的需求

4.2复制算法

将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

 

优点:空间规则,分配对象内存容易。
缺点:只使用了内存的一半,浪费内存空间

 

 JVM对于新生代的回收的复制算法进行优化。

新生的对象(占用内存大的对象会直接进入老年代)会进入新生代eden区域,利用大多对象都朝生夕死的特点。

垃圾回收步骤

1.将eden区域的在使用的对象复制到from survivor

2.垃圾清理的时候将from survivor的在使用的对象实例复制到to survivor,同时把eden区在使用的对象复制到to survivor,清空from survivor和eden区

3.垃圾回收将to survivor的在使用对象有复制到form survivor,同时把eden区在使用的对象复制到form survivor,,清空to survivor和eden区

然后重复2,3步骤
优点:

节约空间,将空闲区域由50%减少到了10%

由于加大了内存空间,垃圾回收频率也会下降

对象的‘年龄也方便统计(第一次进入from survivor的对象年龄为1,复制一次年龄加1,对象年龄到达15(默认大小,可以通过参数 -XX:MaxTenuringThreshold 来设置进入老年代年龄)即存入老年代)

 

没有最好的算法,只有最适合业务场景的算法!!!

4.3标记-整理算法

根据老年代的特点(存活率较高)特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

4.4分代收集算法

新生代中,每次收集都会有大量对象死去,所以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。

老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

 

5.垃圾收集器

 

 

 

新生代老年代垃圾回收器搭配图

 

在开始前补充两个概念

并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。(实验室一起打扫卫生,没有人在学习)
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。(有人在打扫卫生,有人在学习)

 

5.1Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。

它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

 

优点:简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。

 

垃圾回收算法:

Serial:新生代采用复制算法

Serial Old:老年代采用标记-整理算法。

 

适用场景:单CPU(或CPU较少)、小型客户端应用。

5.2ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。

 

 

垃圾回收算法:

ParNew:新生代采用复制算法

ParNew:老年代采用标记-整理算法。

 

适用场景:多CPU(1,2核的情况下不如Serial),应优先(web服务器等)

 

注意:只有Serial(年轻代)收集器外和ParNew(年轻代)能与CMS(老年代)收集器配合工作。

 

5.3 Parallel Scavenge收集器

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU,占用cpu资源少)。

CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。

所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

 

 

垃圾回收算法:

Parallel Scavenge:新生代采用复制算法

Parallel Old:“标记-整理”算法

 

适用场景:在注重吞吐量(高效率的利用CPU)以及CPU资源的场合

 

5.4 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。

是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

 

垃圾回收算法(只针对老年代):标记-清除

初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;( "Stop The World" )
并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。( 并发执行 )
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短( "Stop The World" )
并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫。( 并发执行 )

 

 

优点:并发收集、低停顿

缺点:对CPU资源敏感,无法处理浮动垃圾,“标记-清除”算法会导致收集结束时会有大量空间碎片产生

 

适用场景:响应优先(web服务器等)

5.5 G1收集器

是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
垃圾回收算法(同时针对年轻代+老年代)
初始标记
并发标记
最终标记
筛选回收
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

 

关于垃圾收集器的补充:

查看方法

java -XX:+PrintCommandLineFlags -version

 

修改垃圾收集器

java -XX:+UseParallelGc

 

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)(满足cpu的高吞吐量)+Serial Old(老年代) 

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)(满足cpu的高吞吐量)+Serial Old(老年代)

jdk1.9 默认垃圾收集器G1

关于垃圾收集过程补充:

Minor GC:新生代GC,指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。

Major GC 是清理老年代。

Full GC 是清理整个堆空间—包括年轻代和老年代。

System.gc():这个方法只是提醒虚拟机,程序员希望你在这回收一下对象,但回不回收还是虚拟机来决定,也就是说程序员对回不回收没有绝对的控制权(苦瓜脸)

 

 

引用:

https://snailclimb.top/JavaGuide/#/./java/%E6%90%9E%E5%AE%9AJVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%B0%B1%E6%98%AF%E8%BF%99%E4%B9%88%E7%AE%80%E5%8D%95

https://www.cnblogs.com/grey-wolf/p/9217497.html

https://blog.csdn.net/lovexiaoqiqi/article/details/81737213

http://www.importnew.com/15820.html