文章目录
目录
目录
一、Java内存区域
1.1 运行时数据区域
1.1.1 程序计数器
作用:用于存储下一条指令的地址。可以看作是当前线程执行的字节码的行号指示器
特点:
- 线程私有。
- 执行java方法时,程序计数器是有值的,执行native本地方法时,程序计数器的值为空。
- 程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。
- 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
1.1.2 java虚拟机栈
java虚拟机栈(java virtual machine stack) ,早期也叫java栈,每个线程在创建是都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应这一次次的java方法调用。
Java虚拟机栈用于管理Java方法的调用
生命周期:生命周期和线程一致
作用:主管java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈的特点(优点)
- 栈是一种快速有效的分配存储方式,访问速度仅次于PC程序计数器。
- JVM直接对java栈的操作只有两个: a:每个方法执行,伴随着进栈(入栈、压栈)
c:执行结束后的出栈工作 - 对于栈来说,不存在垃圾回收问题
栈的运行原理:
- 不同线程的栈帧是不允许互相调用的,即不可能一个栈帧调用另一个栈帧。不存在安全问题
- 先进先出
1.1.3 本地方法栈
本地方法栈用于管理本地方法的调用
。本地方法栈,是线程私有的
。- 允许被实现成固定或者是可动态扩展的内存大小(在内存溢出方面和虚拟机栈相同
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError 异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutofMemoryError异常。
- 本地方法一般是使用C语言实现的。
- 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
1.1.4 Java堆
- 一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域。
- java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。(堆内存的大小是可以调节的)
- 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。
- 所有的对象实例以及数组都应当在运行时分配在堆上。
- “几乎”所有的对象实例都在这里分配内存
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
1.1.5 方法区
与java堆一样,是各个线程共享的内存区域,用于春初已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区存储的信息主要是跟类相关的信息。
方法区在虚拟机启动的时候被创建,它逻辑上是堆的一部分。也就是说,它在概念上定义了方法区是堆的一部分,但实际上它只是个规范,不同的虚拟机厂家去实现JVM时,不一定会遵从这个JVM的规范。
- 不需要连续的内存、可选择固定小或者可扩展
- 可以选择不是先垃圾收集
1.1.6 运行时常量池
用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字母量信息。
- 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,也就是并非预置人Class 文件中常量池的内容才能进人方法区运行时常量池,运行期间也可能将新的常量放入池中
1.1.7 直接内存
并不属于虚拟机运行时数据区的一部分
在JDK 1.4中新加入了NIO(New Input/Output)类,引人了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java堆和 Native堆中来回复制数据。
1.1.8 对象的创建过程
-
常量池中定位类的符号引用
-
检查符号引用所代表的类是否已被加载,解析和初始化过。如果没有,那必须先执行相应的类加载过程
-
为新生对象分配内存
-
初始化为零值 - 可以不赋就直接使用
-
设置对象头信息
-
执行对象方法 - 构造方法
1.2 垃圾收集器与内存分配策略
1.2.1 什么是垃圾?
- 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
- 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
1.3 标记阶段(对象是否存活)
- 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先
需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。
只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。 - 那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
- 判断对象存活一般有两种方式:
引用计数算法
和可达性分析算法
。
1.3.1 引用计数算法
- 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的
引用计数器属性。用于记录对象被引用的情况。
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
- 优点:
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。大部分情况下都是一个不错的算法
- 缺点:
- 它需要单独的字段存储计数器,这样的做法增加了
存储空间的开销
。 - 每次复制都需要更新计数器,伴随着加法和减法操作,这增加了
时间开销
。 - 引用计数器有一个严重的问题,即
无法处理循环引用
的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
- 它需要单独的字段存储计数器,这样的做法增加了
JAVA虚拟机没有选用引用计数法来管理内存,最主要的原因是他很难解决对象之间相互循环引用问题
1.3.2 可达性分析算法
所谓"GC Roots"根集合就是一组必须活跃的引用。
可作为GC ROOT的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)。
- 如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
- 如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
基本思路:
- 可达性分析算法是以根对象集合(GC Roos)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
注意:
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
- 这点也是导致cc进行时必须"stop The world"的一个重要原因。
- 即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时
也是必须要停顿的。
- 即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时
1.3.3 引用
1.强引用
强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2.软引用
用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。
3. 弱引用
弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
4.虚引用
就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
1.3.4 对象的finalization机制
- Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
- finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
try-finally或者其他方法可以做的更好,不建议使用
1.3.5 回收方法区
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的 ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述三个条件的无用类进行回收,不像对象一样不适用了就必然回收
1.4 垃圾收集算法
1.4.1 标记-清除算法
最基础的收集算法
执行过程:
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。(标记非垃圾)
- 清除:collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点:
- 效率不算高
- 在进行cc的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
注意∶何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
1.4.2 复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
特别的:
- 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大或者说非常低才行。
1.4.3 标记-整理算法
执行过程:
- 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
- 之后,清理边界外所有的空间。
与标记-清除算法的区别:
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策
优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点:
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,需要全程暂停用户应用程序。即:STW
1.4.4 三种垃圾回收算法的对比
Mark-Sweep | Mark-Compact | Copying | |
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍的大小(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
1.4.5 分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代
年轻代( Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
- Mark阶段的开销与存活对象的数量成正比。
- Sweep阶段的开销与所管理区域的大小成正相关。
- compact阶段的开销与存活对象的数据成正比。
1.4.6 增量收集算法
上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the world的状态。在stop the world 状态下,应用程序所有的线程都会挂起,暂停切正常的工作,等待垃圾回收的完成。如果垃吸回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental collecting)算法的诞生。
基本思想:
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记清理或复制工作。
缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
1.4.7 分区算法
一般来说,在相同条件下,堆空间越大,一次Gc时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制cc产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次Gc所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,
分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
1.5 垃圾回收器
1.5.1 7个经典收集器与垃圾分代之间的关系
1.5.2 垃圾收集器的组合关系
1.5.3 Serial收集器
简单介绍:
- Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集器的唯一选择,这个收集器是一个单线程的收集器。
- “单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
优点:
- Serial收集器依然是虚拟机在Client模式下的默认新生代收集器,它有着优于其他收集器的地方:
- 简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
缺点:
- 在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。
1.5.4 ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾回收之外,其余可控参数,收集算法,停止工作线程,对象分配原则,回收策略等与Serial收集器完全一致。
- Server模式下的新生代的首选的虚拟机收集器
- 除了Serial收集器外,只有它能与CMS配合使用
- 单CPU环境下绝对不会有比Serial收集器更好的效果
- 再年轻代中同样采用复制算法、“stop-the-world”
1.5.5 Parallel Scavenge收集器
简单介绍:
- Parallel Scavenge 收集器是⼀个新⽣代收集器,它也是使⽤复制算法的收集器,⼜是并⾏的多线程收集器,“Stop-the-World”机制。
- 由于与吞吐量关系密切,Parallel Scavenge 收集器也经常称为“吞吐量优先”收集器
- 吞吐量是什么?CPU⽤于运⾏⽤户代码的时间与CPU总时间的⽐值,99%时间执⾏⽤户线程,1%时间回收垃圾 ,这时候吞吐量就是99%
特点:
- Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时⽤户线程的停顿时间,⽽ Parallel Scavenge 收集器的⽬标则是达到个可控制的吞吐(Throughput)。所谓吞吐量就是 CPU ⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值,即吞吐量=运⾏⽤户代码时间/(运⾏⽤户代码时间+垃圾收集时间),虚拟机总共运⾏了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99% 停顿时间越短就越适合需要与⽤户交互的程序,良好的响应速度能提升⽤户体验,⽽⾼吞吐量则可以⾼效率地利⽤ CPU 时间,尽快完成程序的运算任务,主要适合在后台运算⽽不需要太多交互的任务。
- 虚拟机会根据当前系统的运⾏情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最⼤的吞吐量,这种调节⽅式称为 GC⾃适应调节策略
- -XX:MaxGCPauseMillis参数GC停顿时间,500MB ——>300MB,这个参数配置太⼩的话会发⽣频繁GC
- -XX:GCTimeRatio参数,99%
1.5.6 Parallel Old收集器
简单介绍:
- 是Parallel Scavenge收集器的老年代版本,用于老年代的垃圾回收,也是基于
并行回收和"Stop the World"机制
。但与Parallel Scavenge不同的是,它使用的是“标记-整理算法”。适用于注重于吞吐量及CPU资源敏感的场合。 - 使用方式:-XX:+UseParallelOldGC,打开该收集器后,将使用Parallel Scavenge(年轻代)+Parallel Old(老年代)的组合进行GC。
- 在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错。
1.5.7 CMS收集器:低延迟
简单介绍:
- CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
- CMS的垃圾收集算法采用标记-清除算法,并且也会"stop-the-world"
- CMS作为老年代的收集器,无法与JDK 1.4.0中已经存在的新生代收集器Parallel scavenge 配合工作,所以在JDK 1.5中使用cMS来收集老年代的时候,新生代只能选择ParNew或者serial收集器中的一个。
CMS步骤:
- 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
- 并发标记(Concurrent-Mark)阶段:从Gc Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。STW(很短)
- 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
CMS的优点:
- 并发收集
- 低延迟.
CMS的弊端:
- 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
- CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
- CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次 Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMs将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
1.5.8 小结
- 如果你想要最小化地使用内存和并行开销,请选serial GC;
- 如果你想要最大化应用程序的吞吐量,请选Parallel GC;
- 如果你想要最小化GC的中断或停顿时间,请选CMS GC。
1.5.9 G1
特点:
- 并行与并发
- 并行性: G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
- 并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
- 分代收集
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
- 空间整合
- CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
- G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
- 可预测的停顿时间模型(即:软实时soft real-time)
- 这是G1相对于CMS 的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个 Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1 收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
缺点:
- 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
- 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
使用场景:
- 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
- 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
- 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。
- 用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比cMS好:
- 超过50%的Java堆被活动数据占用;
- 对象分配频率或年代提升频率变化很大;
- GC停顿时间过长(长于0.5至1秒)。
- HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
G1的垃圾回收过程:
- 新生代GC
- 和其他垃圾收集器差不多,回收eden区,转移survivor区或者晋升年老的
- 并发标记周期
- 初始标记:标记从根节点可直接到达对象,会伴随一次ygc,STW
- 根区域扫描:因为已经进行了ygc,所以只有survivor区有对象,扫描survivor区到老年代的引用(不能有ygc,必须等结束才行, 因为ygc会导致survivor区变化)
- 并发标记:扫描并查找整个堆的存活对象,做好标记(可ygc,会中断标记过程)
- 重新标记:STW,使用SATB,会在标记之初为存活对象创建一个快照,加快重新标记的速度
- 独占清理:计算各个区域存活对象和回收比例并排序,识别可供混合回收的区域,更新RemeberedSet,该阶段给需要混合回收的区域做了标记,STW
- 并发清理:识别并清理完全空闲的区域
- 混合回收
- 并发标记周期,虽然有部分对象回收,但是总体上说,回收的比例是比较低的。
混合回收不仅进行ygc,而且会回收之前标记出来垃圾最多的区域
- 并发标记周期,虽然有部分对象回收,但是总体上说,回收的比例是比较低的。
- Full GC
- 在混合回收过程中,当内存不足时也会触发Full GC
①.concurrent mode failure
②.大对象分配失败
③.晋升失败
- 在混合回收过程中,当内存不足时也会触发Full GC