它使得 Java 程序员在编写程序的时候不再需要考虑内存管理。垃圾回收器通常是作为一个单独的低级别的线程运行, 不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收, 程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。 程序员可以手动执行System.gc(), 通知 GC 运行, 但是 Java 语言规范并不保证 GC 一定会执行。
垃圾回收机制可以用 3 个词来概括: where, when 和 how?
Where: 运行时的内存分布情况。 见上一博客。
When: 对象何时需要被回收的? 也就是何时回收无效对象, 已死对象的?
这里涉及到两种做法: 引用计数法和可达性分析算法。 这里还涉及到 java
中 4 种引用方式: 强引用, 软引用, 弱引用和虚引用, 其引用强度越来越来低,意味着引用越弱的对象越容易被垃圾回收的。
how: 对象如何被回收的? 4 种垃圾回收算法。
程序计数器、 虚拟机栈、 本地方法栈3个区域随线程而生, 随线程而灭。这几个区域的内存分配和回收都具备确定性, 在这几个区域内就不需要过多考虑回收的问题, 因为方法结束或者线程结束时, 内存自然就跟随着回收了。Java堆不一样,一个接口中的多个实现类需要的内存可能不一样, 一个方法中的多个分支需要的内存也可能不一样, 我们只有在程序处于运行期间时才能知道会创建哪些对象, 这部分内存的分配和回收都是动态的, 垃圾收集器所关注的是这部分内存。方法区也就是HotSpot虚拟机中的永生代也有一定的垃圾收集,主要回收两部分内容,废弃常量和无用的类。
垃圾收集器对堆进行回收前,第一件事就是要确定这些对象之中哪些还存活着,哪些已经死去。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1,引用失效时,计数器就减1,任何时刻计数器为0的对象就是不可能再被使用的。
这种方法实现简单,判断效率高,但是主流虚拟机没有采用这种方法,因为它很难解决对象之间相互循环引用的问题。如:objA.instance = objB. objB.instance = objA.
这个算法的基本思路就是通过一系列的称为“GC Roots” 的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链 , 当一个对象到GC Roots**没有任何引用链相连**(用图论的话来说, 就是从GC Roots到这个对象不可达) 时, 则证明此对象是不可用的。
在Java中,可以作为GC roots 的对象包括以下几种。
在JDK 1.2以前, Java中的引用的定义很传统: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址, 就称这块内存代表着一个引用。 这种定义很纯粹, 但是太过狭隘, 一个对象在这种定义下只有被引用或者没有被引用两种状态, 对于如何描述一些“食之无味, 弃之可惜” 的对象就显得无能为力。
在JDK 1.2之后, Java对引用的概念进行了扩充, 将引用分为强引用 、 软引用 、 弱引用 、 虚引用 4种, 这4种引用强度依次逐渐减弱。
⑴强引用(StrongReference)
强引用是使用最普遍的引用。 如果一个对象具有强引用, 那垃圾回收器绝不会回收。 当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止, 也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps: 强引用其实也就是我们平时 A a = new A()这个意思。
⑵软引用(SoftReference)
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj=null;
sf.get();//内存足够时候能获取对象值,内存不够则为null
如果一个对象只具有软引用, 则内存空间足够, 垃圾回收器就不会回收它;如果内存空间不足了, 就会回收这些对象的内存。 只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可用来实现内存敏感的高速缓存(下文给出示例) 。
软引用可以和一个引用队列(ReferenceQueue) 联合使用, 如果软引用所引用的对象被垃圾回收器回收, Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
示例: 实现学生信息查询操作时有两套数据操作的方案。
一、 将得到的信息存放在内存中, 后续查询则直接读取内存信息(优点:
读取速度快; 缺点: 内存空间一直被占, 若资源访问量不高, 则浪费内存空间)。
二、 每次查询均从数据库读取, 然后填充到 TO 返回。 (优点: 内存空间
将被 GC 回收, 不会一直被占用; 缺点: 在 GC 发生之前已有的 TO 依然存在,但还是执行了一次数据库查询, 浪费 IO) 。可以通过软引用来解决。
⑶弱引用(WeakReference)
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj=null;
wf.get();有时候返回null
wf.isEnqueued()//返回是否被垃圾其标记为即将回收的垃圾
弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。
在垃圾回收器线程扫描它所管辖的内存区域的过程中, 一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否, 都会回收它的内存。 不过, 由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。第二次垃圾回收时候肯定将其回收。
弱引用可以和一个引用队列(ReferenceQueue) 联合使用, 如果弱引用所引用的对象被垃圾回收, Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
⑷虚引用(PhantomReference)
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();永远返回null 幽灵引用
pf.isEnqueued()//返回是否被垃圾其标记为即将回收的垃圾
“虚引用”顾名思义, 就是形同虚设, 与其他几种引用都不同, 虚引用并不会决定对象的生命周期。 如果一个对象仅持有虚引用, 那么它就和没有任何引用一样, 在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用与软引用和弱
引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时, 如果发现它还有虚引用, 就会在回收对象的内存之前, 把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(object,queue);
即使在可达性分析算法中不可达的对象, 也并非是“非死不可” 的, 要真正宣告一个对象死亡, 至少要经历两次标记过程: 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法【执行finalize()方法对象可能重新进入引用链】。 当对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过, 虚拟机将这两种情况都视为“没有必要执行” 。就直接进行GC.
最基础的收集算法就是标记清除算法,分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。主要不足有两个,一是效率,标记和清除两个过程的效率都不高,另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,会导致以后有较大对象时没有内存分配而又一次触发GC.
此方法将内存划分为相等的两块,每次使用其中的一块,当这一块用完了,就将还存活的对象复制到另一块上,然后把已使用过的内存空间一次性清理掉,分配对象时候在刚刚复制了数据的对象块中进行。这样使得每次都是对整个半区进行内存回收,分配时就不用考虑内存碎片的问题,但是代价是内存缩小为了原来的一半。
现在的商用虚拟机都采用这种收集算法来回收新生代。但是进行了一定的改进,不是按照1比1进行划分。将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用一块eden和其中一块Survivor。当回收时,将Eden 和Survivor中还存活的对象一次性复制到另外一块Survivor 中。然后清理掉Eden和Survivor .HotSpot 虚拟机设置的默认比例是8:1:1.只有10%的内存会被浪费【之所以分为三份,因为需要保证不存在内存碎片,因为存活后的对象也需要再次进行垃圾回收】,当Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保。
复制收集算法在对象存活率较高时就要进行较多的复制,所以一般老年代不选用这种算法。根据老年代特点,有另外一种标记-整理算法,标记过程和前面标记清除过程一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
从GC Roots节点中找引用链这个操作,会消耗很多时间,因此必须这个分析工作能确保一致性的快照中进行,这就需要在GC进行时候需要停止所有Java执行线程。hotspot 在实现中,使用一组称为Oopmap 数据结构记录哪些地方存放什么对象引用可以快速实现GC Roots 枚举。
HotSpot 通过为特定指令位置(safepoint)生成OooMap,通过抢占式中断和主动式中断停止用户线程,实现GC.
垃圾收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。下面讨论的1收集器基于JDK1.7后的HotSpot虚拟机。
Serial 收集器:最基本,发展历史最悠久的单线程收集器。jdk1.3
之前虚拟机新生代的唯一选择。它在进行垃圾收集时,必须暂停所有其他所有的工作线程。“stop the world
“直到它收集结束。
ParNew 收集器:Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余和Serial几乎完全一样。是运行在Server模式下的虚拟机中首选的新生代收集器。同时,也只有Serial和ParNew 能和CMS收集器配合工作。
Serial Old 收集器:Serial收集器的老年代版本,是一个单线程使用标记-整理算法的收集器。主要给Client模式下的虚拟机使用。
Parallel Old :parallel Scaenge 收集器的老年代版本,使用多线程和标记-整理算法。
CMS收集器
一种以获取最短回收停顿时间为目标的收集器。很大一部分应用于互联网站或则B/S系统的服务端。基于标记清除算法实现。主要有四个步骤,初始标记,并发标记,重新标记,并发清除。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象速度很快。
并发标记阶段就是进行GC RootsTracing的过程,与用户进程同步。
重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要”stop the world”.这个阶段的停顿时间一般会比初始标记阶段稍长一些, 但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作, 所以, 从总体上 来说,
CMS收集器的内存回收过程是与用户线程一起并发执行的。 下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。
CMS也有很明显的缺点,主要是
1对CPU资源非常敏感
2无法处理浮动垃圾【重新标记解决的是将误标为不可达对象标记为可达,但是会漏标不可达对象,这部分对象为浮动垃圾】
3,使用标记清除算法会产生大量碎片
G1 收集器:一个面向服务器应用的垃圾收集器,主要有以下优点,并行与并发,分代收集,空间整合(整体上标记整理,局部是复制算法),可预测的停顿(相比CMS的一大优势)。将堆划为多个大小相等的独立区域(region),新生代老年代不再物理隔离了.
1)G1 之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集,G1追踪各个region里面的垃圾堆积的价值大小,后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region.
2)主要划分为四个步骤
初始标记- 并发标记- 最终标记 - 筛选回收
初始标记仅标记一下GC root 能关联到的对象,这个阶段需要停顿,但耗时短
并发标记是从GC root 进行可达性分析 ,找出存活对象. 耗时长,但是可并发
最终标记是为了修正并发标记期间因用户程序继续运作导致标记变动的记录 ,需要停顿,但是可多条线程同时执行。
筛选回收 根据各个region的回收价值和成本进行排序.根据用户期望的GC停顿时间来执行回收计划.
如果 Eden 空间占满了, 会触发 minor GC。 Minor GC 后仍然存活的对象会被复制到 S0 中去。 这样 Eden 就被清空可以分配给新的对象。又触发了一次 Minor GC , S0 和 Eden 中存活的对象被复制到 S1 中, 并且 S0和 Eden 被清空。 在同一时刻, 只有 Eden 和一个 Survivor Space 同时被操作。当每次对象从 Eden 复制到 Survivor Space 或者从 Survivor Space 中的一个复制到另外一个, 有一个计数器会自动增加值。 默认情况下如果复制发生超过 16.次, JVM 会停止复制并把他们移到老年代中去.同样的如果一个对象不能在 Eden 中被创建, 它会直接被创建在老年代中。 如果老年代的空间被占满会触发老年代的 GC, 也被称为 Full GC。 Full GC 是一个压缩处理过程, 所以它比 Minor GC 要慢很多。
综上,FULL GC 发生的原因有两种,①大对象分配时候引发老年代空间不足;②持续存活的对象转移到老年代引发的空间不足