文章目录
- 垃圾回收机制
-
- 方法区的回收
- 堆回收
-
- 基本介绍
- 五种对象引用
- 垃圾回收算法
-
-
- 标记清除算法
- 复制算法
- 标记整理算法
- 分代垃圾回收算法
-
- 垃圾回收器
-
- Serial 收集器
- ParNew 收集器
- Parallel Scavenge 收集器
- CMS 收集器
- Serial Old 收集器
- Parallel Old 收集器
- G1垃圾回收器
- 总结
- 面试题
-
- 介绍一下Java的垃圾回收机制
- JVM中一次完整的GC流程是怎样的?
- Full GC会导致什么?
- JVM什么时候触发GC,如何减少FullGC的次数?
- 对象如何晋升到老年代?
- 为什么老年代不能使用标记复制?
- 为什么新生代和老年代要采用不同的回收算法?
- 内存泄漏和内存溢出有什么区别?
垃圾回收机制
C/C++的内存管理
- 在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。
- 内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
Java的内存管理
java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃
圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。其他很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器
垃圾回收的对比
**自动垃圾回收 **自动根据对象是否使用由虚拟机来回收对象
• 优点:降低程序员实现难度、降低对象回收bug的可能性
• 缺点:程序员无法控制内存回收的及时性
**手动垃圾回收 **由程序员编程实现对象的删除
• 优点:回收及时性高,由程序员把控回收的时机
• 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题
方法区的回收
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会
自动弹出栈并释放掉对应的内存。
方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:
1、此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
2、加载该类的类加载器已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用。
- 可以调用System.gc()方法手动触发垃圾回收,但是并不一定会立即回收垃圾,只是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收由Java虚拟机自行判断。
- 开发中此类场景一般很少出现,主要在如 OSGi、JSP 的热部署等应用场景中。每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
堆回收
基本介绍
- 如何判断堆上的对象可以回收?
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还 在使用,不允许被回收。
- 如何判断堆上的对象没有被引用?
常见的有两种判断方法:引用计数法 和可达性分析法。
- 引用计数法
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。
优点:
- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收
- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误
- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象
缺点:
1.每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
2.存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
- 可达性分析算法
- 可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。
GC Root(垃圾收集根节点)是指在垃圾回收过程中被认为是活动对象 并且不能被回收的对象 。GC Root对象具有特殊的引用关系,保持着对其他对象的直接或间接引用,而其他对象通过这些引用与GC Root对象相互关联。它们被认为是内存中的起始点,GC Root对象以及可以从GC Root对象直接或间接访问到的对象都会被视为活动对象,并且不会被垃圾回收器回收。
基本原理:
- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
- 哪些对象被称之为GC Root对象呢?
- 线程Thread对象:正在运行的线程是GC Root 对象,因为它们是不可中断的。
- 系统类加载器加载的java.lang.Class对象:被系统类加载器加载的类的 Class 对象也是 GC Root 对象,因为它们是类加载过程中必不可少的。
- 监视器对象,用来保存同步锁synchronized关键字持有的对象。
- 本地方法调用时使用的全局对象。
- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等
- 堆中类静态属性引用的对象
- 方法区中的常量引用的对象
- 字符串常量池(string Table)里的引用
- 查看垃圾回收日志
五种对象引用
- 强引用
被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收
- 强引用可以直接访问目标对象
- 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象
- 强引用可能导致内存泄漏
- 软引用
软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。
软引用的执行过程如下:
1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象) 。
2.内存不足时,虚拟机尝试进行垃圾回收。
3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
4.如果依然内存不足,抛出OutOfMemory异常。
java
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
- 软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收。如何知道哪些SoftReference对 象需要回收呢?
SoftReference提供了一套队列机制:
1、软引用创建时,通过构造器传入引用队列
2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
3、通过代码遍历引用队列,将SoftReference的强引用删除
- 弱引用
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。 在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收。
java
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
- 虚引用
虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道
直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
- 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象
- 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
java
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
- 终结器引用
终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后 由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该 对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。
垃圾回收算法
- **垃圾回收要做的有两件事: **
1、找到内存中存活的对象
2、释放不再存活对象的内存,使得程序能再次利用这部分空间
- 垃圾回收算法分分类
- 垃圾回收算法的评价标准
Java垃圾回收过程会通过单独的GC线程 来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所
有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
- 垃圾回收算法的评价标准
1.吞吐量
吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 /
(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高
2.最大暂停时间
最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中,黄色部分的STW就是最
大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时受到的影响就越短。
**3.堆使用效率 **
不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算
法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。
- 堆内存越大,垃圾回收器要扫描所回收的对象范围就大,GC线程时间执行长时间最大暂停时间就越长。
- 想要减少最大暂停时间,一次GC线程只回收部分对象,GC线程执行频率增加,而线程每次启动前期准备耗时长,就会就会降低吞吐量。
- 不同的垃圾回收算法,适用于不同的场景
标记清除算法
标记清除算法的核心思想分为两个阶段:
- 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
- 清除阶段,从内存中删除没有被标记也就是非存活对象
优缺点:
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:1. 碎片化问题:由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
- 分配速度慢。由于内存碎片的存在,需要维护一个空闲链表(记录着每块空闲内存的地址),极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
复制算法
复制算法的核心思想是:
- 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。
- 在垃圾回收GC阶段,将From中存活对象复制到To空间。
- 将两块空间的From和To名字互换。
完整的复制算法的例子:
- 将堆内存分割成两块From空间 To空间,对象分配阶段,创建对象,存储在From空间。
- GC阶段开始,将GC Root搬运到To空间 。
- 将GC Root关联的对象,搬运到To空间 。
- 清理From空间,并把名称互换。
复制算法的优缺点
- 吞吐量高
复制算法只需要遍历一次存活对象复制到To空间即可,比**标记-整理(清除对象一次,整理对象一次)**算法少了一次遍历的过程,因而性 能较好,但是不如标记-清除算法, 因为标记清除算法不需要进行对象的移动
- 不会发生碎片化
复制算法在复制之后就会将对象按顺序放 入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
- 内存使用效率低
每次只能让一半的内存空间来为创 建对象使用
标记整理算法
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。
- 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出
所有存活对象。
- 整理阶段,将存活对象移动到堆的一端。清理掉存活对象以外的内存空间。
优缺点
优点
- 内存效率使用高:整个堆内存都可以使用
- 不会发生碎片化:在整理阶段可以将对象往内存的一侧进行 移动,剩下的空间都是可以分配对象的有效空间
缺点
- 整理阶段效率不高:整理算法有很多种,比如Lisp2整 理算法需要对整个堆中的对象搜索3次,整体性能 不佳。可以通过TwoFinger、表格算法、ImmixGC等高 效的整理算法优化此阶段的性能
分代垃圾回收算法
分代垃圾回收将整个内存区域划分为年轻代和老年代
- 创建出来的对象,首先会被放入Eden伊甸园区。
- 随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。
- Minor GC会把Eden和From中需要回收的对象回收,把没有回收的对象放入To区。
- 接下来,To区变为From区,当Eden区满时再往里放入对象,依然会发生Minor GC。 此时会再次回收eden区和From中的对象,并把Eden和From区中剩余的对象放入To区。
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
- 如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。
- 当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
- 如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常
为什么分代GC算法要把堆分成年轻代和老年代?
- 系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回给用户之后就可以释放了。 针对这种情况,使用复制算法的新生代垃圾回收器是比较适合的。由于新生代中存活时间较短,复制算法能够快速地将存活对象复制到 To 空间,并回收 From 空间的对象,不会产生较大的性能开销。同时,复制算法结合空间分配,可以减少内存碎片,提高内存利用率。
- 老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。 针对这种情况,建议将老年代设置得相对较大,使用标记-清除算法 或标记-整理算法的老年代垃圾回收器。由于老年代中存放的对象生命周期比较长,需要减少垃圾回收的频率,因此可以将老年代设置得较大。同时,标记-清除算法和标记-整理算法对老年代进行垃圾回收时,不需要将存活的对象复制到其他区域,因此在一定程度上可以减少性能的开销。
- 在虚拟机的默认设置中,新生代大小要远小于老年代的大小。
故有以下几个原因:
- 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
- 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,**老年代可以选择标记-清除和标记-整理 算法,由程序员来选择灵活度较高。 **
- 分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(full gc),STW时间就会减少。
在arthas中查看分代之后的内存情况:
垃圾回收器
垃圾回收器的组合关系
垃圾回收器是垃圾回收算法的具体实现。 由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。 具体的关系图如下:
Serial 收集器
- Serial 翻译为串行,也就是说它以串行的方式执行。
- 它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
- 它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
- 它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
ParNew 收集器
它是 Serial 收集器的多线程版本。
Parallel Scavenge允许手动设置最大暂停时间和吞吐量。
Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。
停顿时间较短:因为多个线程进行垃圾回收,回收速度快
吞吐量低:多线程占用CPU执行时间长,导致吞吐量低
Parallel Scavenge 收集器
-
与 ParNew 一样是多线程收集器。
-
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为"吞吐量优先"收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
-
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
-
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
-
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
CMS 收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。用户体验好。
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高,线程多切换。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
Serial Old 收集器
SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
G1垃圾回收器
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
- Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小。
- CMS关注暂停时间,用户体验好,但是吞吐量方面会下降。
而G1设计目标就是将上述两种垃圾回收器的优点融合:
1.支持巨大的堆空间回收,并有较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。
JDK9之后强烈建议使用G1垃圾回收器。
G1垃圾回收器 -- 内存结构
G1出现之前的垃圾回收器,内存结构一般是连续的,如下图:
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参XX:G1HeapRegionSize=32m指定(其 中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
G1垃圾回收有两种方式:
- 年轻代回收(Young GC) :回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数 -XX:MaxGCPauseMillis=n(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。
- 混合回收(Mixed GC)
G1垃圾回收器 -- 执行流程
- 新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC。
- 标记出Eden和Survivor区域中的存活对象,
- 根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。 比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。
G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个优先列表,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率
- 后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
- 当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
- 部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是 4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。
混合回收
- 混合回收分为:初始标记(initial mark)、并发标记(concurrent mark)、最终标记(remark或者Finalize Marking)、并发清理(cleanup)
- G1对老年代的清理会选择**存活度最低的区域(存活对象最少的区域)**来进行回收,这样可以保证回收效率最高,这也是G1(Garbage first)名称的由来
- 如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法, 此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。
总结
Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同:
- 最小化地使用内存和并行开销,选 Serial GC
- 最大化应用程序的吞吐量,选 Parallel GC
- 最小化 GC 的中断或停顿时间,选 CMS GC
面试题
介绍一下Java的垃圾回收机制
从以下四个方面回答
- 首先我们需要知道哪些内存需要回收
在Java内存运行时区域的各个部分中分为堆、栈、方法区。栈是线程独占的,与线程的生命周期相同。所以;主要就是回收方法区和堆区。
- 怎么定义垃圾
引用计数算法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值 就减一;任何时刻计数器为零的对象就是不可能再被使用的。
但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这 个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的 引用计数就很难解决对象之间相互循环引用的问题。
举个简单的例子:对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问, 但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
可达性分析算法:
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
如下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参 数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 如何回收垃圾,也就是垃圾回收算法
标记清除算法、标记整理算法、复制算法、分代回收算法
- 实现垃圾回收算法的垃圾回收器。
又分为老年代、新生代的垃圾回收器,上面有详解,这里就不过多赘述了。
JVM中一次完整的GC流程是怎样的?
新创建的对象一般会被分配在新生代中,常用的新生代的垃圾回收器是ParNew 垃圾回收器,它按照8:1:1 将新生代分成Eden 区,以及两个Survivor 区。某一时刻,我们创建的对象将Eden 区全部挤满,这个对象就是挤满新生代的最后一个对象。此时,Minor GC 就触发了。
在正式Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。为什么要做这样的检查呢?原因很简单,假如Minor GC 之后Survivor 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:
- 老年代剩余空间大于新生代中的对象大小,那就直接Minor GC,GC完survivor不够放,老年代也绝对够放;
- 老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了"老年代空间分配担保规
则",具体来说就是看-XX:-HandlePromotionFailure
参数是否设置了。
老年代空间分配担保规则是这样的,如果老年代中剩余空间大小,大于历次Minor GC 之后剩余对象的大小,那就允许进行Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。那就有两种情况:
- 老年代中剩余空间大小,大于历次Minor GC之后剩余对象的大小,进行Minor GC;
- 老年代中剩余空间大小,小于历次Minor GC之后剩余对象的大小,进行Full GC,把老年代空出来再检查。
开启老年代空间分配担保规则只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:
- Minor GC 之后的对象足够放到Survivor 区,皆大欢喜,GC 结束;
- Minor GC 之后的对象不够放到Survivor 区,接着进入到老年代,老年代能放下,那也可以,GC 结束;
- Minor GC 之后的对象不够放到Survivor 区,老年代也放不下,那就只能Full GC。
前面都是成功GC 的例子,还有3 中情况,会导致GC 失败,报OOM:
- 紧接上一节Full GC 之后,老年代任然放不下剩余对象,就只能OOM;
- 未开启老年代分配担保机制,且一次Full GC 后,老年代任然放不下剩余对象,也只能OOM;
- 开启老年代分配担保机制,但是担保不通过,一次Full GC 后,老年代任然放不下剩余对象,也是能OOM。
Full GC会导致什么?
Full GC会"Stop The World",即在GC期间全程暂停用户的应用程序。
JVM什么时候触发GC,如何减少FullGC的次数?
当Eden 区的空间耗尽时Java 虚拟机便会触发一次Minor GC 来收集新生代的垃圾,存活下来的对象, 则会被送到Survivor 区,简单说就是当新生代的Eden区满的时候触发Minor GC。
serial GC 中,老年代内存剩余已经小于之前年轻代晋升老年代的平均大小,则进行Full GC4。
而在CMS等并发收集器中则是每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行Full GC 回收。
可以采用以下措施来减少Full GC的次数:
- 增加方法区的空间;
- 增加老年代的空间;
- 减少新生代的空间;
- 禁止使用System.gc()方法;
- 使用标记-整理算法,尽量保持较大的连续内存空间;
- 排查代码中无用的大对象。
对象如何晋升到老年代?
虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生, 如果经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
设置。
为什么老年代不能使用标记复制?
因为老年代保留的对象都是难以消亡的,而标记复制算法在对象存活率较高时就要进行较多的复制操 作,效率将会降低,所以在老年代一般不能直接选用这种算法。
为什么新生代和老年代要采用不同的回收算法?
如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每 次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大 量的空间。如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来 回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
内存泄漏和内存溢出有什么区别?
内存泄漏(memory leak):内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其他程序,于是就发生了内存泄漏。
内存溢出(out of memory):简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。