> [Java -- 深入浅出GC自动回收机制](https://www.cnblogs.com/wjtaigwh/p/6635484.html)
> [Java系列笔记(3) - Java 内存区域和GC机制](https://www.cnblogs.com/zhguang/p/3257367.html)
> 整理自 腾讯 QQ音乐一面总结ada
GC:Garbage Collection,垃圾回收器,用来释放没有用的对象所占用的内存空间
Java不像C++那样有delete或者free方法来释放对象的内存,而是通过GC自动地定时地回收没有被使用的对象的空间,这样降低了产生内存溢出和内存泄漏的可能性。但是一旦出现了内存溢出和内存泄漏,却又不了解虚拟机是怎么分配内存的,那么定位错误和排查错误将会是一个很困难的事情。
Java内存分配和回收的机制概括的说,就是:**分代分配,分代回收。**
Java对象根据存活的事件被划分为:**年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。**
### 年轻代(Young Generation)
年轻代的内存区域又可以分为3个区域:Eden区和两个存活区(Survivor 0 、Survivor 1)。
年轻代中的GC活动可以描述为以下几点:
1. 绝大多数刚创建的对象会被分配在Eden区(大对象可以直接被创建在年老代),其中的大多数对象很快就会消亡(IBM的研究表明,98%的对象都是很快消亡的)。Eden区是连续的内存空间,因此在其上分配内存极快;
2. 程序开始运行之后,最初一次Eden区满时,执行Minor GC(Yong GC),将消亡的对象清理掉,并将剩余的对象复制到一个空的存活区,此处假设为Survivor0(此时,Survivor1是空白的,两个Survivor总是至少有一个是空白的);
3. 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;
4. 将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;
5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
从上面的过程可以看出,Eden区是连续的空间,而两个Survivor区至少有一个为空。经过一次GC,其中一个Survivor肯定会持有经过GC之后存活的对象。那么其他两个区的内容就不再需要了,可以直接清空。到下一次GC时,两个Survivor区角色互换。这种分配内存和清理内存的效率都极高,这种GC方式就是著名的 **“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中)**
但是这不代表停止-复制方法很高效,他只在这种情况下高效。如果老年代也采用停止-复制,那么效率将会极低。(思考为什么?)
#### 拓展
在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。
### 老年代(Old Generation)
对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代。年老代的空间一般比年轻代大(存活时间长),比较稳定,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。
当年老代内存不足时,将执行Major GC,也叫 Full GC。但是因为老年代空间大、对象大小也偏大,所以如果采用停止-复制方法,那么花费的事件和空间与年轻代相比那将是效率极低的。这里介绍几种老年代GC算法。
1. Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是**标记出所有存活的对象**,清除阶段就是回收没有标记的对象所占用的空间。
![image](https://images0.cnblogs.com/i/288799/201406/181024382398115.jpg)
优点:实现容易
缺点:在多次回收之后,内存空间不连续,容易产生内存碎片,内存利用率降低。
2. Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
![image](https://images0.cnblogs.com/i/288799/201406/181041528488728.jpg)
这个有点像年轻代的停止-复制算法。但是这样也会带来比较大的缺陷
优点:不会产生碎片
缺点:空间利用率低,相比标记-清除算法回收时间长(老年代对象多、空间大)
3. Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,**而是将存活对象都向一端移动,然后清理掉端边界以外的内存。**
![image](https://images0.cnblogs.com/i/288799/201406/181100129575916.jpg)
优点:空间利用率高,不会产生碎片,回收时间较短
缺点:实现较为复杂
这种算法是一般JVM中老年代使用的算法
### 永久代(方法区)
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
+ 类的所有实例都已经被回收
+ 加载类的ClassLoader已经被回收
+ 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。
### 扩展:可达性分析算法
> 参考资料: [可达性算法、Java引用 详解](https://www.jianshu.com/p/8f5fa8288d9b)
在Java中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
![image](https://upload-images.jianshu.io/upload_images/6287954-6d92104b3089fc72.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp)
在java中,可以作为GC root的对象包括以下几种:
+ 虚拟机栈(栈帧中的本地变量表)中引用的对象
+ 方法区中类静态属性引用的对象
+ 方法区中常量引用的对象
+ 本地方法栈中JNI(即一般说的Native方法)引用的对象
### 扩展:Java中的引用
从可达性算法中可以看出,判断对象是否可达时,与“引用”有关。那么什么情况下可以说一个对象被引用,引用到底代表什么?
在JDK1.2之后,Java对引用的概念进行了扩充,可以将引用分为以下四类:
+ 强引用(Strong Reference)
在代码中的普遍存在的,类似于`Object obj = new Object();`这样的引用。
只要强引用存在,GC将永远不会收集被引用对象。哪怕是OOM。
+ 软引用(Soft Reference)
软引用是用来描述一些有用但并不是必需的对象,在Java中用`java.lang.ref.SoftReference`类来表示。
对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题
```java
SoftReference<String> sr = new SoftReference<String>(new String("hello")); // 创建一个软引用
```
+ 弱引用(Weak Reference)
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
在java中,用`java.lang.ref.WeakReference`类来表示。
```java
WeakReference<String> sr = new WeakReference<String>(new String("hello")); // 创建一个弱引用
```
+ 虚引用(Phantom Reference)
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用`java.lang.ref.PhantomReference`类表示。
如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
### 扩展:GC种类
上面的是一些常见的垃圾收集算法,垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面有几种创建的垃圾收集器,用户可以根据自己的需求组合出新年代和老年代使用的收集器。下面是常见的划分办法
新生代GC :串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)
串行GC:在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
并行回收GC:在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
并行GC:与老年代的并发GC配合使用。
老年代GC:串行GC(Serial MSC)、并行GC(Parallel MSC)和并发GC(CMS)。
串行GC(Serial MSC):client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。
并行GC(Parallel MSC):吞吐量大,但是GC的时候响应很慢:server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。
并发GC(CMS):响应比并行gc快很多,但是牺牲了一定的吞吐量。
<br />
<br />
<br />
<br />
<div align="right">
Chen Sicong
搬运时间:2019年8月2日 22:34:19
</div>
【学习】内存回收器 GC