文章目录
如何判定对象为垃圾对象:
1、引用记数法
在对象中添加引用的标识,每对对象增加一个引用,引用标识进行+1操作,减少一个引用,标识-1,当标识为0的时候,说明对象不存在引用可以被回收;
缺点:无法处理对象间相互引用的问题;
当我们把上图的x1指针置为空的时候,那就没有指针指向这些对象了,也就是这些对象没人用了,应该作为垃圾被回收,但是因为这几个对象相互引用,导致它们的引用标识都不为0,所以这几个对象不会被标记为垃圾对象;
2、可达性分析
判断对象是否存活,将堆中对象想象成一棵树,从树根(GC root)开始遍历所有的对象,能到达的称为可用对象,不能到达的称之为垃圾;
GC root(树根)一定是可达的,主要有:
- 虚拟机栈中引用的对象一定是可达的;
- 本地方法栈中的JNI引用的对象;
- 方法区中静态属性引用的对象;
- 方法区中常量引用的对象;
这个判定方法就能解决引用计数法的问题了,只要从GC root节点出发,不可到达的对象就是垃圾对象,所以一般在虚拟机中判定对象是否是垃圾对象用的都是这种方法;
如何回收垃圾对象
回收策略
堆中的对象以及栈中的引用如下:
1、标记—清除
用可达性分析,先标记、再清除;
标记:
清除:
缺点:
①标记—清除之后会出现断断续续的空闲空间(内存碎片),空间无法高效利用,
②并且先标记后清除需要对堆空间前后两次遍历,效率不高;
2、复制算法
一开始,将堆空间内存分成两个部分A、B,只使用其中一部分,对象都是在A中进行分配的,B是空的,然后对A进行可达性分析,将可用的对象拷贝到另一个空间B去,再把A中的对象全部擦除,然后下一次分配空间是在B中分配,垃圾回收也是在B中进行,相当于是A、B换着来的,这样就把标记—清除算法的两个问题都解决了;
可达性分析,回收前:
回收后:
缺点:
①只使用一半堆空间,浪费一半的空间,
②如果对应的A那半部分出现极端情况(A全都是或大部分都是生命周期比较长的对象),那就需要全部拷贝);
3、标记—整理
先标记、再整理,先可达性分析,标记对象是否可用,然后将可用对象向一端移动,这样垃圾回收之后的堆空间的剩余空间是连续的;
回收前:
回收后:
缺点:
效率也不高,不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法;
可以这么理解,标记—整理算法也是把堆分了两个部分X、Y,把其中一部分X用来存放可用对象,把剩下的那一部分Y的可用对象全部放过来,把X的垃圾对象放出去,这就是标记—整理算法;
4、分代回收算法
这个算法把堆分为新生代和老年代:
新生代:朝生夕灭(也没这么久),存活时间短,老年代:经过多次minor GC依旧存在,存活时间比较长;
分代回收是对上面三种算法的通用:
- 在新生代中每次垃圾回收都发现有大量的对象死去,只有少量存活,因此采用复制算法回收新生代,只需要付出少量对象的复制成本就可以完成收集;
- 而老年代中对象的存活率高,不适合采用复制算法,而且如果老年代采用复制算法,它是没有额外的空间进行分配担保的,因此必须使用标记/清除算法或者标记/整理算法来进行回收。
总结一下就是,分代收集算法的原理是采用复制算法来收集新生代,采用标记—清除算法或者标记—整理算法收集老年代。
垃圾回收器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
下图展示了7种作用于不同分代的收集器:
- 其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge;
- 回收老年代的收集器包括SerialOld、Parallel Old、CMS,
- 还有用于回收整个Java堆的G1收集器。
1、Serial、Serial Old(单线程垃圾收集器)
Serial、Serial Old收集垃圾的方式都是下图那样的,只不过Serial使用复制算法收集的是新生代、Serial Old使用标记整理算法收集的是老年代;
这个收集算法就是当需要垃圾回收的时候,工作线程全部暂停,由一个单线程的垃圾回收线程回收垃圾对象,回收完成之后,工作线程继续工作,垃圾回收线程暂停,如此往复;
优点:标记和清理都是单线程,优点是简单高效;
这个收集器最基本,发展最悠久,单线程(整体性能低,单个看的话效率高),适合分配内存比较小的、收集起来速度比较快的场景,比如桌面应用;
2、ParNew(Serial的多线程版本)、Parallel Scavenge、Parallel Old
ParNew新生代收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现,它也是采用复制算法收集新生代;唯一可以和CMS合作收集垃圾的垃圾收集器;
Parallel Scavenge和ParNew一样都是使用复制算法的新生代多线程收集器,不同点在于最初设计的时候他们的关注点不同,Parallel Scavenge的关注点在于达到可控制的吞吐量(CPU运行用户代码的时间与CPU消耗的总时间的比值,吞吐量=执行用户代码消耗的时间/执行用户代码的时间+垃圾回收所占用的时间
);
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
以上两个收集器都是并发执行的,也就是不论是单线程(Serial、Serial Old)还是多线程(ParNew),收集垃圾的时候是要暂停工作线程的,而工作线程执行的时候垃圾回收线程是不能执行的,它们的收集垃圾的线程和工作线程是不能同时执行的;下面的垃圾回收器收集垃圾的线程和工作线程是可以并行执行的;
3、CMS收集器
使用标记—清除算法,老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
工作过程:
- 初始标记:标记GC root能直接关联的对象(比如上面可达性分析里面的对象1),这个非常快;
- 并发标记:就是接着标记从GC root直接关联的对象继续往下走能到达的对象;
- 重新标记:为了修正并发标记期间因用户程序基于运作而导致产生变动的那部分对象,也就是对并发标记做一个修正;
- 并发清理:把垃圾对象清理掉;
可以看到,CMS也不是完全的并行执行,但是它实现了垃圾回收过程中最耗时最基本的操作并发标记、垃圾清理的并行执行,这样:
- 吞吐量提高了很多,高并发;
- 低停顿,
但这样同时:
- 占用了大量的CPU资源;
- 无法处理浮动垃圾(这样并行执行的时候就像边打扫边扔垃圾一样,对于打扫过的地方,后面再扔的垃圾那就得等下一次打扫了);
- 出现Concurrent Mode Failure错误,这是因为清理垃圾的线程和工作线程并行执行,那么对于这些工作线程创建的新的对象我们得预留一块空间,这块空间留大了浪费资源,留小了不够用就会发生这个错误;
- 产生空间碎片,标记—清除算法导致的;
4、G1收集器
最牛逼的垃圾回收器,老年代和新生代它都可以用,它的优势:
- 并行和并发
- 分代收集
- 空间整合(标记—整理算法实现的)
- 可预测的停顿(能指定停顿不超过某一时间)
与CMS比较:吞吐量并不比CMS好多少,但在减少停顿方面G1比CMS强很多;
步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1收集器可以在几乎不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的由来)。区域划分、有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。
垃圾回收器总结
总结一下垃圾回收器吧:
垃圾回收器 | 采用的回收算法 | 线程数 | 回收的区域 | 备注 |
---|---|---|---|---|
Serial | 复制算法 | 单线程 | 新生代 | |
Serial Old | 标记—整理算法 | 单线程 | 老年代 | Serial收集器的老年代版本 |
ParNew | 复制算法 | 多线程 | 新生代 | Serial收集器的多线程版本 |
Parallel Scavenge | 复制算法 | 多线程 | 新生代 | 追求高吞吐量 |
Parallel Old | 标记—整理算法 | 多线程 | 老年代 | Parallel Scavenge的老年代版本 |
CMS | 标记—清除算法 | 多线程 | 老年代 | 最求最短GC停顿时间 |
G1 | 标记—整理算法 | 多线程 | 整个堆 | 高效 |
内存分配策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:
- 给对象分配内存 ;
- 回收分配给对象的内存。
一般而言,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB,下面会说),将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中。总的来说,内存分配规则并不是一层不变的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
我们知道,垃圾回收策略(选择哪种垃圾回收算法)决定了我们的堆是否规整,而堆是否规整决定了我们给新对象分配内存的策略(规整的话用指针碰撞,就是一边是空闲的空间,一边是存放对象的空间,中间拿一个指针隔开,增加对象的时候移动指针即可,而堆不规整的话使用空闲列表,就是记录空闲的位置,每次创建出一个新的对象都往空闲的、足够的地方补就是了),而不同的内存分配策略又决定了线程安全性问题(两种分配策略都有线程安全性问题),那么对于线程安全的处理一般有两种方法:
- 线程同步,就是给资源上锁,这样相当于加了synchronized关键字,对于访问此处的资源的线程,只能一个一个来,给串行化了,低效;
- 本地线程分配缓存(TLAB),就是事先给每个线程一块空间,每个线程创建的对象都放在自己对应的空间里面;
下面就来说说JVM的内存分配策略吧:
-
对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。
-
大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
-
长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
-
动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
需要注意的是,Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。也就是说,垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。
方法区的回收
方法区的内存回收目标主要是针对 常量池的回收 和 对类型的卸载。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。特别地,在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
推荐一篇好的博主博文→入口