美文网首页
JVM体系结构

JVM体系结构

作者: Aimerwhy | 来源:发表于2018-01-03 17:09 被阅读0次

    内存溢出和内存泄漏的区别

    内存溢出 :out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。

    内存泄露 :memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

    Java虚拟机的体系结构和运行时数据区域

     一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:

    Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁

    Java虚拟机的运行时数据区包括了:方法区、Java堆、Java虚拟机栈、PC寄存器、本地方法栈,还有常量池。它们被分为两大类-------线程共享、私有数据区。

    1.线程共享数据区

    包括:Java堆、方法区、常量池。它们会随着虚拟机启动而创建,随着虚拟机退出而销毁。

    堆:

    java堆在虚拟机启动的时候被创建,Java堆主要用来为类实例对象和数组分配内存。Java虚拟机规范并没有规定对象在堆中的形式。对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT(Just In Time)编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了-----可是使用逃逸分析和栈帧存储技术。

            如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

            参考《Java虚拟机规范(第7版)》的描述,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。

    在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old );这也就是JVM采用的“分代收集算法”,简单说,就是针对不同特征的java对象采用不同的策略实施存放和回收,自然所用分配机制和回收算法就不一样。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

            分代收集算法:采用不同算法处理[存放和回收]Java瞬时对象和长久对象。大部分Java对象都是瞬时对象,朝生夕灭,存活很短暂,通常存放在Young新生代,采用复制算法对新生代进行垃圾回收。老年代对象的生命周期一般都比较长,极端情况下会和JVM生命周期保持一致;通常采用标记-压缩算法对老年代进行垃圾回收。

    方法区:

    方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。 

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

    对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory来实现方法区的规划了。

     Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

    方法区可能发生如下异常情况: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常. 

    常量池:

    运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池在方法区中。

            运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。 既然运行时常量池是方法区的一部分自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

            在创建类和接口的运行时常量池时,可能会发生如下异常情况:当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那Java虚拟机将会抛出一个OutOfMemoryError异常。

    线程私有数据区

    包括:PC寄存器、JVM栈、本地方法区。它们是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

    (1)PC寄存器

            PC(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    每个Java虚拟机线程都有自己的PC寄存器。在某个线程被新建时,会获得一个PC寄存器。线程当前执行的方法称为当前方法,PC寄存器用来存放当前方法中当前执行的字节码指令的地址;之所以为每一个线程都分配一个PC寄存器,试想:多线程运行时,某个时间片内只执行一个线程,CPU在不停的切换多个线程,那如何记录具体每一个线程上一次执行到哪个位置了呢,这时候PC寄存器用来存放当前方法中当前执行的字节码指令的地址,就完美解决了,这就是为什么PC寄存器是线程私有数据区的原因。

    如果当前方法是本地方法(Native),那么寄存器存放undefined。寄存器的大小至少应该能够存放一个returnAddress类型的数据或者与平台相关的本地指针的值。

    PC寄存器是惟一一个没有明确规定需要抛出OutOfMemoryError异常的运行时数据区。

    (2)JVM栈

    每个Java虚拟机线程都有自己的Java虚拟机栈。Java虚拟机栈用来存放栈帧,而栈帧主要包括了:局部变量表、操作数栈、动态链接。Java虚拟机栈允许被实现为固定大小或者可动态扩展的内存大小。

            与程序一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期计数器与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

    Java虚拟机使用局部变量表来完成方法调用时的参数传递。局部变量表的长度在编译期已经决定了并存储于类和接口的二进制表示中,一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference 和 returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。

    Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

    每个栈帧中都包含一个指向运行时常量区的引用支持当前方法的动态链接。在Class文件中,方法调用和访问成员变量都是通过符号引用来表示的,动态链接的作用就是将符号引用转化为实际方法的直接引用或者访问变量的运行是内存位置的正确偏移量。 

    总的来说,Java虚拟机栈是用来存放局部变量和过程结果的地方。 

    Java虚拟机栈可能发生如下异常情况: 如果Java虚拟机栈被实现为固定大小内存,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。 

    如果Java虚拟机栈被实现为动态扩展内存大小,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。 

    (3)本地方法区

           本地方法栈用于支持native方法的运行。(native方法,比如用C/C++实现的代码)。

          本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

    堆和GC

     Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象和数组。它的管理是由垃圾回收器(GC)来负责的;不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。Java的堆区,可以是不连续的。

    在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old );这也就是JVM采用的“分代收集算法”,简单说,就是针对不同特征的java对象采用不同的策略实施存放和回收,自然所用分配机制和回收算法就不一样。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。 

    分代收集算法:采用不同算法处理[存放和回收]Java瞬时对象和长久对象。大部分Java对象都是瞬时对象,朝生夕灭,存活很短暂,通常存放在Young新生代,采用复制算法对新生代进行垃圾回收。老年代对象的生命周期一般都比较长,极端情况下会和JVM生命周期保持一致;通常采用标记-压缩算法对老年代进行垃圾回收。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

    堆的内存模型大致为下图:

              从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。

              默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。

              老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。

    默认的----Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )。

    JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。

    因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

    GC堆:

    (1)Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC(新生代回收算法)、Full GC ( 老年代的回收算法,或称为 Major GC )。

    (2)Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。

    新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。

    (3)当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。

    (4)当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳

    ( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。

    但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

    (5)Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。

    (6)现实的生活中,老年代的人通常会比新生代的人 "早死"。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 "死掉" 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。

    (7)另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

    设置 JVM 参数为 -XX:+PrintGCDetails,使得控制台能够显示 GC 相关的日志信息,执行上面代码,下面是其中一次执行的结果。

    jvm 可配置的参数选项可以参考 Oracle 官方网站给出的相关信息:

    下面只列举其中的几个常用和容易掌握的配置选项:

     -Xms 初始堆大小。如:-Xms256m

     -Xmx 最大堆大小。如:-Xmx512m

     -Xmn 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%  

     -Xss JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。

     -XX:NewRatio 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3

     -XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10  

     -XX:PermSize 永久代(方法区)的初始大小

     -XX:MaxPermSize 永久代(方法区)的最大值

     -XX:+PrintGCDetails 打印 GC 具体细节信息

     -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

    JVM运行时数据区域及GC

    直接内存(Direct Memory):

    直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。JDK1.4加的NIO中,ByteBuffer有个方法是allocateDirect(intcapacity) ,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

    JVM栈

    是运行时的单位,而JVM堆是存储的单位。JVM栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;JVM堆解决的是数据存储的问题,即数据怎么放、放在哪儿。在Java中一个线程就会相应有一个线程JVM栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程JVM栈。而JVM堆则是所有线程共享的。JVM栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而JVM堆只负责存储对象信息。

    JVM栈的组成元素——栈帧

    栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入JVM栈。

    局部变量区:局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引是3、4项,取值时,指令只需取索引为3的long值即可。

    操作数栈和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。

    JVM堆

    在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。堆的内存模型大致为:

    从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。本人使用的是 JDK1.7,以下涉及的 JVM 默认值均以该版本为准。默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

    GC收集算法

    在HotSpot中采用可达性分析方法(Reachability Analysis)判断一个对象是否可以被垃圾回收,这个算法的基本思想是通过一系列的称谓”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则说明该对象是不可用的,如下图所示:

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

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

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

    方法区中常量引用的对象

    本地方法栈中JNI(即Native方法)引用的对象

    4种对象引用类型

    在JDK1.2之后,Java对引用的概念进行的扩充,将引用分为强引用(Strong Reference)、软应用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这4中引用强度依次逐渐减弱:

     强引用(Strong Reference):使用new 这个关键字创建对象时被创建的对象就是强引用,如Object object = new Object() 这个object就是一个强引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用对象。 

    软应用(Soft Reference):软引用用来描述那些还有用但并非必须的对象,对于软引用关联的对象,在系统即将发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够内存时,才会抛出内存溢出异常。

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

    虚引用(Phantom Reference):它是最弱的一种引用关系,对象是否有虚引用不会影响其生命周期,虚引用的唯一目的就是能在这个对象被回收时收到一个系统通知。

    finalize()方法

           对象在被回收之前要经历两次标记过程,如果发现对象经可达性分析检测,没有引用关联,它将会被标记并且进行筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或finalize()方法已被虚拟机调用过,虚拟机认为这两种情况均为没有必要执行,对象将被回收,反之先执行finalize()后,收集对象,JVM并不保证finalize()一定成功被执行。

    内存清理算法思想

    标记-清除算法:标记-清除(Mark-Sweep)算法是最基础的算法,就如它的名字一样,算法分为”标记”和”清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。它主要有两个缺点:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清楚后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够连续的内存空间而不得不提前出发另一次垃圾收集动作。

    标记-整理算法:根据老年代的特点,有人提出了另外一种”标记-整理”算法,标记过程仍然与标记-清楚算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。

    分代收集算法:当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分成新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那么就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来进行回收。

    GC收集器

    HotSpot JVM收集器,7种收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

    Serial(串行GC)收集器

    Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

    ParNew(并行GC)收集器

    ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

    Parallel Old(并行GC)收集器

    ParallelScavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

    SerialOld(串行GC)收集器

    Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

    ParallelOld(并行GC)收集器

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

    CMS(并发GC)收集器

    CMS(ConcurrentMark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

    ①.初始标记(CMSinitial mark)

    ②.并发标记(CMSconcurrenr mark)

    ③.重新标记(CMSremark)

    ④.并发清除(CMSconcurrent sweep)

        其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但比并发标记阶段要短。

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

    CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,其主要有三个显著缺点:

    CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。

    CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“ConcurrentMode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full  GC之后,跟着来一次碎片整理过程。

    G1收集器

    G1(GarbageFirst)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。

    垃圾收集器参数总结

    参数描述

    -XX:+UseSerialGCJvm 运行在Client 模式下的默认值,打开此开关后,使用 Serial + Serial Old 的收集器组合进行内存回收

    -XX:+UseParNewGC打开此开关后,使用 ParNew + Serial Old 的收集器进行垃圾回收

    -XX:+UseConcMarkSweepGC使用 ParNew + CMS +  Serial Old 的收集器组合进行内存回收, Serial Old 作为CMS 出现 “Concurrent Mode Failure” 失败后的后备收集器使用。

    -XX:+UseParallelGCJvm 运行在Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge +  Serial Old的收集器组合进行回收

    -XX:+UseParallelOldGC使用 Parallel Scavenge +  Parallel Old 的收集器组合进行回收

    -XX:SurvivorRatio新生代中 Eden 区域与Survivor 区域的容量比值,默认为 8 ,代表Eden:Subrvivor = 8:1

    -XX:PretenureSizeThreshold直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配

    -XX:MaxTenuringThreshold晋升到老年代的对象年龄,每次 Minor GC 之后,年龄就加 1,当超过这个参数的值时进入老年代

    -XX:UseAdaptiveSizePolicy动态调整 java 堆中各个区域的大小以及进入老年代的年龄

    -XX:+HandlePromotionFailure是否允许 新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留

    -XX: ParallelGCThreads设置并行 GC 进行内存回收的线程数

    -XX: GCTimeRatioGC 时间占总时间的比列,默认值为 99 ,即允许1% 的 GC时间,仅在使用Parallel Scavenge 收集器时有效

    -XX: MaxGCPauseMillis设置 GC 的最大停顿时间,在 Parallel Scavenge 收集器下有效

    -XX:CMSInitiatingOccupancyFraction设置 CMS 收集器在老年代空间被使用多少后出发垃圾收集,默认值为 68% ,仅在CMS 收集器时有效, -XX:CMSInitiatingOccupancyFraction=70

    -XX:+UseCMSCompactAtFullCollection由于 CMS 收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在 CMS 收集器时有效

    -XX:+CMSFullGCBeforeCompaction设置 CMS 收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与 UseCMSCompactAtFullCollection 参数一起使用

    -XX:+UseFastAccessorMethods原始类型优化

    -XX:+DisableExplicitGC是否关闭手动 System.gc

    -XX:+CMSParallelRemarkEnabled降低标记停顿

    -XX:LargePageSizeInBytes内存页的大小不可设置过大,会影响 Perm 的大小,-XX:LargePageSizeInBytes=128m

     

    Client、Server模式默认GC新生代GC方式老年代和持久代GC方式

    ClientSerial 串行GCSerial Old 串行GC

    ServerParallel Scavenge  并行回收GCParallel Old 并行GC

     Sun/oracle JDK GC组合方式新生代GC方式老年代和持久代GC方式

    -XX:+UseSerialGCSerial 串行GCSerial Old 串行GC

    -XX:+UseParallelGCParallel Scavenge  并行回收GCSerial Old  并行GC

    -XX:+UseConcMarkSweepGCParNew 并行GCCMS 并发GC

    当出现“Concurrent Mode Failure”时

    采用Serial Old 串行GC

    -XX:+UseParNewGCParNew 并行GCSerial Old 串行GC

    -XX:+UseParallelOldGCParallel Scavenge  并行回收GCParallel Old 并行GC

    -XX:+UseConcMarkSweepGC

    -XX:+UseParNewGC

    Serial 串行GCCMS 并发GC

    当出现“Concurrent Mode Failure”时

    采用Serial Old 串行GC

    相关文章

      网友评论

          本文标题:JVM体系结构

          本文链接:https://www.haomeiwen.com/subject/zzohnxtx.html