1. 垃圾回收机制
概念: java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,由于有个GC,java中的对象不再有作用域的概念,只有对象的引用有作用域,垃圾回收可以有效的防止内存泄漏,有效的使用空闲的内存。
- 所谓程序的运行,其实就是变量不断地在内存中的申请领地,进行活动,最后交还领地给JVM内存的一个过程,假设程序最后不交还领地,则会导致内存垃圾越来越多。
- Java对内存垃圾的处理方式是全自动的,JDK提供了一个垃圾回收员(Garbage Collection),它会不定时跑到你的程序中去回收内存垃圾,消除了程序员手动清理垃圾的烦恼。
1.1 JVM栈的GC
概念: 内存的划分中,程序计数器、JVM栈和本地方法栈都是随线程生和死的,栈中的栈帧随着方法的调用有序的进栈和出栈,每个栈帧上分配的内存大小在类结构确定时就已知了,所以这些区域内存的分配和回收都是具有确定性的,很容易回收,当方法调用结束或者线程结束,占用的内存就可以被回收。
1.2 方法区的GC
概念:
- 虽然JVM规范中没有要求对方法区进行垃圾回收,但是一些虚拟机,如HotSpot虚拟机仍然实现了方法区的垃圾回收,方法区的垃圾主要是废弃的常量和无用的类。我们知道方法区中有一些常量池,如字符串常量池,如果系统中不存在引用常量池中常量的引用,那么在内存紧张的时候,这些常量就应该被废弃回收,常量池中的其他类(接口)、方法、字段、符号引用也是如此。
- 判断常量是否应该被废弃的方法比较简单,而判断一个类是无用的类,则需要满足下面三个条件:
- 该类的所有实例都已经被回收了,即Java堆内存中没有该类的对象实例。
- 加载该类的类加载器ClassLoader已经被回收了。
- 该类对应的java.lang.Class对象在任何地方都没有被引用,也即无法通过反射访问该类。
- 但满足了上述这些条件,也不是说这个类就要被非回收不可,我们是可以通过设置虚拟机参数进行控制的。
1.3 堆的GC
概念: 在Java堆中,每个类需要的内存都可能不一样,一个方法中多个分支需要的内存也可能不一样,这些都只有在运行期才能知道创建哪些对象,所以这部分内存的分配和回收都是动态的,垃圾回收也主要是对这部分的内存进行回收。
我们可以使用代码来建议GC回收System.gc();
2. 垃圾判定算法
概念: 什么样的实例算是垃圾?判断一个实例对象的生死很难,就像有些人虽然活着,但其实早就死了,一些人虽然死了,但永远活在我们心中,文艺可以扯淡,但是编程必须严谨,所以我们必须要有判断实例对象生或者死的方法。
2.1 引用计数算法
概念:
- 当一个对象被创建时,为这个对象实例分配一个变量,该变量计数设置为1:
- 每当有一个地方引用它时,计数器加1。
- 每当一个对它的引用失效时,计数器减1。
- 当计数器的值为0时,就说明不存在对它的引用了,它就可以去死了(不是立刻死亡,而是等待GC啥时候心情好,就过来干死它)。
- 一个对象被回收时,这个对象所引用的其他任何对象的引用计数器也会减一。
- 优点:简单高效直接。
- 缺点:无法检测和解决实例和实例之间的循环引用的问题,如实例A和实例B都是同一个类的实例,A中引用B,B中引用A,则A和B都不能被回收。
源码: /javase-oop/
- src:
c.y.gc.ReferenceCountDemo
/**
* 建立循环引用,测试当前JDK版本是否使用了引用计数回收策略。
*
* 使用 `-verbose:gc -XX:+PrintGCDetails` 运行参数可以看到详细的GC情况,
* 可以发现PSYoungGen空间变小,说明程序依然执行了GC,也说明jdk1.8使用的不是引用计数回收策略。
*
* @author yap
*/
public class ReferenceCountDemo {
private ReferenceCountDemo field;
public static void main(String[] args) {
ReferenceCountDemo instanceA = new ReferenceCountDemo();
ReferenceCountDemo instanceB = new ReferenceCountDemo();
// 让两个实例循环引用,此时两个实例的引用计数均为1
instanceA.field = instanceB;
instanceB.field = instanceA;
// 断开两个实例的堆栈联系,此时堆中的两个对象应为垃圾,但因为互相引用,永远无法被GC回收
instanceA = null;
instanceB = null;
// 事实上内存在GC之后变小了,说明jdk8使用的不是引用计数的回收方式
System.gc();
}
}
2.2 可达性分析算法
概念: jdk1.8使用的是可达性分析算法,可达性分析算法也叫根搜索算法,是从离散数学中的图论引进而来的,算法把所有的引用关系看成是一张图,从根节点(GC-Root)开始向下搜索实例对象,搜索所走的路径称为是引用链,当一个对象从根节点开始找不到任何一条引用链时,就说明这个对象可被回收,可达性分析算法的本质就是判断某个实例是否有从GC-Root出发的,可达的引用链。
- GC-Root是一个特殊的对象,且绝对不能被其他对象引用(所以不会出现循环引用的问题)。
- 虚拟机栈(栈帧本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区常量引用的对象。
- 本地方法栈中(Native 方法)引用的对象。
finalize():在可达性分析算法中,即使是不可达的对象,也并非是要立即执行死刑,它们暂时处于死缓状态,然后被判断是否要执行这个实例的finalize():- 不执行:如果实例没有重写Object类中的
finalize(),或者已经执行过一次finalize()且复活了一次,那么均被判定不执行finalize(),直接被回收。 - 执行:判定需要执行
finalize()的实例会被加入到一个队列中,并且由JVM分配一个单独的线程来执行队列中这些实例中的finalize(),如果某个实例的finalize()方法中,该实例突然被引用链上其他的可达实例关联了,那么这个实例就可以被移出这个即将回收的队列,从而死里逃生。
- 不执行:如果实例没有重写Object类中的
可达性分析算法图
2.3 引用强软弱虚
概念: 引用也是一个很模糊的概念,为了更加清晰的描述Java中的对象引用,在JDK1.2后,Java将引用分为4种,并且除了强引用外都有与之对应的Java类,都继承自Reference类,引用强度自上而下:
- 强引用:new的时候就是强引用:
- 如
Object obj = new Object()
- 如
- 软引用:指向一些有用,但非必须的实例引用,当内存特别紧张的时候才会回收这些对象,若回收后还是没有足够的内存,则OOM。
- 对应
java.lang.ref.SoftReference,一般用于缓存设计。
- 对应
- 弱引用:指向一些有用,但非必须的实例引用,GC看到它,就会立刻将它干掉。
- 对应
java.lang.ref.WeakReference,一般用于容器设计,如ThreadLocal。
- 对应
- 虚引用:又称幽灵引用或幻影引用,GC看到它,就会立刻将它干掉。
- 一个实例是否被虚引用,完全影响它的生死,也无法用虚引用来获取这个实例,即无法使用get()。
- 为一个实例关联虚引用之后,在这个实例被GC回收的时候,虚引用会被存储到一个队列中,然后返回给用户一个死亡通知。
- 对应
java.lang.ref.PhantomReference,一般用于管理堆外内存,比如NIO的直接内存,你可以设计一个变量并为其添加虚引用,当虚引被回收的时候,你会收到一个通知,当收到这个通知的时候,你去利用别的手段回收对应的堆外内存。
源码: /javase-oop/
- src:
c.y.gc.StrongReferenceTest
/**
* @author yap
*/
public class StrongReferenceTest {
public static void main(String[] args) throws IOException {
Demo demo = new Demo();
demo = null;
System.gc();
// block the main thread...
System.out.println(System.in.read());
}
static class Demo {
@Override
protected void finalize() {
System.out.println("GC会调用finalize()...");
}
}
}
- src:
c.y.gc.SoftReferenceTest
/**
* -Xms20M -Xmx20M
*
* @author yap
*/
public class SoftReferenceTest {
public static void main(String[] args) throws InterruptedException {
System.gc();
TimeUnit.SECONDS.sleep(1L);
SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024 * 10]);
System.out.println(softReference.get() == null ? "be recycled" : "not recycled");
System.gc();
TimeUnit.SECONDS.sleep(1L);
System.out.println(softReference.get() == null ? "be recycled" : "not recycled");
byte[] bsB = new byte[1024 * 1024 * 11];
System.out.println(softReference.get() == null ? "be recycled" : "not recycled");
}
}
- src:
c.y.gc.WeekReferenceTest
/**
* @author yap
*/
public class WeekReferenceTest {
public static void main(String[] args) throws InterruptedException {
WeakReference<Object> weakReference = new WeakReference<>(new Object());
System.out.println(weakReference.get() == null ? "be recycled" : "not recycled");
System.gc();
TimeUnit.SECONDS.sleep(1L);
System.out.println(weakReference.get() == null ? "be recycled" : "not recycled");
}
}
3. 垃圾回收算法
3.1 Mark-Sweep
标记清除算法图
概念: Mark-Sweep算法,标记-清除算法,它是最基础的收集算法,后续的收集算法都是对它的改进。
- 描述:从头到尾遍历内存区域,绿色标记0,黄色标记1,再次遍历,将所有1清除。
- 优点:直接在原内存上进行操作,不需要占用额外的内存空间
- 缺点:
- 整理后内存不连续,产生内存碎片,可能会导致以后再需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发另一次GC动作。
- 效率慢,标记和清除这两个过程的效率都不高。
3.2 Copy
复制算法图
概念: copy算法,复制算法
- 描述:复制算法需要在内存中,再创建一块相同大小的区域,从头到尾遍历之前的区域,将所有已使用的内存移动到新区域中,然后删除之前的内存区域。
- 优点:整理后没有碎片,复制代价小,速度快。
- 缺点:
- 需要额外开辟相同大小的内存空间。
- 在对象存活率较高的时候,进行大量的复制操作的效率也是很低的,所以对于存活时间长的对象一般不使用这种收集算法。
3.3 Mark-Move
标记移动算法图
概念: Mark-Move算法,标记-移动算法。
- 描述:从头到尾遍历区域,绿色标记0,黄色标记1,再次遍历,遇到0向前移动,遇到1直接清除。
- 优点:整理后没有碎片,不需要双倍内存。
- 缺点:但因在标记-清除算法的基础上增加了"移动",所以效率比标记-清除要低一些。
3.4 分代收集算法
分代收集算法图
概念: 分代收集指的是根据实例年龄的不同而进行不同方式的回收,堆内存分为新生代和老年代,默认内存大小比例是1:2,这个比例值是可以通过 -XX:NewRatio 运行参数来动态设置的。
- 新生代
Young Generation:动态存储新new出来的对象,新生代又被划分为3个区域:伊甸园Eden和两个Survivor幸存区:Survivor from和Survivor to。Eden:实例一开始都在Eden区产生,当Eden满了,JVM执行一次minorGC,在整个新生代里面利用复制算法执行GC动作,80%-90%的实例会在这里直接死去,存活下来的幸存实例会被拷贝到某个Survivor区(假设为S1),且将幸存对象身上的计数器加1,Eden被清空的同时S2中存活下来的幸存实例也会被拷贝到S1,因为JVM规定必须保证至少有一个Survivor区是空的,另外,GC过程中年龄足够老的对象(计数器值过大)会直接进入老年代。Survivor:达到某个条件时,会将大于Survivor区的一半相对年龄大的实例都移入老年代。
- 老年代
Old Generation:存储年龄稍大的实例,主要由MajorGC负责GC工作。MajorGC采用标记清除或者标记移动的算法,回收速度比minorGC低10倍左右。- 达到某个条件时,JVM会执行一次
FullGC,整体大回收。
4. 垃圾收集实现
概念: 垃圾收集器是垃圾回收机制的具体实现,在JVM中,垃圾收集器可能存在一个或多个(单线程或多线程),它们在工作的时候,会短暂地暂停其他线程,这种情况被称为STW(Stop The Word),减少STW时间是优化垃圾回收机制的重要指标。
- 垃圾收集器常见组合:
SerialGC=Serial+Serial Old,JVM客户端默认使用它。ParNewGC=ParNew+Serial OldConcMarkSweepGC=ParNew+CMS+Serial OldParallelGC=PS Scavenge+Serial Old(PS MarkSweep),JVM服务端默认使用它。
4.1 垃圾收集器分类
概念: 垃圾收集器的种类很多:
Serial:- 一个单线程的新生代收集器,采取复制算法实现。
- 在单CPU环境下,因为没有线程切换的开销,效率最高,STW控制在在几十到几百毫秒内。
Serial Old:- Serial收集器的老年代版本,也是一个单线程收集器,采取标记-整理算法。
- 它有个别名叫
PS-MarkSweep。
ParNew:- Serial收集器的多线程版本,收集算法、STW、对象分配的规则、回收策略等都与Serial收集器完全一样。
- ParNew收集器的优势是更充分的利用CPU资源(多线程),缺点是是在单CPU下,效果不一定会比Serial好。
PS Scavenge收集器:- 一个多线程的新生代收集器,采取复制算法实现。
- 对比ParNew收集器,它可以更加精准的控制CPU的吞吐量和STW的时间。
PS Old:- PS-Scavenge收集器的老年代版本,采取标记-清除算法。
- PS-Scavenge + PS-Old的组合,对于多CPU环境,吞吐量要求高的环境是很适合的。
CMS:- CMS(Concurrent Mark Sweep),采取标记-清除算法,设计理念是尽可能地缩短STW时间。
- CMD适用于一些特别重视响应速度的项目,但缺点是会产生大量的空间碎片,(用时间换空间)。
G1:- 目前最高端的收集器,采取标记-整理算法,不会产生内存碎片,并且也可以精准地控制STW的时间。
- 对于新生代和老年代都是适用的,优先回收垃圾最多的区域。
4.2 垃圾收集器查看
概念: 不同版本的JDK选择的垃圾收集器也可能不同,JDK8使用的是 ParallelGC 包括新生代收集器 PS Scavenge 和老年代收集器 PS MarkSweep。
源码: /javase-oop/
- src:
c.y.gc.GarbageCollectorTest
/**
* @author yap
*/
public class GarbageCollectorTest {
@Test
public void myGarbageCollector() {
for (GarbageCollectorMXBean e : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println(e.getName());
}
}
}
也可以直接使用java -XX:+PrintCommandLineFlags -version命令查看垃圾回收器。