Java虚拟机(JVM)

1 篇文章 0 订阅
1 篇文章 0 订阅

1.java内存区域和内存溢出异常

1.1 运行时数据区域

Java虚拟机(JVM)

根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9FLy7bKT-1575962908320)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p31)]

1.1.1 程序计数器(Program Counter Register)

内存小,线程私有。字节码解释器工作时就是通过改变 这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节 码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此 内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

线程私有:由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现 的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行 一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要 有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内 存区域为“线程私有”的内存。

1.1.2 Java虚拟机栈(Java Virtual Machine Stacks)

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

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

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、 float、long、double),对象引用(reference类型,它不等同于对象本身,根据不同的虚拟 机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或 者其他与此对象相关的位置)和retumAddress类型(指向了一条字节码指令的地址)

StackOverflowError:如果线程请求的栈深度大 于虚拟机所允许的深度
OutOfMemoryError:如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时

1.1.3 本地方法栈(Native Method Stacks)

与虚拟机栈所发挥的作用是非常相似的,其区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务.本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError 异常。

有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

1.1.4 Java堆(Java Heap)

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的 唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap).Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

1.1.5 方法区(Method Area)

各个线程共享的内存区域,它用于存储储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

限制宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

1.1.6 运行时常量池(Runtime Constant Pool)

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

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

1.1.7 直接内存(Direct Memory)

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

在JDK 1.4中新加入了 NIO (New Input/Output)类,引入了一种基于通道(Channel) 与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行 操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来 回复制数据。
各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError 异常。

2. 对象访问

Object obj = new Object();

假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java栈 的本地变量表中,作为一个reference类型数据出现。
而“new Object()”这部分的语义 将会反映到Java堆中,形成一块存储了 Object类型所有实例数据值(Instance Data,对 象中各个实例字段的数据)的结构化内存。

目前有两种主流访问对象方式

  • 句柄式:Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的 具体地址信息,如图所示。
    优点: 就是reference中存 储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只 会改变句柄中的实例数据指针,而reference本身不需要被修改
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNae43wa-1575962908321)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p35)]

  • 直接指针试
    Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。
    优点: 速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

3.垃圾收集器与内存分配策略

□ 概述
□ 对象已死?
□ 垃圾收集算法
□ 垃圾收集器
□ 内存分配与回收策略

3.1 概述

□ 哪些内存需要回收?
□ 什么时候回收?
□ 如何回收?

程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或线程结束时,内存自然就跟随着回收了。

而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

3.2 对象已死?

垃圾收集器在对堆进行回收前,第一 件事情就是要确定这些对象有哪些还"存活”着,哪些已经“死去”

3.2.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。

缺点不足:
假如对象objA和objB都有字段instance,赋值令 objA.instance = objB 及 objB.instance = objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

3.2.2 根搜索算法

目前主流算法。
通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

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

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。

  • 方法区中的类静态属性引用的对象。

  • 方法区中的常量引用的对象。

  • 本地方法栈中JNI(即一般说的Native方法)的引用的对象。

3.2.3 再谈引用

JDK 1.2之前,Java中的引用 的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始 地址,就称这块内存代表着一个引用。

JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference),软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

  • 强引用

指在程序代码之中普遍存在的,类似“Object obj = new Object。”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用

用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在 系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。

  • 弱引用

用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用 关联的对象只能生存到下一次下圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用

一个对象是否 有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得- 个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收 集器回收时收到一个系统通知。

3.2.4 生存还是死亡?

在根捜索算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于 “缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行 根捜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次 筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize。 方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为’‘没有必要 执行”。

如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。

finalize() 方法只会被系统自动调用一次。

3.2.5 回收方法区

在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%〜95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

判定一个类是否是"无用的类" 需要下面3个条件

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

是否对类进行回收,HotSpot虚拟机提供 了 -Xnoclassgc 参数进行控制,还可以使用-verbose:class 及-XX: +TraceClassLoading、 -XX:+TraceClassUnLoading 査看类的加载和卸载信息。
-verbose: class 和-XX: +TraceClassLoading 可以在 Product 版的虚拟机中使用,但是-XX:+TraceClassUnLoading 参数需要fastdebug版的虚拟机支持。
在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和 OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

3.3 垃圾收集算法

3.3.1 标记-清除算法

分为“标记“和“清除“两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:在这里插入图片描述

缺点:
1.造成内存碎片
2.分配效率较低

3.3.2 复制算法

将可用内存按容量划分为大小相等的两块,毎次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。算法的执行情况如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PoUGo8NK-1575962908322)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p40)]

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90% (80%+10%),只有10%的内存是会被“浪费”的。当然,98%的 对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保 (Handle Promotion)。
内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能 按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一块Survivor空间没有足够的空间存放上一次新生代收集 下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

3.3.3 标记-整理算法

不同于针对新生代的复制算法,针对老年代的特点,创建该算法。

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,从而留下一段连续的内存空间。
这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nv66TIGI-1575962908323)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p41)]

3.3.4 分代收集算法

根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

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

3.4 垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uwBj3Jqy-1575962908323)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p45)]

如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所属的区域则表明他是属于新生代收集器还是老年代收集器。

tenured 终身的; 长期保有的

3.4.1 Serial收集器

1、特点
针对新生代;
采用复制算法;
单线程收集;
进行垃圾收集时,必须暂停其他所有工作线程,直到完成; 即会"Stop The World";
Serial/Serial Old组合收集器运行示意图如下: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2g11CU2w-1575962908323)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p46)]

2、应用场景
依然是HotSpot在Client模式下默认的新生代收集器;
优于其他收集器的地方:
简单高效(与其他收集器的单线程相比);
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的

3、设置参数
“-XX:+UseSerialGC”:添加该参数来显式的使用串行垃圾收集器;

4、Stop TheWorld说明
JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;
会带给用户不良的体验;
从JDK1.3到现在,从Serial收集器-》Parallel收集器-》CMS-》G1,用户线程停顿时间不断缩短,但仍然无法完全消除;

3.4.2 ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。

1、特点
除了多线程外,其余的行为、特点和Serial收集器一样;
如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等;
两个收集器共用了不少代码;
ParNew/Serial Old组合收集器运行示意图如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h3ixfgr7-1575962908324)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p48)]

2、应用场景
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

3、设置参数
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

4、为什么只有ParNew能与CMS收集器配合
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;
因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

注意 从ParNew收集器开始,后面还将会接触到几款并发和并行的收集器。有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,他们可以解释为:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能 会交替执行),用户程序继续运行,而垃圾收集程序运行于另一个CPU上。

3.4.3 Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

1、特点

(A)、有一些特点与ParNew收集器相似

  • 新生代收集器;
  • 采用复制算法
  • 多线程收集

(B)、主要特点是:它的关注点与其他收集器不同
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
关于吞吐量与收集器关注点说明详见本节后面;

2、应用场景
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;
例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;

3、设置参数
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:

(A)、"-XX:MaxGCPauseMillis"
控制最大垃圾收集停顿时间,大于0的毫秒数;
MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;
因为可能导致垃圾收集发生得更频繁;
(B)、"-XX:GCTimeRatio"
设置垃圾收集时间占总时间的比率,0<n<100的整数;
GCTimeRatio相当于设置吞吐量大小;
垃圾收集执行时间占应用程序执行时间的比例的计算方法是:
1 / (1 + n)
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%–1/(1+19);
默认值是1%–1/(1+99),即n=99;

垃圾收集所花费的时间是年轻一代和老年代收集的总时间;

如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;
此外,还有一个值得关注的参数:
(C)、"-XX:+UseAdptiveSizePolicy"
开启这个参数后,就不用手工指定一些细节参数,如:
新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;
JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);
这是一种值得推荐的方式:
(1)、只需设置好内存数据大小(如"-Xmx"设置最大堆);
(2)、然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;
(3)、那些具体细节参数的调节就由JVM自适应完成;
这也是Parallel Scavenge收集器与ParNew收集器一个重要区别;

4、吞吐量与收集器关注点说明

(A)、吞吐量(Throughput)
CPU用于运行用户代码的时间与CPU总消耗时间的比值;
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;

(B)、垃圾收集器期望的目标(关注点)

(1)、停顿时间
停顿时间越短就适合需要与用户交互的程序;
良好的响应速度能提升用户体验;

(2)、吞吐量
高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;
主要适合在后台计算而不需要太多交互的任务;

(3)、覆盖区(Footprint)
在达到前面两个目标的情况下,尽量减少堆的内存空间;
可以获得更好的空间局部性;

3.4.4 Serial Old收集器

Serial Old是 Serial收集器的老年代版本;

1、特点
针对老年代;
采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
单线程收集;

Serial/Serial Old收集器运行示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-voMsyBJO-1575962908324)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p46)]
2、应用场景
主要用于Client模式;
而在Server模式有两大用途:
(A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
(B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);

3.4.5 Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;
JDK1.6中才开始提供;
1、特点
针对老年代;
采用"标记-整理"算法;
多线程收集;
Parallel Scavenge/Parallel Old收集器运行示意图如下:
在这里插入图片描述
2、应用场景
JDK1.6及之后用来代替老年代的Serial Old收集器;
特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;
3、设置参数
“-XX:+UseParallelOldGC”:指定使用Parallel Old收集器;

3.4.6 CMS收集器

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;

1、特点
针对老年代;
基于"标记-清除"算法(不进行压缩操作,产生内存碎片); 以获取最短回收停顿时间为目标;
并发收集、低停顿;
需要更多的内存(看后面的缺点);
是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

2、应用场景
与用户交互较多的场景;
希望系统停顿时间最短,注重服务的响应速度;
以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用;

3、设置参数
“-XX:+UseConcMarkSweepGC”:指定使用CMS收集器;

4、CMS收集器运作过程
比前面几种收集器更复杂,可以分为4个步骤:
(A)、初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象;
速度很快;
但需要"Stop The World";

(B)、并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;

(C)、重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;

(D)、并发清除(CMS concurrent sweep)
回收所有的垃圾对象;
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;
所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;

CMS收集器运行示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkmOVfLz-1575962908325)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p51)]

5、CMS收集器3个明显的缺点
(A)、对CPU资源非常敏感
(B)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败

(1)、浮动垃圾(Floating Garbage)
在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;
这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;
也要可以认为CMS所需要的空间比其他垃圾收集器大;
“-XX:CMSInitiatingOccupancyFraction”:设置CMS预留内存空间;
JDK1.5默认值为68%;
JDK1.6变为大约92%;
(2)、"Concurrent Mode Failure"失败
如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;
这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。

(C)、产生大量内存碎片
由于CMS基于"标记-清除"算法,清除后不进行压缩操作;
产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
解决方法:

(1)、"-XX:+UseCMSCompactAtFullCollection"
使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
但合并整理过程无法并发,停顿时间会变长;
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);

(2)、"-XX:+CMSFullGCsBeforeCompaction"
设置执行多少次不压缩的Full GC后,来一次压缩整理;
为减少合并整理过程的停顿时间;
默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;

总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;
但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;

3.4.7 G1收集器

G1(Garbage-First)是JDK7-u4才推出商用的收集器;

1、特点
(A)、并行与并发
能充分利用多CPU、多核环境下的硬件优势;
可以并行来缩短"Stop The World"停顿时间;
也可以并发让垃圾收集与用户程序同时进行;

(B)、分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
虽然保留分代概念,但Java堆的内存布局有很大差别;
将整个堆划分为多个大小相等的独立区域(Region);
新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;

(C)、结合多种垃圾收集算法,空间整合,不产生碎片
从整体看,是基于标记-整理算法;
从局部(两个Region间)看,是基于复制算法;
这是一种类似火车算法的实现;
都不会产生内存碎片,有利于长时间运行;

(D)、可预测的停顿:低停顿的同时实现高吞吐量
G1除了追求低停顿处,还能建立可预测的停顿时间模型;
可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;

2、应用场景
面向服务端应用,针对具有大内存、多处理器的机器;
最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;

用来替换掉JDK1.5中的CMS收集器;
在下面的情况时,使用G1可能比CMS好:
(1)、超过50%的Java堆被活动数据占用;
(2)、对象分配频率或年代提升频率变化很大;
(3)、GC停顿时间过长(长于0.5至1秒)。

是否一定采用G1呢?也未必:
如果现在采用的收集器没有出现问题,不用急着去选择G1;
如果应用程序追求低停顿,可以尝试选择G1;
是否代替CMS需要实际场景测试才知道。

3、设置参数
XX:+UseG1GC":指定使用G1收集器;
“-XX:InitiatingHeapOccupancyPercent”:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
“-XX:MaxGCPauseMillis”:为G1设置暂停时间目标,默认值为200毫秒;
“-XX:G1HeapRegionSize”:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;

4、为什么G1收集器可以实现可预测的停顿
可以有计划地避免在Java堆的进行全区域的垃圾收集;
G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
这就保证了在有限的时间内可以获取尽可能高的收集效率;

5、一个对象被不同区域引用的问题
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
在其他的分代收集器,也存在这样的问题(而G1更突出):

回收新生代也不得不同时扫描老年代?
这样的话会降低Minor GC的效率;

解决方法:
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;
就可以保证不进行全局扫描,也不会有遗漏。

6、G1收集器运作过程
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
(A)、初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象;
且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
需要"Stop The World",但速度很快;

(B)、并发标记(Concurrent Marking)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
耗时较长,但应用程序也在运行;
并不能保证可以标记出所有的存活对象;

(C)、最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
上一阶段对象的变化记录在线程的Remembered Set Log;
这里把Remembered Set Log合并到Remembered Set中;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;

(D)、筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本;
然后根据用户期望的GC停顿时间来制定回收计划;
最后按计划回收一些价值高的Region中垃圾对象;
回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
可以并发进行,降低停顿时间,并增加吞吐量;

G1收集器运行示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RJInmFFP-1575962908325)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p52)]

3.5 内存分配与回收策略

3.5.1 对象优先在Eden分配

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

Minor GC和Full GC
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具 备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。MajorGC的速度一般会比Minor GC慢10倍以上。

一般来说 Java 堆的内存模型如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z9eCuium-1575962908326)(evernotecid://B95AA1A8-9368-4056-B83F-C32656366620/appyinxiangcom/25469498/ENResource/p53)]

3.5.2 大对象直接进入老年代

所谓大对象就是指,需要大量连续内存空间的Java对象。

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

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

3.5.4 动态对象年龄判定

虚拟机并不总是要求对象的年龄必须达到 MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

3.5.5空间分配担保

4. 类文件结构

Java在诞生时就以一次编写,到处运行特点在各个平台都可以进行运行。其实就是通过不同的编译器(Javac编译器,jrubyc编译器,groovyc编译器等等)将代码编译成规范的class文件,虚拟机只要接收到claas文件而并不关心是class文件时哪一种编译器编译的,这样就到达了(write one,run anywhere)。

细说JVM(1)
细说JVM(2)
Class文件是一组以8bit为基础单位的二进制流,各个数据项目严格按顺序紧凑地排列在class文件中,中间无任何添割符。Class文件格式采用一种类似于C语言结构体的伪结构来储存数据,这种伪结构只有两种数据:无符号数和表。

无符号数属于基本的数据类型,以u1,u2,u3,u4,u8表示1个字节,2个字节,3个字节,4个字节,8个字节,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串。

表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯地以"_info"结尾。表用于描述由层次关系的复合结构的数据,整个class文件本质上就是一张表。

类型名称数量
u4magic(魔数)1
u2minor_version(JDK次版本号)1
u2major_version(JDK主版本号)1
u2constant_pool_count(常量池数量)1
cp_infoconstan_pool(常量表)constant_pool_count-1
u2access_flags(访问标志)1
u2this_class(类引用)1
u2super_class(父类引用)1
u2interfaces_count(接口数量)1
u2interfaces(接口数组)interfaces_count
u2fields_count(字段数量)1
field_infofields(属性表)fields_count
u2methods_count(方法数量)1
method_infomethods(方法表)methods_count
u2attributes_count(属性数量)1
attribute_infoattributes(属性表)attributes_count

4.1 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是用于确 定这个文件是否为一个能被虚拟机接受的Class文件,值为:OxCAFEBABE (咖 啡宝贝?)。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本 号(Minor Version),第7个和第8个字节是主版本号(Major Version)。

4.2 常量池

紧接着主次版本号之后的是常量池入口,常量池是Class文件结构中与其他项目关 联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。

常量池之中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为 final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

常量池中的每一项常量都是一个表,共有11种结构。各不相同的表结构数据,这11 种表都有一个共同的特点,就是表开始的第一位是一个ul类型的标志位(tag,取值为 1至12,缺少标志为2的数据类型),代表当前这个常量属于哪种常量类型,11种常量 类型所代表的具体含义如表所示。
在这里插入图片描述

4.3 访问标志

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用 于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为 public类型;是否定义为abstract类型;如果是类的话,是否被声明为final,等等。具 体的标志位及标志的含义见表
在这里插入图片描述
未完待续。。。。

5. 字节码指令简介

未完待续。。。。

参考《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》-周志明
还有一些博客 如有侵权请联系

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值