在面试中我之前被闻到过,什么是垃圾回收机制?能否讲讲垃圾回收机制?
当时说的有点语无伦次,没有很好的逻辑性,特此来总结。
垃圾回收机制主要做了两件事情:
1.跟踪并监控每一个java对象,当某个对象处于不可达状态时,回收该对象所占用的内存。
2.清理内存分配,回收过程中产生的内存碎片。
因为垃圾回收都是在内存中进行的,因此我们有必要了解一下jvm的内存结构。
程序计数器:是线程私有区,是内存中较小的区域,是当前线程执行字节码的行号指示器。
虚拟机栈:是线程私有的,该区域描述的是java方法执行的动态内存模型,每一个方法在执行的时候都会产生一个栈帧,用来存储局部变量等信息,操作数栈、动态链接、方法出口等信息。
本地方法栈:是为本地native服务的。
堆:线程共享的一块区域,用来存放对象实例,该区域是垃圾回收的主要区域,堆空间不足的时候会抛出OOM异常(内存溢出)。
方法区:线程共享的区域,存储JVM加载的类的信息、常量、静态变量以及编译后的代码等数据,当方法区空间不足时就会抛出OOM异常。方法区中还有一个运行时常量池,class文件中的常量池在类加载之后就放入运行时常量池。
除了java虚拟机规定的这几个区域之外,还存在一个堆外内存,即直接内存,直接内存可以减少IO时内存复制,实现零拷贝,而且没有GC,减少了垃圾回收的工作,加快了复制速度,同样该区域也难以控制,如果内存泄露很难排查。
1.引用计数法
为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数,当该对象的引用计数为0时,则代表没有地方在被使用了,就可以被认为是“垃圾对象”。每当一个地方引用了一个对象之后引用计数加1。
但是该方法有一个缺陷:无法检测循环引用的问题,当两个对象相互引用时,这两个对象的引用计数器都不是0,但这两个对象对我们来说已经没有用了,但是没有办法回收掉。因此Java中没有采用这种方法判断对象的“存活性”。
2.可达性分析
可达性行分析的基本思想就是通过一个“GC Root”对象为起点,这些节点开始向下搜索,节点走过的路径称为引用链,当一个对象到GC Root时没有任何引用链,则证明此对象不可用。
那么有哪写点可以作为GC Root呢?
1.虚拟机栈中的引用对象
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI引用的对象
1.标记清除算法
垃圾回收器先从根开始访问所有可达对象,将其标记为可达状态,然后再遍历一次整个内存区域,把没有标记的对象进行回收处理。
未回收之前内存:
垃圾回收之后:
这种算法需要两次循环遍历算法,第一次遍历将可达性对象标记出来,第二次在便利的时候将不可达对象进行回收,造成遍历的成本比较大,因此造成程序暂停的时间随堆空间大小线性增加。而且通过该算法回收回来的内存往往是不连续的,导致回收后的内存碎片化严重。
特点:①两次遍历成本比较高,随着堆空间的变大遍历时间也会增加
②垃圾回收之后内存碎片化
2.标记整理算法
首先进行一次可达性遍历,将可达与不可达对象区分出来;将所有存活的对象搬迁到一起,这个过程也成为内存压缩。
回收之后的内存:
这样就避免了之前标记清除算法中的内存碎片化的问题。
特点:①适合存活对象多,垃圾对象少的情况
②需要对可达对象与不可达对象的整理。
3.复制算法
将内存按照容量划分为两个相等大小的控件,从根开始访问每一个关联的可达对象,当一块用完了之后,将或者对象赋值到另一块上 ,然后再一次性回收整个空间。
回收之前:
通过复制算法回收垃圾之后:
特点:①并不会产生内存碎片
②内存使用率太低
在说垃圾分代回收之前我们要先讲一下方法区中的回收。
方法区又称永久代。永久代的垃圾回收主要有两种:废弃常量和无用的类。
废弃常量:没有任何地方对这个常量进行引用就表示是废弃常量。
回收无用的类:
①Java堆中不存在该类的任何实例,也就是该类的所有实例都被回收
②加载该类的ClassLoader已经被回收
③该类对应的Class对象在任何地方没有引用了,也不能通过反射获取到该类的方法。
满足以上三个条件就可以回收,但不是强制的。在《Java虚拟机规范》说到过不要求虚拟机对方法区进行回收。并且方法区进行垃圾回收的性价比比较低。
分代回收
java堆控件分为三部分:
新生代:刚刚创建的对象。
老年代:存活了一段时间的对象。
永久代:永久存在的对象。
现在的垃圾回收器用分带的方式来采用不同的回收设计。分代回收的基本思路是根据对象生存时间长短,将堆中的内存分为三个代:
年轻代
特点:存活对象少,垃圾多。
新生代对象中,由于每次GC都会有大量新对象死去,只有少量存活,因此采用复制算法,GC时只需要将少量存活的对象复制过去即可。
工作原理:
①首先Eden对外提供堆内存,当Eden快满的时候触发垃圾回收机制,将存活的对象放入到SurvivorA区,清空Eden区。
②Eden清空之后继续对外提供堆内存。
③当Eden再次填满之后,对Eden和SurvivorA同时进行垃圾回收,将存活的对象放入SurvivorB区中,同时清空Eden和SurvivorA区。
④当某个Survivor被填满时,将多余的对象放入到Old区。
⑤当Old区被填满时,进行下一轮的垃圾回收。
老年代
特点:存活对象多,垃圾少。
Old代的大部分对象都是久经考验,没有那么容易死,并且随着时间堆积Old代的对象会越来越多,因此Old代的空间要比年轻代空间更大。
根绝老年代的特点我们可以知道Old代很少有对象会死掉所以Old代进行垃圾回收的频率无需太高;由于老年代不容易死掉,所以在垃圾回收需要更长时间来完成。
针对以上特点,Old代通常采用标记整理算法,这种算法避免复制大量对象,并且Old不会很快死亡,回收过程不会产生大量的内存碎片。
永久代
永久代主要用于装载Class、方法等信息,默认64M,垃圾回收器通常不会回收永久代中的对象。对于一些需要加载很多类的服务器程序,往往需要加大永久代的内存,否则可能会因为内存不足导致程序终止。
前面我们都介绍了垃圾回收器的所有算法,垃圾回收器就是实现上面讲到算法的一个东西。
在了解垃圾回收器之前我们要先知道几个名词:
吞吐量:CPU用于运行用户代码和CPU总消耗时间的占比。
停顿时间:之垃圾回收器正在运行时,程序停顿的时间。
MinorGC:新生代GC
MajorGC:老年代GC
并发与并行:①串行:垃圾回收线程在进行垃圾回收时,此时用户线程处于等待状态。
②并发:用户线程与垃圾回收线程交替执行
③并行:用户线程和多条垃圾回收线程在不同的CPU上执行。
垃圾回收器是基于HotSpot虚拟机:
Serial收集器(复制算法):新生代单线程收集器,进行标记和清理时不存在线程切换,在单核CPU中执行效率比较高。
Serial Old收集器(标记整理算法):老年代单线程收集器。
ParNew收集器(复制算法):新生代并行收集器,是在Serial收集器的基础上演化而来,在多核CPU情况下比Serial更好。
Parallel Scavenge收集器(复制算法):新生代并行收集器,追求高吞吐量,高效利用CPU。
Parallel Old收集器(标记整理算法):老年代并行收集器,优先考虑吞吐量。
CMS收集器(标记清除算法):老年代并行收集器,以获取最短停顿时间为目标,具有高并发、短停顿的特点,追求最短GC停顿时间。
G1收集器(标记整理算法):Java堆并行收集器。G1收集器是JDK1.7出现的垃圾收集器,由于采用的算法不会产生内存碎片,并且面向的范围是整个堆(包括老年代、新生代)。
整理一个表格会更加直观:
收集器 | 串行/并行/并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单核CPU情况下的Client模式 |
Serial Old | 串行 | 老年代 | 标记整理 | 响应速度优先 | 单核CPU情况下的Client模式、CMS后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多核CPU情况下的Server模式下与CMS配合 |
Parallel Scaveneg | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端山东个java程序 |
G1 | 并发 | 两者都包含 | 标记整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
到这了Java垃圾回收器的相关内容基本上总结完了,用来面试的话也没有问题了,希望对大家有帮助。