一. 概述
由于程序计数器、虚拟机栈和本地方法栈都是跟线程相关的,栈中的栈帧随着方法的进入和退出进行着出栈和入栈的操作,当方法结束的时候,这部分内存也会跟着回收,所以一般gc讨论的都是方法区和堆。
GC对象的判断
1. 引用计数法
给对象添加一个计数器,有人引用就加1,引用失效就减1,任何时刻计数器为0的对象就不可能再被使用了。
2. 可达性分析算法
判断当前对象与GC Root是否可达
可作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
说的通俗一点就是:方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。在网上看老外说的更具体分成了下面这些:
- System Class
Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .
- JNI Local
Local variable in native code, such as user defined JNI code or JVM internal code.
- JNI Global
Global variable in native code, such as user defined JNI code or JVM internal code.
- Thread Block
Object referred to from a currently active thread block.
- Thread
A started, but not stopped, thread.
- Busy Monitor
Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.
- Java Local
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
- Native Stack
In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.
- Finalizable
An object which is in a queue awaiting its finalizer to be run.
- Unfinalized
An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.
- Unreachable
An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.
- Java Stack Frame
A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.
13.Unknown
An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.
3. 引用的分类
在java1.2之后对引用的概念进行了扩充,有了4种引用
- 强引用:程序代码中普遍存在的类似
1
Object o = new Object();
只要强引用还在,GC就不会回收掉被引用的对象。
- 软引用: 描述一些还有用但是非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果此时内存还不够才会oom
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
- 弱引用: 也是描述非必须对象,强度比弱引用还要弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当gc工作无论当时内存是否足够都会回收掉只被弱引用关联的对象。
- 虚引用: 最弱的引用关系。一个对象是否有虚引用完全不会对其生存周期构成影响,也无法通过虚引用来获取一个对象实例。设置他的目的就是在被GC回收的时候收到一个系统通知
4. 可达性分析
若一个对象的引用类型有多个,那到底如何判断它的可达性呢?其实规则如下:
单条引用链的可达性以最弱的一个引用类型来决定;
多条引用链的可达性以最强的一个引用类型来决定;
我们假设图2中引用①和③为强引用,⑤为软引用,⑦为弱引用,对于对象5按照这两个判断原则,路径①-⑤取最弱的引用⑤,因此该路径对对象5的引用为软引用。同样,③-⑦为弱引用。在这两条路径之间取最强的引用,于是对象5是一个软可及对象
5. 回收方法区
废弃常量和无用类的回收
回收废弃常量类似回收对象,比如一个字符串常量,没有被任何String对象使用,如果这时发生了内存回收,而且有必要的话,会被系统清理出常量池
废弃类的判断要同时满足下面3个条件:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射方位该类的方法
在java8中由于永久代的废弃,类信息保存在本地内存中,所以废弃类导致oom溢出的情况得到了很大的改善。
二. 垃圾收集算法
1. 标记清除算法
对象真正死亡要经历2次标记过程
- 对象在进行可达性分析后,没有与GC ROOTS相连接的引用链,那么它会被第一次标记,并进行一次筛选,筛选条件是此对象是否有必要执行的finalize()方法。以下是2种没有必要执行finalize()方法的情形。
- 对象没有覆盖finalize()
- finalize()方法已经被虚拟机调用过。因为finalize()方法只会被虚拟机调用一次。
如果要执行finalize()方法,则会将对象加入F——QUEUE的队列中,并且会在稍后由jvm自动建立的、低优先级的Finalizer线程去执行。这里的执行只是会去触发这个方法并不能承诺会等待方法运行结束。因为如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,会导致F-Queue中其他对象处于永久等待,甚至导致整个内存回收系统崩溃。对象可以在finalize()方法中重新和引用链建立关联,这样就可以逃脱回收。
- GC对F-Queue中的对象进行第二次小规模的标记。也就是说如果没有执行finalize()方法进入F-Queue那么直接就被删除了。
finalize()是对象逃脱死亡的最后机会,但是finalize()是对c++的妥协,他的运行代价高,不确定性大,使用try——finally能更好的完成他需要执行的工作,所以不推荐使用它。
缺点:
- 效率问题,标记和清楚2个过程的效率都不高
- 空间问题,标记清除之后会长生大量的不连续碎片,碎片太多在分配较大对象的时候,无法找到足够的连续内存不得不触发领一次垃圾收集动作
2. 复制算法
复制算法(新生代 GC)
基于大多数新生对象(98%)都会在GC中被收回的假设。新生代的GC 使用复制算法。
在GC前To 幸存区(survivor)保持清空,对象保存在 Eden 和 From 幸存区(survivor)中,GC运行时,Eden中的幸存对象被复制到 To 幸存区(survivor)。针对 From 幸存区(survivor)中的幸存对象,会考虑对象年龄,如果年龄没达到阀值(tenuring threshold),对象会被复制到To 幸存区(survivor)。如果达到阀值对象被复制到老年代。复制阶段完成后,Eden 和From 幸存区中只保存死对象,可以视为清空。如果在复制过程中To 幸存区被填满了,剩余的对象会被复制到老年代中。最后 From 幸存区和 To幸存区会调换下名字,在下次GC时,To 幸存区会成为From 幸存区。当survivor空间不够的时候会进行分配担保,把对象保存到老年代。
- 优点:运行高效,实现简单
- 缺点:会浪费一定的内存,对象存活率较高会产生过多的复制
上图演示GC过程,黄色表示死对象,绿色表示剩余空间,红色表示幸存对象
3. 标记-整理算法(老年代)
由于老年代的对象存活时间久,使用复制算法将进行大量复制操作,效率很低,而且其存在时间久,很可能出现大面积的存活对象,这样在极端情况下100%内存的对象都存活,就还需要额外的空间分配担保。
标记-整理算法类似之前的标记清楚算法,但是他不会直接清除可回收对象,而是将存活对象都移动到一段,再直接清理边界以外的内存。
4. HostSpot的实现
1. 枚举GCRoot
通过OopMap数据结构,在类加载的时候,就把对象内什么偏移量上是什么类型的数据计算出来了。
2. 安全点
会导致OopMap内容变化的指令非常多,所以只会在特定的位置记录这些信息,这样的位置称为安全点。 比如:方法调用、循环跳转、异常跳转等。
在这个点, 所有GC Root的状态都是已知并且heap里的对象是一致的; 在这个点进行GC时, 所有的线程都需要block住, 这就是(STW)Stop The World.
在安全点终端所有线程的两种方法
- 抢先式中断:所有线程中断,如果有的线程不在安全点上就恢复它,让其执行到安全点再中断,但是几乎没有jvm采用这种方式。
- 主动式中断:在和安全点重合的地方设置一个轮询标志,让线程执行的时候主动去轮询这个标志,如果中断标志为真,就自己中断挂起。
3. 安全区域safe region
针对于处于Sleep状态或者blocked状态的线程,指的是在一段代码片段之中引用关系不会发生变化,在这片区域的任何地方GC都是安全的。
当线程执行到安全区域的时候首先会标识自己进入了安全区域,这样在这段时间内都可以进行gc,当要离开安全区域时,会检查是否已经完成了gcroot的枚举,如果完成了就继续执行,否则就等待。
5. 垃圾收集器
连线代表可搭配使用
1.Serial收集器(Client模式默认新生代收集器,复制算法,单线程)
在进行垃圾回收的时候,暂停所有正在工作的线程,直到结束。
优点:简单高效。
2. ParNew收集器(新生代,复制,多线程)
ParNew就是Serial的多线程版本,除了多线程外几乎一致。Server模式下的首选新生代收集器,只有他和Serial能和cms配合。使用-XX:+UseParNewGC开关来控制,使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
3. Parallel Scavenge收集器(新生代,复制,多线程,吞吐量优先)
吞吐量=运行用户代码时间/(运行用户代码时间+gc时间)
Parallel Scavenge收集器不像别的收集器关注的是减少用户停顿时间,而是吞吐量。
-XX:+UseParallelGC开关控制
-XX:MaxGCPauseMillis控制最大gc停顿时间
-XX:GCTimeRatio设置吞吐量
-XX:+UseAdaptiveSizePolicy,自动的动态调整停顿时间和吞吐量,GC的自适应调节策略,适合新手。
4. Serial Old收集器(老年代,标记-整理,单线程)
是Serial收集器的老年代版本
1.5之前和Parallel Scavenge搭配使用或者作为cms的备案
5. Parallel Old收集器(老年代, 标记-整理,多线程)
Parallel Scavenge的老年代版本,适合注重吞吐量和cpu资源敏感的场合
6. CMS收集器(老年代,标记-清理,多线程)
以获取最短回收停顿时间为目标的收集器。
过程:
- 初始标记:标记一下GCROOT能关联到的对象stw
- 并发标记:GCROOT Tracing
- 重新标记:修正并发标记期间产生变动的引用stw
- 并发清除
在初始标记和重新标记的时候需要stop the world
优点:
并行,停顿小。
缺点:
- cpu资源敏感:
因为并发导致吞吐量降低,随着cpu数量的下降对程序影响越大 无法处理浮动垃圾:
因为并发清理是并发执行的,所以此时还是会产生心的垃圾,称之为浮动垃圾。这部分垃圾只能等待下一次的GC。这里产生一个问题就是老年代不能等到完全的利用,需要一部分用来存储浮动垃圾,如果预留的内存无法满足需要就会产生“ConcurrentModeFailure”,这是会启用Serial Old收集器进行gc。
- 使用标记-清除算法产生大量内存碎片
7. G1收集器
特点:
- 缩短stop the world停顿时间,通过并发的方式将停顿并发执行从而取消停顿。
- 分代收集,不需要其他收集器单独管理整个gc堆,采用不同方式处理新对象和老对象
- 不会产生碎片。
- 可预测的停顿
G1收集的范围是整个新生代和老年代,但是他是将整个堆划分为多个大小相等的独立区域(Region),它会优先回收价值最大的Region,保证在有限时间获取最高的收集效率。根据用户所期望的GC停顿时间来指定回收计划。
- 初始标记
标记GC ROOTs能直接关联到的对象,耗时短
- 并发标记
可达性分析,耗时长,但是可以和用户线程并发
- 最终标记
需要停顿,但是可以并行执行
- 筛选回收
对回收价值和成本进行排序
其他收集器的范围都是整个新生代或者老年代,G1自己管理整个,他将heap的内存布局划分成多个大小相等的独立区域(region),虽然还保留新生代和老年代的概念。
垃圾堆积的价值:回收所获得的空间大小以及回收所需时间的经验值的关系
维护一个优先队列,优先回收价值最大的region
g1和cms区别
CMS收集器:是基于标记清除算法实现的,一般就是初始标记,并发标记,重新标记,并发清除,目的是实现最短的响应回收时间。保证系统的响应时间,减少垃圾收集时的停顿时间
G1收集器:他的过程是初始标记、并发标记、最终标记、筛选回收,基于标记整理算法实现,以吞吐量优先,保证保证吞吐量的。
内存分配机制
新生代与老年代
为了优化垃圾回收的性能,将堆分为了新生代和老年代
优点:
- 是简化了新生对象的分配(只在新生代分配内存),
- 是可以针对不同区域使用更有效的垃圾回收算法。
新生代
通过广泛研究面向对象实现的应用,发现一个共同特点:很多对象的生存时间都很短。同时研究发现,新生对象很少引用生存时间长的对象。结合这2个特点,很明显 GC 会频繁访问新生对象。在新生代中,GC可以快速标记回收”死对象”,而不需要扫描整个Heap中的存活一段时间的”老对象”。
SUN/Oracle 的HotSpot JVM 又把新生代进一步划分为3个区域:一个相对大点的区域,称为”伊甸园区(Eden)”;两个相对小点的区域称为”From 幸存区(survivor)”和”To 幸存区(survivor)”。按照规定,新对象会首先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden 中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。
- 新生代GC(Minor GC):发生在新生代的垃圾收集动作,频繁,回收速度快
- 老年代GC(Full GC/ Major GC):发生在老年代的GC,一般比Minor GC慢10倍
对象优先在Eden分配
当Eden不够时,发起一次Minor GC大对象直接进入老年代
比如很长的数组和字符串
—XX:PretenureSizeThreshold参数,零大于这个值的对象直接在老年代分配
长期存活的对象进入老年代
经过一次Minor GC后存活进入Survivor的对象设置为1岁,每经过1次Minor GC加一岁,达到一定岁数进入老年代。
动态对象年龄判定
survivor空间中相同年龄所有对象大小的总和大于单个survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代
空间分配担保
在Minor Gc前检查老年代的连续空间大于新生代对象总大小,if (true)则这次Minor GC是安全的。
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会尽心Minor GC 否则 full GC。
参考地址
http://ifeve.com/useful-jvm-flags-part-5-young-generation-garbage-collection/