柴少鹏的官方网站 技术在分享中进步,水平在学习中升华

JVM学习了解

#首先就是此篇略过,本人本身并非java开发者,对java这块也不是太懂,就是工作中老是接触java的东西,又学习了架构师里面的jvm调优,所以这里只是网上收集记录一下给自己做个笔记的作用。可略过写的没什么深度。

一、JVM介绍

1.1 什么是JVM

       JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
       Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

1.2 JVM的内存结构

图片.png

#JVM是按照运行时数据的存储结构来划分内存结构的,JVM在运行java程序时,将它们划分为几种不同格式的数据,分别存储在不同的区域,这些数据统一称为运行时数据。运行时数据包括Java程序本身的数据信息和JVM运行java需要的额外数据信息。

图片.png

#此图来源于http://images2015.cnblogs.com/blog/331425/201606/331425-20160623115838891-809895495.png  ,文章也得也非常棒

      JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

1.3 JVM运行时数据区

简单介绍:      

       JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途、创建和销毁的时间。

       java虚拟机定义了若干种程序运行时使用到的运行时数据区。1.有一些是  随虚拟机的启动而创建,随虚拟机的退出而销毁。2.第二种则是与线程一一对应,随线程的开始和结束而创建和销毁。
       java虚拟机所管理的内存将会包括以下几个运行时数据区域。

图片.png

#此图摘自:http://chenzhou123520.iteye.com/blog/1585224

程序计数器:线程私有
Java虚拟机栈:线程私有
本地方法栈:线程私有
Java堆:线程公用
方法区:线程公用

图片.png

#此图摘自:http://blog.csdn.net/loveslmy/article/details/46820929

程序计数器(Program Counter Register):

       PC寄存器也叫程序计数器(Program Counter Register),它是一块较小的内存空间,它的作用是记录当先线程所执行的字节码的信号指示器。每一条JVM线程都有自己的PC寄存器,各条线程之间互不影响,独立存储,这类内存区域被称为“线程私有”内存在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址。如果该方法是native,那PC寄存器的值是undefined。此内存区域是在Java虚拟机规范中唯一没有规定OutOfMemoryError情况出现的区域。     

       这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址,对于JVM中的程序计数器可以看做是当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器(这很好理解,每个线程都有在执行任务,如果线程切换后要能保证能恢复到正确的位置),重要的一点——程序计数器,这是JVM规范中唯一一个没有规定会导致OutOfMemory(内存泄露,下文简称OOM)的区域。换句话上图中的其余4个区域,都有可能导致OOM。

直接内存(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异常。

Java堆(Heap)

       对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显示地被销毁。

      Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

      在JVM中,堆(heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。Java堆的容量可以是固定大小,也可以随着需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。

      根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
     如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

Java虚拟机栈/JVM栈(JVM Stacks) :

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

      JVM stack 可以被实现成固定大小,也可以根据计算动态扩展。如果采用固定大小的JVM stack设计,那么每一条线程的JVM Stack容量应该在线程创建时独立地选定。JVM实现应该提供调节JVM Stack初始容量的手段。如果采用动态扩展和收缩的JVM Stack方式,应该提供调节最大、最小容量的手段。
      JVM Stack 异常情况:
      StackOverflowError:当线程请求分配的栈容量超过JVM允许的最大容量时抛出
      OutOfMemoryError:如果JVM Stack可以动态扩展,但是在尝试扩展时无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈时抛出,通过-Xss参数可设置栈大小。

方法区 (Method Area or Permanent Generation):

       方法区也叫永久代。方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来,,通过-XX:permSize和-XX:MaxPermSize设置该空间大小。当方法区无法满足内存分配需求时就会抛OutOfMemoryError。

      方法区在虚拟机启动的时候创建。方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。

      Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“效果”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

     从JDK7开始永久代的移除工作,贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap。但永久代仍然存在于JDK7,并没有完全的移除:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。

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

本地方法栈(Native Method Stack):

       本地方法栈与虚拟机栈作用相似,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范对于本地方法栈中方法使用的语言,使用方式和数据结构没有强制规定,因此具体的虚拟机可以自由实现它,甚至有的虚拟机(比如HotSpot)直接把二者合二为一。

      Java虚拟机可能会使用到传统的栈来支持native方法(使用Java语言以外的其它语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack)。如果JVM不支持native方法,也不依赖与传统方法栈的话,可以无需支持本地方法栈。如果支持本地方法栈,则这个栈一般会在线程创建的时候按线程分配。
     异常情况:
    StackOverflowError:如果线程请求分配的栈容量超过本地方法栈允许的最大容量时抛出。
    OutOfMemoryError:如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,Java虚拟机将会抛出一个            OutOfMemoryError异常。

1.4 JVM栈

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

     在介绍JVM栈之前,我先了解一下 栈帧 概念。栈帧:一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。栈帧内存放者方法中的局部变量,操作数栈等数据。Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。下面我们来看一个Java栈图。

图片.png   

#此图来源于http://blog.csdn.net/zhangqilugrubby/article/details/59110906  #下面对此图的介绍内容也来源于这篇博客。

        由上图可以看出,Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。

       栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,执行完毕后,先弹出 F2栈帧,再弹出 F1 栈帧,遵循“先进后出”原则。光说比较枯燥,我们看一个图来理解一下 Java栈,如下图所示:

图片.png

#此图来自:https://www.cnblogs.com/wangjzh/p/5258254.html

        栈内存的大小可以有两种设置,固定值和根据线程需要动态增长。 在JVM栈这个数据区可能会发生抛出两种错误。
1. StackOverflowError 出现在栈内存设置成固定值的时候,当程序执行需要的栈内存超过设定的固定值会抛出这个错误。
2. OutOfMemoryError 出现在栈内存设置成动态增长的时候,当JVM尝试申请的内存大小超过了其可用内存时会抛出这个错误。
       总结:
1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。对象都存放在堆区中。
2. 每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
3. 栈分为3个部分:基本类型变量,执行环境上下文,操作指令区(存放操作指令).
4. 在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
5. 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

1.5 JVM堆

     Java堆是被所有线程共享的一块内存区域,所有对象和数组都在堆上进行内存分配。为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代、老年代和永久代(1.8中无永久代,使用metaspace实现)三块区域。在JVM中堆空间划分如下图所示:

图片.png

#此图来源于:http://www.codeceo.com/article/jvm-memory-stack.html

图片.png

#此图来源于: https://www.cnblogs.com/SaraMoring/p/5713732.html

#从上面两张图可以看出:

1.JVM中共享数据空间可以分成三个大区,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分为新生代和老年代
2.新生代可以划分为三个区,Eden区(存放新生对象),两个幸存区(From Survivor和To Survivor)(存放每次垃圾回收后存活的对象)
3.永久代管理class文件、静态对象、属性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information )
4.JVM垃圾回收机制采用“分代收集”:新生代采用复制算法,老年代采用标记清理算法。

在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例:

1.JVM运行时堆的大小
  -Xms堆的最小值
  -Xmx堆空间的最大值
  
2.新生代堆空间大小调整
  -XX:NewSize新生代的最小值
  -XX:MaxNewSize新生代的最大值
  -XX:NewRatio设置新生代与老年代在堆空间的大小
  -XX:SurvivorRatio新生代中Eden所占区域的大小

3.永久代大小调整
  -XX:MaxPermSize4.其他
   -XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收

#作为操作系统进程,Java 运行时面临着与其他进程完全相同的内存限制:操作系统架构提供的可寻址地址空间和用户空间。地址空间被划分为用户空间和内核空间。内核是主要的操作系统程序和C运行时,包含用于连接计算机硬件、调度程序以及提供联网和虚拟内存等服务的逻辑和基于C的进程(JVM)。除去内核空间就是用户空间,用户空间才是 Java 进程实际运行时使用的内存。

#下图为一个32 位 Java 进程的内存布局(加深下理解):

图片.png

可寻址的地址空间总共有 4GB,OS 和 C 运行时大约占用了其中的 1GB,Java 堆占用了将近 2GB,本机堆占用了其他部分。请注意,JVM 本身也要占用内存,就像 OS 内核和 C 运行时一样。
注意:
1. 上文提到的可寻址空间即指最大地址空间。
2. 对于2GB的用户空间,理论上Java堆内存最大为1.75G,但一旦Java线程的堆达到1.75G,那么就会出现本地堆的Out-Of-Memory错误,所以实际上Java堆的最大可使用内存为1.5G。

通过图来展示通过参数来控制各区域的内存大小:

图片.png

#此图来自:https://www.cnblogs.com/ityouknow/p/5610232.html

#控制参数
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小。
#没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。老年代空间大小=堆空间大小-年轻代大空间大小

 1.6 复制(Copying)算法:

       在上面的配置中,老年代所占空间的大小是由-XX:SurvivorRatio这个参数进行配置的,看完了上面的JVM堆空间分配图,可能会奇怪,为啥新生代空间要划分为三个区Eden及两个Survivor区?有何用意?为什么要这么分?要理解这个问题,就得理解一下JVM的垃圾收集机制(复制算法也叫copy算法),步骤如下:

复制(Copying)算法:

将内存平均分成A、B两块,算法过程:
1. 新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象对象复制到B块。
2. 清理A块所有对象。
3. 新生对象被分配的B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象对象复制到A块。
4. 清理B块所有对象。
5. goto 1。
优点:简单高效。缺点:内存代价高,有效内存为占用内存的一半。
图解说明如下所示:(图中后观是一个循环过程)

图片.png

对复制算法进一步优化:使用Eden/S0/S1三个分区:

平均分成A/B块太浪费内存,采用Eden/S0/S1三个区更合理,空间比例为Eden:S0:S1==8:1:1,有效内存(即可分配新生对象的内存)是总内存的9/10。
算法过程:
1. Eden+S0可分配新生对象;
2. 对Eden+S0进行垃圾收集,存活对象复制到S1。清理Eden+S0。一次新生代GC结束。
3. Eden+S1可分配新生对象;
4. 对Eden+S1进行垃圾收集,存活对象复制到S0。清理Eden+S1。二次新生代GC结束。
5. goto 1。
默认Eden:S0:S1=8:1:1,因此,新生代中可以使用的内存空间大小占用新生代的9/10,那么有人就会问,为什么不直接分成两个区,一个区占9/10,另一个区占1/10,这样做的原因大概有以下几种
1.S0与S1的区间明显较小,有效新生代空间为Eden+S0/S1,因此有效空间就大,增加了内存使用率
2.有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其分到老年代中,设想一下,如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放,你可能会说,在对象里加一个计数器记录经过的GC次数,或者存在一张映射表记录对象和GC次数的关系,是的,可以,但是这样的话,会扫描整个新生代中的对象, 有了S0/S1我们就可以只扫描S0/S1区了。

1.7 为什么要把JVM堆和JVM栈区分出来呢?JVM栈中不是也可以存储数据吗?

第一,从软件设计的角度看,JVM栈代表了处理逻辑,而JVM堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,JVM堆与JVM栈的分离,使得JVM堆中的内容可以被多个JVM栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,JVM堆中的共享常量和缓存可以被所有JVM栈访问,节省了空间。
第三,JVM栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于JVM栈只能向上增长,因此就会限制住JVM栈存储内容的能力。而JVM堆不同,JVM堆中的对象是可以根据需要动态增长的,因此JVM栈和JVM堆的拆分,使得动态增长成为可能,相应JVM栈中只需记录JVM堆中的一个地址即可。
第四,JVM堆和JVM栈的完美结合就是面向对象的一个实例。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在JVM堆中;而对象的行为(方法),就是运行逻辑,放在JVM栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
JVM栈是程序运行的根本,JVM堆是为JVM栈进行数据存储的服务,简单讲JVM堆就是一块共享的内存。不过,正是因为JVM堆和JVM栈的分离的思想,才使得Java的垃圾回收成为可能。
JVM栈的组成元素——栈帧
栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入JVM栈。
局部变量区:局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引是3、4项,取值时,指令只需取索引为3的long值即可。

1.8 JVM内存分配

堆栈的内存分配:

栈内存分配:

保存参数、局部变量、中间计算过程和其他数据。退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。
栈的优点:存取速度比堆快,仅次于寄存器,栈数据可以共享。
栈的缺点:存在栈中的数据大小、生存期是在编译时就确定的,导致其缺乏灵活性。

堆内存分配:

堆的优点:动态地分配内存大小,生存期不必事先告诉编译器,它是运行期动态分配的,垃圾回收器会自动收走不再使用的空间区域。
堆的缺点:运行时动态分配内存,在分配和销毁时都要占用时间,因此堆的效率较低。

内存分配的几个原则:

      对象的内存分配,就是在Java堆上分配的,对象主要分配在堆中的新生代的Eden区。
对象优先在Eden分配:大多数情况下,对于一个新的对象将会首先分配在新生代的Eden区,只有Eden区没有足够的空间进行分配的时候,虚拟机发起一次Minor GC,可以使用-verbose:gc -XX:+PrintGCDetails来打印内存分配的状态。当分配的对象无法容纳在Eden区的时候,首先会将Eden中存活的对象复制到另一个Survivor中,如果Survivor无法容纳这些存活的对象,则只有通过分配担保机制将这些存活对象提前移动到老年代中,然后将要分配的对象分配到Eden区中。什么叫Minor GC呢?Minor GC是指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,当然了,其回收速度肯定也是比较快的,与之对应,还有个Full GC或者称为Major GC,是指老年代中的GC,经常会伴随一次Minor GC,Major GC速度一般会比Minor GC速度慢10倍以上!

大对象直接进入老年代:所谓的大对象,是指占用大量连续内存空间的Java对象。最经典的大对象就是那种很长的字符串和数组。可以设置-XX:PretenureSizeThreshold参数,当大于这个值的对象直接会在老年代中分配,避免了在Eden区和两个Survivor之间大量的拷贝。大对象对于虚拟机来说是个坏消息~我们写程序时,尽量要避免出现一群朝生夕死的大对象。经常出现大对象容易导致内存还有不少空间时就得提前触发垃圾收集以获取足够的空间来存放大对象。

长期存活的对象将进入老年代:虚拟机为每个对象定义了对象年龄,在Eden区出生,并经过一次Minor GC后仍然存活,并且能被To Suvivor容纳,移动到To Suvivor区后,年龄设置为1。以后每经历一次Minor GC就将年龄加1,当它的年龄达到一个阀值(默认15,也可以更改-XX:MaxTenurinigThreshold来设置),就会被晋级到老年代中。。JVM采用分代收集思想来管理内存,就要去区分哪些是年轻的对象,哪些是老年的对象。

动态对象年龄判定:为了更好地适应不同程序内存情况,不一定非得达到年龄阀值才会进入老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象就会直接进入老年代,无需等MaxTenuringThreshold的阀值年龄。这句话可能有点绕,不太好理解,我来再解释一下,就是说,假设Survivor的空间大小为max,年龄为y的对象总共有n个,如果y*n>max/2,那么所有年龄大于y的对象全部进入到老年代。

空间分配担保:在发生Minor GC时候,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则直接对于老年代进行一个Full GC。如果小于,则查看HandlePermotionFailure设置是否允许进行担保失败,如果允许,则在新生代进行Minor GC;如果不允许,则在老年代进行Full 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倍以上。Major GC会触发整个heap的回收,包括回收young generation。

知识补充:

VM内存区域总体分两类,heap区 和 非heap 区 :

heap区   #堆区分为Young Gen(新生代),Tenured Gen(老年代-养老区)。其中新生代又分为Eden Space(伊甸园)、Survivor Space(幸存者区)。 
非heap区 #Code Cache(代码缓存区)、Perm Gen(永久代)、Jvm Stack(java虚拟机栈)、Local Method Statck(本地方法栈)。

#堆内存和非堆内存:http://www.importnew.com/27645.html

为什么要区分新生代和老生代?

堆中区分的新生代和老年代是为了垃圾回收,新生代中的对象存活期一般不长,而老年代中的对象存活期较长,所以当垃圾回收器回收内存时,
新生代中垃圾回收效果较好,会回收大量的内存,而老年代中回收效果较差,内存回收不会太多。

不同代采用的算法区别?

基于以上特性,新生代中一般采用复制算法,因为存活下来的对象是少数,所需要复制的对象少,而老年代对象存活多,不适合采用复制算法,一般是标记整理和标记清除算法。 
因为复制算法需要留出一块单独的内存空间来以备垃圾回收时复制对象使用,所以将新生代分为eden区和两个survivor区,每次使用eden和一个survivor区,另一个survivor作为备用的对象复制内存区。

对象何时进入老年代:

(1)当对象首次创建时, 会放在新生代的eden区, 若没有GC的介入,会一直在eden区, GC后,是可能进入survivor区或者年老代 
(2)当对象年龄达到一定的大小 ,就会离开年轻代, 进入老年代, 对象进入老年代的事件称为晋升, 而对象的年龄是由GC的次数决定的, 每一次GC,若对象没有被回收, 则对象的年龄就会加1, 可以使用以下参数来控制新生代对象的最大年龄: 
  -XX:MaxTenuringThreshold=n  假设值为n , 则新生代的对象最多经历n次GC, 就能晋升到老年代, 但这个必不是晋升的必要条件 
  -XX:TargetSurvivorRatio=n  用于设置Survivor区的目标使用率,即当survivor区GC后使用率超过这个值, 就可能会使用较小的年龄作为晋升年龄 
(3)除年龄外, 对象体积也会影响对象的晋升的, 若对象体积太大, 新生代无法容纳这个对象, 则这个对象可能就会直接晋升至老年代, 可通过以下参数使用对象直接晋升至老年代的阈值, 单位是byte 
  -XX:PretenureSizeThreshold  即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配, 此参数只对串行回收器以及ParNew回收有效, 而对ParallelGC回收器无效,

1.9 JVM堆配置参数

#首先更多的参数配置可参考官网:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

大面的配置:

1. -Xms #初始堆大小,默认物理内存的1/64(<1GB)
2. -Xmx #最大堆大小,默认物理内存的1/4(<1GB),实际中建议不大于4GB
3. 一般建议设置-Xms = -Xmx,好处是避免每次在gc后,调整堆的大小,减少系统内存分配开销
4. 整个堆的大小=年轻代大小+年老代大小+持久代大小

jvm新生代(young generation):

1. 新生代=1个eden区+2个Survivor区
2. -Xmm 年轻代大小(1.4 or lator)  -XX:NewSize,-XX:MaxNewSize(设置年轻代大小(for 1.3/1.4))默认值大小为整个堆的3/8.
3. -XX:NewRatio  年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
4. -XX:SurvivorRatio  Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10.
5. 用来存放JVM刚分配的Java对象。

java老年代(tenured generation):

1. 老年代=整个堆-年轻代大小-持久代大小
2.  年轻代中经过垃圾回收没有回收掉的对象被复制到年老代
3.  老年代存储对象比年轻代年龄大的多,而且不乏大对象
4.  新建的对象也有可能直接进入老年代。第一种是大对象可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。第二种是大的数组对象,且数组中无引用外部对象。
5.  老年代大小无配置参数

java持久代(pem generation):

1. 持久化=整个堆-年轻代大小-老年代大小
2. -XX:PermSize -XX:MaxPermSize  #设置持久代的大小,一般情况推荐把-XX:PermSize设置成XX:MaxPermSize的值为相同的值,因为永久代大小的调整也会导致堆内存需要出发fgc。
3. 存放Class、Method元信息,其大小与项目的规模、类、方法的数量有关。一般设置为128M就足够,设置原则是预留30%的空间。
4. 永久代的回收方式:
   4.1 常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收
   4.2 对于无用的类进行回收,必须保证3点:类的所有实例都已经被回收、加载类的ClassLoader已经被回收、类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

常用的配置参数:

# java -Xmx128m -Xms128m -Xmn64m -Xss1m   #设置例子
-Xms    #初始堆大小也是堆的最小值。如:-Xms256m    
-Xmx    #最大堆大小。如:-Xmx512m    
-Xmn    #新生代大小。通常为Xmx的1/3或1/4。新生代=Eden+2个Survivor 空间。实际可用空间为=Eden+1个Survivor,即90%.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。    
-Xss    #JDK1.5+ 每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。一般来说如果栈不是很深的话,1M是绝对够用了的。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。    
# java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0   #设置例子
-XX:NewRatio    #设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代),设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5,如–XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3。    
-XX:SurvivorRatio    #新生代中Eden与 Survivor的比值。默认值为8。即Eden占新生代空间的8/10,另外两个Survivor 各占1/10。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6    
-XX:PermSize    #永久代(方法区)的初始大小    
-XX:MaxPermSize    #永久代(方法区)的最大值    
-XX:+PrintGCDetails    #打印 GC 信息    
-XX:+HeapDumpOnOutOfMemoryError    #让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用
-XX:MaxTenuringThreshold=0   #设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。 对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

注意:PermSize永久代的概念在jdk1.8中已经不存在了,取而代之的是metaspace元空间,当认为执行永久代的初始大小以及最大值是jvm会给出如此下提示:
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=30m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=30m; support was removed in 8.0

1.10 GC堆

GC堆介绍:

       Java中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
       Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
       新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

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

图片.png

       从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –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 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。 JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

GC日志:

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

图片.png

#此图来自:http://blog.csdn.net/ls5718/article/details/51777195    #这里的文字介绍也是来自此篇博客

       Full GC 信息与 Minor GC 的信息是相似的,这里就不一个一个的画出来了。从 Full GC 信息可知,新生代可用的内存大小约为 18M,则新生代实际分配得到的内存空间约为 20M(为什么是 20M? 请继续看下面…)。老年代分得的内存大小约为 42M,堆的可用内存的大小约为 60M。可以计算出: 18432K ( 新生代可用空间 ) + 42112K ( 老年代空间 ) = 60544K ( 堆的可用空间 )新生代约占堆大小的 1/3,老年代约占堆大小的 2/3。也可以看出,GC 对新生代的回收比较乐观,而对老年代以及方法区的回收并不明显或者说不及新生代。并且在这里 Full GC 耗时是 Minor GC 的 22.89 倍。

1.11 JVM内存溢出的问题

图片.png

#此图来自:http://blog.csdn.net/ns_code/article/details/17565503

a) 程序计数器(Program Counter Register)

每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。

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

在Java虚拟机规范中,对这个区域规定了两种异常情况:
1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是StackOverflowError异常,而不会得到OutOfMemoryError异常。而在多线程环境下,则会抛出OutOfMemoryError异常。

c)堆Java Heap

Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。

d)方法区域,又被称为“永久代”  
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

二、JVM垃圾回收

2.1 jvm垃圾收集算法

垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了。

判断对象是否存活一般有两种方式:

引用计数算法:

       引用计数法是最经典的一种垃圾回收算法。其实现很简单,对于一个A对象,只要有任何一个对象引用了A,则A的引用计算器就加1,当引用失效时,引用计数器减1.只要A的引用计数器值为0,则对象A就不可能再被使用。
虽然其思想实现都很简单(为每一个对象配备一个整型的计数器),但是该算法却存在两个严重的问题:

1)  无法处理循环引用的问题,因此在Java的垃圾回收器中,没有使用该算法。
2)  引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

      一个简单的循环引用问题描述:
对象A和对象B,对象A中含有对象B的引用,对象B中含有对象A的引用。此时对象A和B的引用计数器都不为0,但是系统中却不存在任何第三个对象引用A和B。也就是说A和B是应该被回收的垃圾对象,但由于垃圾对象间的互相引用使得垃圾回收器无法识别,从而引起内存泄漏(由于某种原因不能回收垃圾对象占用的内存空间)。如下图:不可达对象出现循环引用,它的引用计数器不为0,

图片.png

#此图来自:http://blog.csdn.net/wen7280/article/details/54428387   #这里的大部分文字介绍也来自于此篇博客

注意:由于引用计数器算法存在循环引用以及性能的问题,java虚拟机并未使用此算法作为垃圾回收算法。

【可达对象】:通过根对象的进行引用搜索,最终可以到达的对象。
【不可达对象】:通过根对象进行引用搜索,最终没有被引用到的对象。

可达性分析(Reachability Analysis)/根搜索算法:

从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。在Java语言中,GC  Roots包括:

   虚拟机栈中引用的对象。
   方法区中类静态属性实体引用的对象。
   方法区中常量引用的对象。
   本地方法栈中JNI引用的对象。

2.2 jvm垃圾回收算法

图片.png

我们总结一下JVM中的垃圾收集器:

图片.png

#上面两张图图来自于:https://www.cnblogs.com/zhouyuqin/p/5164060.html

从不同角度分析垃圾收集器,可以将其分为不同的类型。

1. 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。串行垃圾回收器一次只使用一个线程进行垃圾回收;并行垃圾回收器一次将开启多个线程同时进行垃圾回收。在并行能力较强的 CPU 上,使用并行垃圾回收器可以缩短 GC 的停顿时间。

2. 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间;独占式垃圾回收器 (Stop the world) 一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。

3. 按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器。压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片;非压缩式的垃圾回收器不进行这步操作。
4. 按工作的内存区间,又可分为新生代垃圾回收器和老年代垃圾回收器。

#这里的介绍内容来自:http://www.importnew.com/15802.html

复制算法(Copying):

       复制算法是为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。

     复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法相当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。此算法用于新生代内存回收,从E区回收到S0或者S1。

优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:算法的代价是将内存缩小为了原来的一半,未免太高了一点。需要一块儿空的内存空间,需要复制移动对象。
原理:从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉
适用场景:存活对象较少的情况下比较高效,扫描了整个空间一次(标记存活对象并复制移动),适用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少

图片.png

#此图来源于:http://blog.csdn.net/u011080472/article/details/51324103  #问题内容也来源这个博客

       现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
      当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。
      当然,90%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时(例如,存活的对象需要的空间大于剩余一块Survivor的空间),需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

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

       图片.png

      标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清楚算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清楚算法直接回收不存活的对象,因此会造成内存碎片。

        算法的执行过程与名字一样,先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法有两个问题:

标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。显然遍历的效率是很低的
会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。

图片.png

缺点:容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收。扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)
适用场景:存活对象较多的情况下比较高效,适用于年老代(即旧生代)

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

        也叫标记-压缩算法。结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
       与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,二是让所有的存活对象都向一端移动,然后清理掉边界外的内存。该方法主要用于老年代。如图:

图片.png

#此图来自:https://www.cnblogs.com/cielosun/p/6674431.html

分代收集算法:

目前商用虚拟机都使用“分代收集算法”,所谓分代就是根据对象的生命周期把内存分为几块,一般把Java堆中分为新生代和老年代,这样就可以根据对象的“年龄”选择合适的垃圾回收算法。

新生代:“朝生夕死”,存活率低,使用复制算法。
老年代:存活率较高,使用“标记-清除”算法或者“标记-整理”算法。

分代算法思想:将内存空间根据对象的特点不同进行划分,选择合适的垃圾回收算法,以提高垃圾回收的效率。

图片.png

#此图来自:http://blog.csdn.net/wen7280/article/details/54428387   #这里的内容也来自于此篇博客

      通常,java虚拟机会将所有的新建对象都放入称为新生代的内存空间。
      新生代的特点是:对象朝生夕灭,大约90%的对象会很快回收,因此,新生代比较适合使用复制算法。
当一个对象经过几次垃圾回收后依然存活,对象就会放入老年代的内存空间,在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以存活的,因此,认为这些对象在一段时间内,甚至在程序的整个生命周期将是常驻内存的。
老年代的存活率是很高的,如果依然使用复制算法回收老年代,将需要复制大量的对象。这种做法是不可取的,根据分代的思想,对老年代的回收使用标记清除或者标记压缩算法可以提高垃圾回收效率。
       注意:分代的思想被现有的虚拟机广泛使用,几乎所有的垃圾回收器都区分新生代和老年代。
       对于新生代和老年代来说,通常新生代回收的频率很高,但是每次回收的时间都很短,而老年代回收的频率比较低,但是被消耗很多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表的数据结构,卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用,
        这样一来,新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有当卡表的标记为1时,才需要扫描给定区域的老年代对象,而卡表为0的所在区域的老年代对象,一定不含有新生代对象的引用。
        如下图表示:
       卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,只有卡表为1的区域才有对象包含新生代对象的引用,因此在新生代GC时,只需要扫面卡表为1所在的老年代空间,使用这种方式,可以大大加快新生代的回收速度。

图片.png

分区算法:

      算法思想:分区算法将整个堆空间划分为连续的不同小区间,
      如图所示:每一个小区间都独立使用,独立回收。算法优点是:可以控制一次回收多少个小区间。

      通常,相同的条件下,堆空间越大,一次GC所需的时间就越长,从而产生的停顿时间就越长。为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一个GC的停顿时间。

图片.png

2.3 JVM常见的垃圾回收器

图片.png

#上图是HptSpot里的收集器,中间的横线表示分代,有连线表示可以组合使用。

如何去评价一个垃圾回收器好坏?

  1. 吞吐量:指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。系统总运行时间=应用程序耗时+GC 耗时。如果系统运行了 100分钟,GC 耗时 1分钟,那么系统的吞吐量就是 (100-1)/100=99%。从吞吐量这个层面可以大致去评估一个垃圾回收器的效率。

  2. 停顿时间:指在进行垃圾回收的时候,中断系统应用线程的时间。当然我们是希望不中断系统的应用线程,让JVM垃圾回收和应用线程同时进行,但是这种情况往往会增大垃圾回收时间,同时也会导致系统的应用线程处理速度变慢,也会导致吞吐量会降低。总之,停顿时间越短,越是我们希望看到的。

  3. 垃圾回收频率:指垃圾回收器多长时间会运行一次。一般来说,对于固定的应用而言,垃圾回收器的频率应该是越低越好。通常增大堆空间可以有效降低垃圾回收发生的频率,但是可能会增加回收产生的停顿时间。

按照分代收集的方式,我们把垃圾回收器做如下的划分:

新生代收集器有:Serial 、ParNew、Parallel Scavenge
老年代收集器有:CMS、Serial Old、Paralled Old
新生代和老年代都可以使用的:G1

串行(Serial)回收:

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

      Serial收集器是Java虚拟机中最基本、历史最悠久的收集器。在JDK1.3之前是Java虚拟机新生代收集器的唯一选择。目前也是ClientVM下ServerVM 4核4GB以下机器默认垃圾回收器。Serial收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束。使用算法:复制算法。Serial收集器虽然是最老的,但是它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。

图片.png

#此图来自于:http://blog.csdn.net/lghuntfor/article/details/51052737   #下面的介绍文字也来自于这里。

(1)特点: 
  –它仅仅使用单线程进行垃圾回收 
  –它是独占式的垃圾回收 
  –进行垃圾回收时, Java应用程序中的线程都需要暂停(Stop-The-World) 
  –使用复制算法 
  –适合CPU等硬件不是很好的场合 
(2)设置参数: 
  -XX:+UseSerialGC 指定新生使用新生代串行收集器和老年代串行收集器, 当以client模式运行时, 它是默认的垃圾收集器

SerialOld(-XX:+UseSerialGC):

      SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。如果在Server模式下,它主要还有两大用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。使用算法:标记 - 整理算法

(1)特点: 
  –同新生代串行回收器一样, 单线程, 独占式的垃圾回收器 
  –因为内存比较大的原因,通常老年代垃圾回收比新生代回收要更长时间, 所以可能会使应用程序停顿较长时间 
(2)设置参数: 
  -XX:+UseSerialGC 新生代, 老年代都使用串行回收器,用这个参数来开启
  -XX:+UseParNeGC 新生代使用ParNew回收器, 老年代使用串行回收器 
  -XX:+UseParallelGC 新生代使用ParallelGC回收器, 老年代使用串行回收器

并行(ParNew)回收:

      ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。收集是指多个GC线程并行工作,但此时用户线程是暂停的。使用算法:复制算法。ParNew是许多运行在Server模式下的JVM首选的新生代收集器。但是在单CPU的情况下,它的效率远远低于Serial收集器,所以一定要注意使用场景。

图片.png

#此图来自于:http://blog.csdn.net/zhuangyalei/article/details/51585852

新生代ParNew回收器:

(1)特点: 
  –将串行回收多线程化, 
  –使用复制算法 
  –垃圾回收时, 应用程序仍会暂停, 只不过由于是多线程回收, 在多核CPU上,回收效率会高于串行回收器, 反之在单核CPU, 效率会不如串行回收器 
(2)设置参数: 
  -XX:+UseParNewGC    开启,新生代使用ParNew回收器, 老年代使用串行回收器
  -XX:+UseConcMarkSweepGC 新生代使用ParNew回收器, 老年代使用CMS回收器 
  -XX:ParallelGCThreads=n 指回ParNew回收器工作时的线程数量,cpu核数小于8时,其值等于cpu数量,高于8时,可以使用公式(3+((5*CPU_count)/8))。默认最好与CPU数理相当,避免过多的线程数影响垃圾收集性能。

ParallelScavenge(-XX:+UseParallelGC):

       ParallelScavenge又被称为吞吐量优先收集器,和ParNew 收集器类似,是一个新生代收集器。使用算法:复制算法。
       ParallelScavenge收集器的目标是达到一个可控件的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。如果虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,用户代码99分钟,那么吞吐量就是99% ,这种收集器能最高效率的利用CPU,适合运行后台运算。

新生代ParallelGC回收器:

(1)特点: 
  –同ParNew回收器一样,不同的地方在于,它非常关注系统的吞吐量(通过参数控制) 
  –使用复制算法 
  –支持自适应的GC调节策略
(3)设置参数:
  -XX:+UseParallelGC  开启,新生代用ParallelGC回收器,老年代使用串行回收器。这也是在Server模式下的默认值。
  -XX:+UseParallelOldGC  新生代用ParallelGC回收器, 老年代使用ParallelOldGC回收器系统吞吐量的控制: 
  -XX:MaxGCPauseMillis=n(单位ms)   设置垃圾回收的最大停顿时间, 
  -XX:GCTimeRatio=n(n在0-100之间)  设置吞吐量的大小,假设值为n,那系统将花费不超过1/(n+1)的时间用于垃圾回收。来设置用户执行时间占总时间的比例,默认99,即1%时间用来进行垃圾回收。
  -XX:+UseAdaptiveSizePolicy  打开自适应GC策略, 在这种模式下, 新生代的大小, eden,survivior的比例, 晋升老年代的对象年龄等参数会被自动调整,以达到堆大小, 吞吐量, 停顿时间之间的平衡点

 ParallelOld(-XX:+UseParallelOldGC):

       ParallelOld是并行收集器,和SerialOld一样,ParallelOld是一个老年代收集器,是老年代吞吐量优先的一个收集器。这个收集器在JDK1.6之后才开始提供的,在此之前,ParallelScavenge只能选择SerialOld来作为其老年代的收集器,这严重拖累了ParallelScavenge整体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实!使用算法:标记 - 整理算法

图片.png

老年代ParallelOldGC回收器:

(1)特点: 
  –同新生代的ParallelGC回收器一样, 是属于老年代的关注吞吐量的多线程并发回收器 
  –使用标记压缩算法, 
(2)设置参数: 
  -XX:+UseParallelOldGC  开启,新生代用ParallelGC回收器, 老年代使用ParallelOldGC回收器, 是非常关注系统吞吐量的回收器组合, 适合用于对吞吐量要求较高的系统 
  -XX:ParallelGCThreads=n   指回ParNew回收器工作时的线程数量, cpu核数小时8时, 其值等于cpu数量, 高于8时, 可以使用公式(3+((5*CPU_count)/8))

CMS (-XX:+UseConcMarkSweepGC):

       CMS是一个老年代收集器,全称 Concurrent Low Pause Collector,是JDK1.4后期开始引用的新GC收集器,在JDK1.5、1.6中得到了进一步的改进。它是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的情况下,使用CMS非常合适。CMS的一大特点,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。使用算法:标记 - 清理

图片.png

      CMS的执行过程如下:

  1. 初始标记(STW initial mark):在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World)。这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。

  2. 并发标记(Concurrent marking):这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程哦。

  3. 并发预清理(Concurrent precleaning):这个阶段任然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。

  4. 重新标记(STW remark):这个阶段会再次暂停正在执行的应用线程,重新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。

  5. 并发清理(Concurrent sweeping):这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。

  6.  并发重置(Concurrent reset):这个阶段任然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。

老年代的并发回收器:

(1)特点: 
  –是并发回收, 非独占式的回收器, 大部分时候应用程序不会停止运行 
  –针对年老代的回收器, 
  –使用并发标记清除算法, 因此回收后会有内存碎片, 可以使参数设置进行内存碎片的压缩整理 
  –与ParallelGC和ParallelOldGC不同, CMS主要关注系统停顿时间 
(2)缺点:
1、内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。不过CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,
    当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致Full GC。
2、需要更多的CPU资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。
3、需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,
   必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。 
(3)设置参数: 
  -XX:-CMSPrecleaningEnabled  关闭预清理, 不进行预清理, 默认在并发标记后, 会有一个预清理的操作,可减少停顿时间 
  -XX:+UseConcMarkSweepGC  老年代使用CMS回收器,新生代使用ParNew回收器.使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现"Concurrent Mode Failure"失败后的后备收集器使用。
  -XX:ConcGCThreads=n  设置并发线程数量, 
  -XX:ParallelCMSThreads=n  同上, 设置并发线程数量,手工设定CMS的线程数量,CMS默认启动的线程数是(ParallelGCThreads+3)/4 
  -XX:CMSInitiatingOccupancyFraction=n  指定老年代回收阀值, 即当老年代内存使用率达到这个值时,会执行一次CMS回收,默认值为68也就是68%,仅在CMS收集器时有效,设置技巧: (Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100)>=Xmn 
  -XX:+UseCMSCompactAtFullCollection  开启内存碎片的整理, 即当CMS垃圾回收完成后, 进行一次内存碎片整理,要注意内存碎片的整理并不是并发进行的,因此可能会引起程序停顿,仅在CMS收集器时有效。 
  -XX:CMSFullGCsBeforeCompation=n  设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UserCMSCompactAtFullCollection参数一起使用。 
  -XX:+CMSParallelRemarkEnabled  在使用UseParNewGC的情况下, 尽量减少mark的时间 
  -XX:+UseCMSInitiatingOccupancyOnly  表示只有达到阀值时才进行CMS回收
  -XX:CMSInitiatingPermOccupancyFraction  设置Pem Gen使用达多少比率时触发,默认92%。

Class的回收(永久区的回收):

设置参数: 
  -XX:+CMSClassUnloadingEnabled  开启回收Perm区的内存, 默认情况下, 是需要触发一次FullGC 
  -XX:CMSInitiatingPermOccupancyFraction=n  当永久区占用率达到这个n值时,启动CMS回收, 需上一个参数开启的情况下使用

G1收集器:

       这是一个新的垃圾回收器,既可以回收新生代也可以回收老年代,SunHotSpot1.6u14以上EarlyAccess版本加入了这个回收器,Sun公司预期SunHotSpot1.7发布正式版本。通过重新划分内存区域,整合优化CMS,同时注重吞吐量和响应时间。

      G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域,通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器。

      在使用G1垃圾回收器的时候,通过 JVM参数 -XX:+UseStringDeduplication 。 我们可以通过删除重复的字符串,只保留一个char[]来优化堆内存。这个选择在Java 8 u 20被引入。

(1)特点: 
  –独特的垃圾回收策略, 属于分代垃圾回收器, 
  –使用分区算法, 不要求eden, 年轻代或老年代的空间都连续 
  –并行性: 回收期间, 可由多个线程同时工作, 有效利用多核cpu资源 
  –并发性: 与应用程序可交替执行, 部分工作可以和应用程序同时执行, 
  –分代GC: 分代收集器, 同时兼顾年轻代和老年代 
  –空间整理: 回收过程中, 会进行适当对象移动, 减少空间碎片 
  –可预见性: G1可选取部分区域进行回收, 可以缩小回收范围, 减少全局停顿 
(2)G1的收集过程 
1. 新生代GC: 
2. 并发标记周期: 
  –初始标记新生代GC(此时是并行, 应用程序会暂停止)–>根区域扫描–>并发标记–>重新标记(此时是并行, 应用程序会暂停止)–>独占清理(此时应用程序会暂停止)–>并发清理 
3. 混合回收: 
  –这个阶段即会执行正常的年轻代gc, 也会选取一些被标记的老年代区域进行回收, 同时处理新生代和年老轻 
4. 若需要, 会进行FullGC: 
  –混合GC时发生空间不足 
  –在新生代GC时, survivor区和老年代无法容纳幸存对象时, 
  –以上两者都会导致一次FullGC产生 
(3)设置参数: 
  -XX:+UseG1GC  打开G1收集器开关, 
  -XX:MaxGCPauseMillis=n  指定目标的最大停顿时间,任何一次停顿时间超过这个值, G1就会尝试调整新生代和老年代的比例, 调整堆大小, 调整晋升年龄 
  -XX:ParallelGCThreads=n  用于设置并行回收时, GC的工作线程数量 
  -XX:InitiatingHeapOccpancyPercent=n  指定整个堆的使用率达到多少时, 执行一次并发标记周期, 默认45, 过大会导致并发标记周期迟迟不能启动, 增加FullGC的可能, 过小会导致GC频繁, 会导致应用程序性能有所下降

2.4 垃圾收集器小结

常用的收集器组合:

         #新生代GC策略    #年老代GC策略      #说明    
组合1     Serial           Serial Old        Serial和SerialOld都是单线程进行GC,特点就是GC时暂停所有应用线程。    
组合2     Serial           CMS+Serial Old    CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。    
组合3     ParNew           CMS               使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNewGC策略。    
组合4     ParNew          Serial Old         使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。    
组合5     Parallel、Scavenge Serial Old      Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。    
组合6     Parallel、Scavenge  Parallel Old   Parallel Old是Serial Old的并行版本     
组合7     G1GC                G1GC          -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC  #开启
                                            -XX:MaxGCPauseMillis =50                  #暂停时间目标
                                            -XX:GCPauseIntervalMillis =200          #暂停间隔目标
                                            -XX:+G1YoungGenSize=512m            #年轻代大小
                                            -XX:SurvivorRatio=6            #幸存区比例

#https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/index.html  #这篇博文也不错。

垃圾收集器参数总结:

-XX:+<option> 启用选项
-XX:-<option> 不启用选项
-XX:<option>=<number> 
-XX:<option>=<string>

下面是参数和描述:

-XX:+UseSerialGC    #Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收    
-XX:+UseParNewGC    #打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收    
-XX:+UseConcMarkSweepGC    #使用ParNew + CMS +  Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。    
-XX:+UseParallelGC    #Jvm运行在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:GCTimeRatio    #GC时间占总时间的比列,默认值为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方式
Client     Serial 串行GC                 Serial Old 串行GC    
Server     Parallel Scavenge并行回收GC    Parallel Old 并行GC

Sun/oracle JDK GC组合方式:

图片.png

#此图来自于:http://blog.csdn.net/java2000_wl/article/details/8030172

垃圾回收一些概念小结:

垃圾回收(Garbage Collection,GC),研究这个主要目的就是为了提升JVM的性能,在内存泄露中能及时查缺问题所在。对于垃圾回收,GC必须要解决的问题包括三个:

  1)哪些内存可以回收?哪些对象可以回收? 这里主要就是判断哪些对象的死活
  2)什么时候回收? 一般就是当内存不够或者设置垃圾收集器的时间间隔
  3)如何回收? 对于已经判断为死的对象的回收算法以及实现这些算法的垃圾收集器

哪些内存和对象可以回收?

JVM中的五大内存区域中,程序计数器、虚拟机栈和本地方法栈都是随着线程而生,随着线程而灭亡。栈中的大小基本上在类结构确定下来的时候就已知了,这三个区域内存分配与回收都具备确定性,所以当方法结束或者线程结束时候,这三块的内存就随着回收了。
而Java堆和方法区中,存放了与类有关的信息以及类的实例对象,这些对象只会在具体运行期间才能创建,而这些对象创建与回收都是动态的,故垃圾回收需要考虑堆和方法区中的回收。
明确了需要回收哪一块的内存后,就需要再次解决哪些对象可以回收?
   垃圾回收的对象,这里主要回收的就是那些已经死去(即不再被任何途径使用的对象)。既然知道要回收这些死的对象,那么接下来就要确定怎么来判断一个对象的死活。
   在Java中使用的就是根搜索算法(GC Roots Tracing)来判断对象是否存活,而不是利用引用计数。
   根搜索算法的基本思想:通过一系列名为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所经过的路径称为引用链,当一个对象到GC Roots没有引用链的时候,则可初次判定该对象不可用。图论中表示就是从GC Roots到这个对象路径不可达。
   Java中可以作为GC Roots的对象有虚拟机栈中的引用对象,方法区中的类静态属性引用的对象,方法区中的常量引用的对象,本地方法区中Native的引用的对象。

垃圾回收的起点?

栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从Java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。
这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。
在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。
Java利用根搜索算法要经历两次标记过程来宣告一个对象死活:
   第一次标记并筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过一次(因该方法只会被调用一次),虚拟机则判定对象没有必要执行finalize()。如果有必要执行,将该对象放入F-Queue队列,稍后由虚拟机自动创建优先级低的Finalizer线程。
   第二次标记就发生在F-Queue中,如果在Finalizer线程执行时F-Queue中的对象与引用链上的对象建立了关联,第二次标记时会将该对象移出F-Queue,队列中剩下的经过两次标记的对象就是可以回收的。

什么时候回收?

 在Java中垃圾回收器启动的时间是不固定的,它根据内存的使用量而进行动态的自适应调整,来运行GC,在为内存分配的过程中就会有GC的产生过程。

如何回收?

确定了哪些对象以及内存需要回收后,此时就需要考虑采用什么样的策略以及用什么来具体实现这些策略。

回收的算法?

垃圾收集算法都是先用“根搜索算法”来判断哪些需要回收的,然后进行垃圾的回收处理,常用的垃圾收集算法的基本概况:

标记-清除算法(Mark-Sweep)、复制算法(Copying)、标记-整理算法(Mark-Compact)、分代收集算法(Generational Collecting)

回收的具体实现?

  1. 年轻代垃圾收集器:Young generation,在垃圾收集的过程中都会使得用户线程等待。

Serial收集器:一种单线程的收集器,采用“复制”收集算法,收集时候会暂停所有的工作线程,直到收集结束,一般虚拟机在Client模式下的默认新生代收集器就是采用这个收集器。优点:与单线程收集器比较简单高效。对于单个CPU下,由于没有多个线程的交互开销,在堆比较小的时候,一般停顿比较短,可以采用。
ParNew收集器:是Serial的一种多线程收集器,采用“复制”收集算法,它和Serial收集器除了多线程外其余行为都相同,即在收集的过程中会暂停所有的线程。它是运行在Server模式下的新生代收集器的首选。可以使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,或者使用-XX:+UseParNewGC选项来强制使用它。只能与CMS收集器配合使用。
Parallel Scavenge收集器:是一种并行(多条垃圾收集线程并行工作,用户线程依旧等待)的多线程收集器,采用“复制”算法来收集新生代。它所关注的是达到一个控制的吞吐量,就是CPU运行用户代码时间与CPU总耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
                         利用-XX:MaxGCPauseMillis可设置垃圾收集停顿时间,-XX:GCTimeRatio可设置垃圾收集占的总时间,是吞吐量的倒数。如果为19,则允许GC时间为5%(1/(1+19));这种收集器也有自适应调节策略。

  2. 老年代垃圾收集器:Tenured generation

Serial Old收集器:是一种单线程收集器,是Serial的老年代版本,使用的是“标记-整理”算法主要是虚拟机在Client模式下的使用的收集器。它在工作中依旧需要暂停所有的用户线程。主要用在作为CMS收集器的后备预案使用。
Parallel Old收集器:是一种多线程收集器,是Parallel Scavenge的老年代版本,使用的是“标记-整理”算法。它在工作中依旧需要暂停所有的用户线程。一般是结合Parallel Scavenge来一起使用,用于在注重吞吐量和CPU资源敏感的场合。
CMS收集器(Concurrent Mark Sweep):采用“标记-清除”的算法,目标是获取最短回收停顿时间的。
G1收集器:采用的是“标记-整理”算法,可以非常精确的控制停顿,可以实现在基本上不牺牲吞吐量的前提下完成低停顿的内存回收。

垃圾收集器中的并发与并行?

并行(Parallel):多条垃圾收集线程并行工作,此时用户线程处于等待停止状态。
并发(Concurrent):用户线程与垃圾收集线程同时执行,即用户程序继续运行,而垃圾收集程序运行在另一个CPU中

两种GC?

(1)对新生代的对象的收集称为minor GC;
(2)对旧生代的对象的收集称为Full GC;
(3)程序中主动调用System.gc()强制执行的GC为Full GC。

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
(3)弱引用:在GC时一定会被GC回收
(4)虚引用:由于虚引用只是用来得知对象是否被GC

GC性能指标:

吞吐量  #应用花在非GC上的时间百分比
GC负荷  #与吞吐量相反,指应用花在GC上的时间百分比
暂停时间 #应用花在GC stop-the-world的时间
GC频率  #顾名思义,GC执行的频率
反应速度 #从一个对象变成垃圾到这个对象被回收的时间
一个交互式的应用要求暂停时间越少越好,然而,一个非交互式的应用,当然是希望GC负荷越低越好。一个实时系统对暂停时间和GC符合的要求,都是越低越好。一个嵌入式系统当然系统Footprint越小越好。

内存容量配置原则:

年轻代大小选择:

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的引用: 尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
避免设置过小,当新生代设置过小会导致:1.YGC次数更加频繁2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC。

年老代大小选择:

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片,高回收频率以及
                    应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
                    并发垃圾收集信息、持久化并发收集次数、传统GC信息、花在年轻代和老年代回收商的时间比例。
吞吐量优先的应用: 一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。


作者:忙碌的柴少 分类:JVM 浏览:2339 评论:0
留言列表
发表评论
来宾的头像