JVM学习笔记(一)——Java内存模型

  1. Java内存模型
  2. 类加载机制(还有待深入)
  3. JVM字节码执行引擎(待….)
  4. Java内存模型与线程(待….)
  5. 线程安全与锁优化(待….)

一 Java内存模型

1、运行时数据区域

1.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,可以看成是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令。

线程私有,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响,独立存储。

如果线程正在执行一个Java Method, 这个计数器记录的是正在执行的虚拟机字节码指令的地址;

如果正在执行的 Native Method,这个计数器的值为空(Undefined)。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

1.2 Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)

线程私有,生命周期与线程相同。

虚拟机栈描述的Java方法执行的内存模型: 每个方法在执行的同时,都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean, byte, char, short, int, float, long ,double), 对象引用(reference类型, 它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
其中64位长度的 long, double类型的数据会占用2个局部变量空间(slot),其余的数据类型只占用 1 个。

局部变量表 所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在 帧中分配多大的局部变量空间是完全确定的,这个方法在运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常情况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  2. 如果虚拟机栈可以动态扩展(当前大部分JVM都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
1.3 本地方法栈

本地方法栈(Native Method Stack) , 与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行的Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

有的虚拟机(如:Sun HotSpot)直接将本地方法栈和Java虚拟机展合二为一。

与Java虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常。

1.4 Java 堆

Java堆(Java Heap)

对大多数应用来说,Java Heap是Java虚拟机所管理的内存中最大的一块。

Java Heap 是被所有线程共享的一块区域,在虚拟机启动时创建

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配。Java虚拟机规范描述的是:所有的对象实例以及数组都要在堆上分配。( The heap is the runtime data area from which memory for all class instances and arrays is allocated )。
但,随着JTI编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术导致一些微妙的变化,使其变得不那么绝对了。

Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称作为“GC”堆(Garbage Collected Heap)。

内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代老年代;再细分: Eden空间, From Survivor空间, To Survivor空间等。(详见后面 GC 部分)

内存分配的角度来看,线程共享的Java heap中可能划分出多个线程私有的分配缓存区(Thread Local Allocation Buffer, TLAB)。

无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。

根据Java虚拟机规范的规定,Java Heap可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

在实现时,Java heap可以实现成固定大小,也可以扩展的。当前主流了的JVM都是可扩展的(通过-Xmx和-Xms控制)

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

1.5 方法区

方法区(Method Area), 与Java heap一样是各个线程共享的内存区域。

用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是却有一个别名:No-Heap(非堆),目的是与Java堆区分。

HotSpot虚拟机上的开发人员,习惯上把方法区成为“永久代”(Permanent Generation),其实两者不等价。

这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

根据Java虚拟机规范的规定:当方法区无法满足内存分配的需求时,将会抛出OutOfMemoryError异常。

1.6 运行时常量池

运行时常量池(Runtime Constant Pool)方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是:常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于Class文件常量池的另外一个重要特征是:具备动态性。 Java并不需要常量一定只有在编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也能将新的常量放入池中,这种特征被开发人员利用的比较多的就是String类的intern()方法。

当常量池无法再申请到内存时,会抛出 OutOfMemoryError异常。

1.7 直接内存

直接内存(Direct Memory), 并不是JVM运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也可能导致OutOfMemoryError异常出现。

在jdk1.4中心加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式, 它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java 堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

附:JVM 内存参数

  • -Xmx 指定JVM的最大heap大小, e.g.: -Xmx256M
  • -Xms 指定JVM的最小heap大小, 高并发应用,建议和 -Xmx一样,防止因为内存收缩/突然增大带来的性能影响
  • -Xmn 指定JVM中New Generation的大小。这个参数很影响性能,如果你的程序需要比较多的临时内存,建议设置到512M,如果用的少,尽量降低这个数值,一般来说128/256足矣
  • -XX:PermSize= 指定JVM中Perm Generation的最小值,如:-XX:PermSize=32M。其实就是方法区
  • -XX:MaxPermSize= 指定Perm Generation的最大值
  • -Xss 指定线程栈大小,如:-Xss128k, 一般来说,webx框架下的应用需要256k。如果程序中有大规模的递归行为,考虑设置到512k/1M. 这个参数对性能的影响比较大的。
  • -XX:NewRatio= 指定JVM中Old Generation heap size 与 New Generation的比例。在使用CMS GC的情况下此参数失效。
  • -XX:SurvivorRatio= 指定New Generation中 Eden Space与**一个**Survivor Space的Heap size 比例。例:-XX:SurvivorRatio=8, 那么New Generation为10M时,Eden Space为8M。
  • -XX:MinHeapFreeRatio= 指定JVM heap在使用率小于n的情况下,heap进行收缩。Xmx==Xms的情况下无效。
  • -XX:MaxHeapFreeRatio= 指定JVM heap在使用率大于n的情况下,heap进行扩张。Xmx==Xms的情况下无效。
  • -XX:LargePageSizeInBytes= 指定Java heap的分页页面大小。

二、GC与内存分配

1 综述

1.1 可达性分析算法

在主流的商用编程语言实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存货的。

基本思想:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链(Reference Chain)”,当一个对象到GC Roots没有任何引用链相连的话(图论:从GC Roots到这个对象不可达)时,则证明这个对象是不可用的,所以判定为可回收对象。

在Java中,可作为GC Roots的对象包括下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象;
1.2 引用

jdk1.2 之后,Java对引用的概念进行了扩充,将引用分为:**强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)**4种。引用强度逐渐减弱。

  • 强引用:指在程序代码之中普遍存在的,类似“Object obj = new Object()” 这类引用,只要强引用存在,GC就永远不会回收这些对象;
  • 软引用:是用来描述一些还有用但并非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。jdk1.2之后,提供:SoftReference类来实现软引用
  • 弱引用:用来描述非必需对象的。但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次gc之前。当gc工作室,无论当前内存是否足骨,都会回收掉只被弱引用关联的对象。在jdk1.2之后,提供 WeakReference类来实现弱引用。
  • 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是:能在这个对象被gc回收时受到一个系统通知。在jdk1.2之后,提供了 PhantomReference类来实现虚引用
引用和队列的使用

强引用一班不会和队列一起使用

软引用可以和一个引用队列联合使用,一般软引用可以用来实现内存敏感的高速缓存,如果软引用的对象被gc回收,JVM就会把引用加入到与之关联的引用队列中去。

弱引用和引用队列一起使用,如果弱引用所引用的对象被回收了,JVM就会把这个弱引用加入到关联的队列中去

虚引用,在JVM回收虚引用时,会把这个虚引用放到与之挂念的引用队列中去。程序可以通过判断引用队列中是否已经引用了虚引用,来了解引用对象是否要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么可以在所引用的对象内存前,采取一些逻辑处理

1.3 对象生存还是死亡: finalize()方法

宣告一个对象的真正死亡,至少要经历两次标记过程:

如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那它将会被第一次标记并且执行一次筛选,筛选的条件是:此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机掉用过,虚拟机将这两种情况视为:“没有必要执行”;

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并且稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里的“执行”值虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是:如果一个finalize()方法中执行缓慢,或者发生了死循环(更极端),将很可能会导致F-Queue队列中的其它对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue队列中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链中的任何一个对象建立关联即可,那么在第二次标记时,它将会被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本还是那个它就真的被回收了。

1.4 回收方法区

方法区(HotSpot中的永久代)

主要回收的内容:废弃常量无用的类

判断一个常量是否是“废弃常量”比较简单

判断一个类是否是“无用的类”,比较严苛,需要同时满足下面3个条件:

  1. 该类的所有实例都已经被回收,也就是Java Heap中不存在该类的实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在大量使用发射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载的功能,以保证永久带不会溢出。

2 垃圾收集算法

算法思想

2.1 标记-清除算法(Mark-Sweep)

分为:标记、清除 两个阶段。

首先标记处所有需要回收的对象,在标记完成后同意回收所有被标记的对象。

不足:

  1. 效率问题。标记和清除两个过程效率都不高;
  2. 空间问题。标记清除之后会喊声大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另外一次GC操作。
2.2 复制算法(Copying)

为了解决效率问题,Copying算法就出现了。

它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样就使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况了,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

不足:Copying算法的代价是将内存缩小为原来的一半。在对象仍然存活时,需要进行较多的复制操作,效率将会变低。

现在的商业JVM都采用这种收集算法来回收新生代,不过不是按照1:1,研究表明:新生代中的对象98%都是“朝生夕死”的。

将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。

Hotspot默认Eden:Survivor = 8:1 (-XX:SurvivorRatio=8),也就是说每次新生代中可用的内存空间为整个新生代容量的90%。

当Survivor空间不够用时,需要依赖其他内存(这里只老年代)进行内存担保(Handle Promotion)。内存担保:当另外一块Survivor空间上没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

2.3 标记-整理算法(Mark-Compact)

过程和 标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

2.4 分代收集算法(Generational Collection)

根据对象的存活周期的不同,将内存划分为几块。

一般把Java堆分为:新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批的对象死去,只有少量存活,那就选复制算法。
老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清除”或“标记-整理”算法进行回收。

3 垃圾收集器

先附上:JVM Client模式 & Server模式介绍

JVM Server模式与Client模式启动,最主要的差别自傲与:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。可通过: java -version查看JVM处于什么工作模式

➜  ~ java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

Server VM!

Server模式会尝试收集更多的系统性能信息,使用更复杂的优化算法对程序进行优化。

因此,当系统完全启动并进入运行稳定期后,Server模式的执行速度会远快于Client模式。

3.1 Serial收集器

这个收集器是一个单线程收集器,但它的“单线程”的意义并不仅仅说明它只会只用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它进行垃圾收集时,必须停掉所有其它工作线程,直到它收集结束。

“Stop the world”,由虚拟机在后台自动发起和自动完成,在用户不可见的情况下吧用户正常工作的线程全部停掉,简直不能接受。

Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。

有点:简单而高效(与其它收集器的单线程比)

3.2 ParNew收集器

ParNew收集器是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(e.g.: -XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The Wold、对象分配规则、回收策略等都与Serial收集器完全一样。

是许多运行在Server模式下的虚拟机中首选的新生代收集器,

有一个很重要的与性能无关的原因:除了Serial收集器外,目前只有它能与CMS收集器配合工作

注:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定并行,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。

附:可以使用 -XX:ParallelGCThreads参数来限制垃圾收集的线程数。

3.3 Parallel Scavenge 收集器

新生代收集器,使用Copying算法,并行的多线程收集器

Parallel Scavenge收集器的关注点与其它收集器不同。
CMS等收集器关注点是:尽可能地缩短垃圾收集时用户线程的停顿时间
Parallel Scavenge收集器的目标是:达到一个可控制的吞吐量(Throughput)。

ps:吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即:吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

适合在后台运算而不需要太多交互的任务。

提供了两个参数用于控制精确控制吞吐量

  1. -XX:MaxGCPauseMillis 控制最大垃圾收集的停顿时间。允许的值是一个大于0的毫秒数,GC的停顿时间是以牺牲吞吐量和新生代空间来换取的
  2. -XX:GCTimeRatio 吞吐量大小。是一个大于0且小于100的整数,也就是垃圾收集时间的占比,相当于吞吐量的倒数

还有一个参数:

  • -XX:+UseAdaptiveSizePolicy 这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态的调整这些参数以提供最合适的停顿时间或者最大吞吐量。这种调节方式称为:GC自适应的调节策略(GC Ergonomics)
3.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法、

主要意义:给Client模式下的虚拟机使用

如果用在Server模式下,那么它主要有两大用途:

  1. 在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
  2. 作为CMS收集器的后备预案,在并发收集发生Cocurrent Mode Failure时使用。
3.5 Parallel Old收集器

Parallel Old是Parallel收集器的老年代版本,多线程收集器,使用“标记-整理”算法。

jdk1.6才提供的

3.6 CMS收集器

CMS( Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。

标记-清除 算法。不过较为复杂一些。 整个过程分为4个步骤:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中初始标记、重新标记 仍然需要“Stop the world”.

  • 初始标记 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
  • 并发标记 就是进行GC Roots Tracing的过程;
  • 重新标记 则是为了修正并发标记期间因为用户程序继续运行而导致的标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行额。

优点:并发收集,低停顿

三个明显的缺点:

  1. CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是:(CPU数量 + 3)/ 4 。也就是当CPU在4个以上是,并发回收时垃圾收集线占用不少于25%的CPU资源,并且随着CPU数量的增加而下降。当CPU不足4个时,比如2个,CMS对用户程序的影响就可能变得很大。为应付这种情况,虚拟机提供了一种称为:增量式并发收集器(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器的变种(不提倡使用。)
  2. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾称为“浮动垃圾”。由于在垃圾回收阶段,用户线程还需要运行,那么就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。启动阈值。在jdk1.5,CMS收集器当老年代使用了68%的空间后,就会激活CMS,可通过参数: -XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。在jdk1.6中,这个启动阈值提升到92%。(重要)要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这是虚拟机会启动后备预案:临时启动Serial Old收集器来重新进行老年代垃圾收集,这样停顿的时间就长了。so…..参数-XX:CMSInitiatingOccupancyFraction设置的太高很容易导致大量的“Concurrent Mode Failure”失败,性能反而降低。
  3. CMS的GC算法是“标记-清除”,意味着收集结束时,会产生大量的空间碎片。空间碎片过多,将会给大对象分配带来很大的麻烦,往往老年代还有很大空间剩余,但是无法找到足够大的空间来分配当前对象,不得不提前出发一次 Full GC。为了解决这个问题,虚拟机提供了两个参数:
    -XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的), 在CMS收集器要出发Full GC之前,开启内存碎片整理。内存整理是无法并发的。所以停顿时间会变长。
    —XX:CMSFullGCsBeforeCompaction 设置执行多少次不压缩的Full GC后,紧着来一次带压缩。默认值为0,表示每次进入Full GC都进行碎片整理。

3.7 G1收集器

G1(Garbage-First)

G1将整个Java heap划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java heap中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价格大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护了一个优先队表每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First名称由来)。

这种使用Region划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

与其它收集器吓你具有的特点:

  • 并行与并发
  • 分代收集
  • 空间整合 与CMS的“Mark-Sweep”不同,G1整体上来看是基于“Mark Compact”算法实现的收集器,从局部(两个Region之间)上来看是基于Copying算法实现的。但无论如何,这两种算法都不会产生内存空间碎片,收集后能提供规整的可用内存。
  • 可预测的停顿

在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是:便通过CardTable把相关信息记录到被引用对象所属的Region的Remembered Set之中。这样在GC时,在GC Roots的枚举范围中加入Remembered Set即可保证不需要进行全堆扫描也不会有遗漏

不计算维护Remembered Set的操作,G1可分为以下几个步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Fianl Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

    • 初始标记 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这阶段需要停顿线程,但耗时很短
    • 并发标记 从GC Roots开始对堆中对象进行可达性分析,找出存活的对象。这阶段耗时长,但是与用户线程并发执行。
    • 最终标记 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。这阶段需要停顿线程,但是可并行执行
    • 筛选回收 首先对各个Region的回收截止和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

4 内存分配及回收策略

对象的内存分配,往大方向将,就是在heap上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下,也可能直接分配在老年代中,分配的规则并不是100%固定的,取决于使用的是哪一种收集器组合,还有JVM的参数设置。

JVM提供 -XX:+PrintGCDDetails 参数启动收集日志打印日志

注:

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一班回收速度也比较快。
  • 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次Minor GC(但非绝对)。Major GC的速度一般会比Minor GC慢10倍以上。

Serial / Serial Old 收集器下:

4.1 对象优先分配在 Eden区

大多数情况下,对象在新生代的Eden区分配。当Eden去没有足够的空间进行分配时,JVM将发起一次 Minor GC

4.2 大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时,就提前触发GC以获取足够的连续空间来存放大对象。

JVM提供了一个参数: -XX:PretenureSizeThreshold, 大于这个设置的值得对象直接在老年代分配。目的是:避免在Eden区以及两个Survivor区之间发生大量的内存复制。

注:XX:PretenureSizeThreshold参数只对Serial和ParNew收集器有效。

4.3 长期存活的对象将进入老年代

JVM给每个对象定义了一个:对象年龄(Age)计数器.

如果对象在Eden区出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每经历一次Minor GC,Age 就加一,当它的年龄增加到一定程度(默认15岁),就会被晋升到老年代中。ps:对象晋升到老年代的阈值可以通过参数: -XX:MaxTenuringThreshold设置

4.4 动态对象年龄判定

为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold值才能晋升老年代。

如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象将可以直接进入老年代,无须等到MaxTenuringThreshold中要求的Age。

4.5 空间分配担保

在发生Minor GC前,JVM会先检查老年代最大可用的连续内存空间是否大于新生代所有对象的总空间,
如果大于:那么Minor GC可以确保是安全的;
如果小于:那么JVM会查看HandlePromotionFailure设置值是否允许担保失败。
如果允许:那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,
如果大于,将尝试进行一次Minor GC,尽管这次GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时将进行一次Full GC。

经验值。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值