我是靠谱客的博主 笑点低钻石,最近开发中收集的这篇文章主要介绍JVM结构、垃圾回收、常用参数与调优汇总,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

参考文献

[1] JVM调优浅谈
[2] GC详解及Minor GC和Full GC触发条件总结
[3] 深入JVM读书笔记(四)——Java的垃圾收集器
[4] 深入理解 Java G1 垃圾收集器
[5] 浅析Java虚拟机结构与机制
[6] JAVA的内存模型及结构


1.JVM虚拟机结构与机制

1.1.JVM结构

下图展示了JVM的主要结构:
这里写图片描述
可以看出,

  • JVM主要由类加载器运行时数据区执行引擎本地方法接口等组成。
  • 其中,运行时数据区又由方法区Java栈本地方法栈程序计数器寄存器组成。
  • 方法区是被所有线程锁共享的。
  • Java栈本地方法栈程序计数器寄存器是每个线程独有的。

1.2.类加载器(Class Loader)

类加载器负责加载编译好的.class字节码文件,装入内存,使JVM可以实例化或以其他方式使用加载后的类。

1.2.1.类加载器分类

1.启动类加载器(BootStrap Class Loader)

  • 负责加载rt.jar文件中所有的类,即Java的核心类都是由它加载的。
  • 在Sun JDK中,这个加载器是由C++实现的,并且在Java语音中无法获取它的引用。

2.扩展类加载器(Extension Class Loader)

  • 负责加载一些扩展功能的jar包。

3.系统类加载器(System Class Loader)

  • 负责加载启动参数中指定的Classpath中的jar包即目录。
  • 通常我们自己编写的Java类都由此加载器加载。
  • 在Sun JDK中,系统加载器的名字叫AppClassLoader。

4.用户自定义类加载器(User Defined Class Loader)

  • 由用户自定义的加载规则,可以控制加载时的过程。

1.2.2.Class Loader工作原理

类加载分为:装载、链接、初始化三步骤。
1.装载

  • 通过类的全限定名加载,在JVM内部形成以“类的全限定名+ClassLoader实例ID”来标明类。
  • ClassLoader实例和类的实例都位于堆中,他们的信息都位于方法区中。
  • 装载过程采用了“双亲委派模型”进行。
  • 加载隔离原理:不同类加载器记在的类之间无法直接交互。

2.链接

  • 链接的任务是将二进制的类信息合并到JVM运行状态中。
  • 第一步,验证:校验.Class文件的正确性。
  • 第二步,准备:为类分配内存,初始化静态变量为默认值。
  • 第三步,解析(可选):将常量池中的符号引用解析成直接引用。

3.初始化

初始化类中静态变量,并执行类中的static代码、构造函数。

何时?

  • 通过new、反射、clone、反序列化机制实例化对象时。
  • 调用类的静态方法时。
  • 使用类的静态字段时。
  • 通过反射调用类的方法时。
  • 初始化该类的子类时。
  • JVM启动时,具有main方法的类。

1.3.Java栈(Java Stack)

Java栈由栈帧组成,一个帧对应一个方法调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。

Java栈分为三部分:J局部变量区、操作数栈、帧数据区。

1.局部变量区:以字节为长度单位的数组。

局部变量区是以字长为单位的数组,在这里,byte、short、char类型会被转换成int类型存储,除了long和double类型占两个字长以外,其余类型都只占用一个字长。特别地,boolean类型在编译时会被转换成int或byte类型,boolean数组会被当做byte类型数组来处理。

局部变量区也会包含对象的引用,包括类引用、接口引用以及数组引用。

局部变量区包含了方法参数和局部变量,此外,实例方法隐含第一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。

2.操作数栈
操作数栈也是以字长为单位的数组,但是正如其名,它只能进行入栈出栈的基本操作。在进行计算时,操作数被弹出栈,计算完毕后再入栈。

3.帧数据区

帧数据区的任务主要有:

a.记录指向类的常量池的指针,以便于解析。

b.帮助方法的正常返回,包括恢复调用该方法的栈帧,设置PC寄存器指向调用方法对应的下一条指令,把返回值压入调用栈帧的操作数栈中。

c.记录异常表,发生异常时将控制权交由对应异常的catch子句,如果没有找到对应的catch子句,会恢复调用方法的栈帧并重新抛出异常。

局部变量区和操作数栈的大小依照具体方法在编译时就已经确定。调用方法时会从方法区中找到对应类的类型信息,从中得到具体方法的局部变量区和操作数栈的大小,依此分配栈帧内存,压入Java栈。

1.4.本地方法栈(Native Method Stack)

本地方法栈类似于Java栈,主要存储了本地方法调用的状态。在Sun JDK中,本地方法栈和Java栈是同一个。

1.5.方法区(Method Area)

类型信息和类的静态变量都存储在方法区中。方法区中对于每个类存储了以下数据:

  • a.类及其父类的全限定名(java.lang.Object没有父类)
  • b.类的类型(Class or Interface)
  • c.访问修饰符(public, abstract, final)
  • d.实现的接口的全限定名的列表
  • e.常量池
  • f.字段信息
  • g.方法信息
  • h.静态变量
  • i.ClassLoader引用
  • j.Class引用

可见类的所有信息都存储在方法区中。

在Sun JDK中,方法区对应了永久代(Permanent Generation),默认最小值为16MB,最大值为64MB。

1.6.堆(Heap)

堆用于存储对象实例以及数组值。堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。堆中还可能存放了指向方法表的指针。堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。

此外,堆中的实例数据中还包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。

在堆的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。主要分为年轻代、旧生代。分代方式大大改善了垃圾收集的效率。

  • 1、年轻代(New Generation)
    • 大多数情况下新对象都被分配在年轻代中。
    • 年轻代由Eden Space和两块相同大小的Survivor Space组成,后两者主要用于Minor GC时的对象复制。
    • JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配。
    • 我们知道在堆上分配内存需要锁定整个堆。
    • 而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率。
  • 2、旧生代(Old Generation/Tenuring Generation)
    • 在年轻代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有年轻代高。

1.7.执行引擎

执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。

JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈和PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例。

1.8.Program Counter Register(程序计数器)

一块较小的内存空间, 作用是当前线程所执行字节码的行号指示器(类似于传统CPU模型中的PC), PC在每次指令执行后自增, 维护下一个将要执行指令的地址.

在JVM模型中, 字节码解释器就是通过改变PC值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖PC完成(仅限于Java方法, Native方法该计数器值为undefined).

不同于OS以进程为单位调度, JVM中的并发是通过线程切换并分配时间片执行来实现的. 在任何一个时刻, 一个处理器内核只会执行一条线程中的指令.

因此, 为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器, 这类内存被称为“线程私有”内存.

返回顶部


2.堆(heap)与栈(stack)

堆(heap)栈(stack)是程序运行的关键,很有必要将他们的关系理顺清除。

程序的运行总要有一个起点(程序执行的入口)。在Java中,main()函数就是栈的起始点,也就是程序的起始点。

堆(heap)栈(stack)的关系图如下所示:
这里写图片描述

概括:

  • 栈(stack)是程序运行时的单元,而堆(heap)是程序存储时的单元。
  • 栈(stack)解决程序运行的问题,即程序如何运行,或者说如何处理数据。
  • 堆(heap)解决数据存储的问题,即数据怎么放,放在哪。
  • 在Java中,每个线程都会有一个相对应的栈(stack)。这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要建立一个线程栈,去管理这些线程。而堆则是所有线程所共享的。

疑问一:为什么要把栈和堆区分出来?栈中不是也可以存储数据吗?

1.从软件设计的角度,栈(stack)代表了处理逻辑,而堆(heap)代表了数据。

这样分开,使得处理逻辑更加清晰,这是一种分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有所体现。

2.堆(heap)和栈(stack)的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。

这样做的好处:

  • 提供了一种有效的数据交互方式:共享内存
  • 堆中的共享常量和缓存可以被所有栈访问,节省了空间

3.堆和栈的拆分使得动态增长成为可能。

栈因为运行的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储数据的能力。

而堆不同,堆中的对象是可以根据需要动态增长的。

因此堆和栈的拆分使得动态增长成为可能,相应栈中只需要记录堆中一个地址即可。

4.堆和栈的结合是面向对象的完美体现。

其实,面向对象的程序和结构化的程序在执行上没有任何区别。

但是,面向对象的引入,使得对待问题的思考方式发生了变化,而更接近于自然方式的思考。

当我们把对象拆开,你就会发现,对象的属性就是数据,存放在堆中;对象的行为(方法)就是运行逻辑,存放在栈中。

我们在编写程序时,其实就是编写了数据结构,也编写了处理数据的逻辑。不得不承认,面向对象的设计确实很美。


疑问二:堆中存放什么?栈中存放什么?

1.栈中存储的都是和当前线程(或程序)相关的信息

在栈中存储局部变量、程序与运行状态、方法、方法返回值等。

在栈中存储基本数据类型和堆中对象的引用。

一个对象的大小是不可预估的,或者说是动态变化的,但是在栈中,一个对象只对应了一个4byte的引用(堆栈分离的好处)

2.堆中只负责存储对象信息。


疑问三:为什么不把基本类型放在堆中?

1.基本类型占用的空间大小为1byte~8byte,占用空间较小。

2.因为是基本类型,所以不会出现动态增长的情况(长度固定),因此在栈中存储就足够了。


疑问四:Java中的参数传递到底是值传递还是引用传递?

基本类型参数传递是值传递;引用类型参数传递传递的是对象引用拷贝,即传递的是引用的地址值,所以还是值传递。

Tips:

  1. 堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。
  2. 而对是为了栈提供数据存储服务的,说白了,对就是一块共享内存。
  3. 不过,正式由于堆和栈分离的设计,才使得垃圾回收成为可能。
  4. 在Java中,栈的大小是通过-Xss来设置,当栈中存储的数据较多时,需要适当调大这个值,否则就会出现StackOverflowError异常。常见的出现这个异常的原因都是因为无法返回的递归,因为此时栈中保存的都是方法返回的记录点。

疑问五:Java对象的大小如何计算

基本数据类型的大小是固定的,不再赘述。这里只是讨论非基本数据类型的数据,其大小指的商榷。

在Java中,一个空Object对象的大小是8byte,这个大小只是保存在堆中一个没有任何属性的对象的大小

看看下面的语句:

Object obj = new Object();

上述语句,完成了一个java对象的声明,但它所占的空间为:4byte + 8byte = 12byte。

其中,4byte是上面所说的栈中保存引用地址所需的空间,8byte是堆中存储对象所需的空间大小

因为java的所有非基本类型的对象都是继承自Object对象,所以无论何种对象,其大小都必须大于等于8byte。

有了Object对象的大小,我们就可以计算其他类型对象的大小了,如下:

Class NewObject{
	int count;
	boolean flag;
	Object ob;
}

其大小为:空对象NewObject大小8byte + int属性大小4byte + boolean属性大小1byte + 空ob对象引用大小4byte = 17 byte。

但是因为**Java在对对象分配内存时都是以8的整数倍进行,**所以此对象被分配的实际大小是24byte。

这里需要注意一下基本类型的包装类型,因为这种类型已经成为对象了,所以我们需要将其以对象看待。

包装类型的大小至少是12byte(声明一个空Object对象的大小),而且12byte是没有包含任何信息的。

同时因为java对象大小是8的整数倍,所以一个基本类型包装类的大小至少是16byte。

这个内存时基本类型内存的N(N>2)倍,这些类型在内存占用十分夸张,在使用基本路线封装类型时需要考虑这个方面。

返回顶部


2.数据类型

Java虚拟机中,数据类型可以分为两种:基本类型引用类型

基本类型的变量保存的是原始值,即:它代表的值就是数值本身。
引用类型的变量保存的是引用值引用值代表了某个对象的引用地址,而不是对象本身,对象本身存放在这个引用值所表示的地址的内存中。

基本类型包括:byte,short,int,long,boolean,float,double,char。
引用类型包括:类类型、接口和数组

下表显示了基本类型的大小、取值范围和默认值:

序号基本类型大小取值范围默认值
1byte8bit/1B-128~1270
2short16bit/2B-65536~655350
3int32bit/4B-231~231 - 10
4long64bit/8B-263~263 - 10L
5boolean8bit/1Btrue,falsefalse
6char16bit/2B‘n0000’’uffff’/065535‘u0000’/0
7float32bit/4B1.4013E-45 ~3.4028E+380.0F
8double64bit16B4.9E-324 ~1.7977E+3080.0D

引用类型

对象引用类型可以分为:强引用、软引用、弱引用、虚引用。

1.强引用:声明对象时虚拟机生成的引用。

eg. Sample sample = new Sample();

强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收。

2.软引用:一般被用作缓存来使用。

垃圾回收时,JVM会根据当前内存剩余情况来决定是否回收被软引用的对象。

换句话说,JVM发送OutOfMemory时,肯定是没有软引用存在的。

3.弱引用:与软引用类似,都是用来做缓存。

垃圾回收时,弱引用的对象是一定会被回收的,也就是说弱引用只存在于一个垃圾回收周期中。

4.虚引用:形同虚设,就如同没有引用一样。

虚引用就和没有引用一样,并不影响垃圾回收。

关于引用类型的更详细说明可以参考:https://blog.csdn.net/rodbate/article/details/72857447

返回顶部


4.垃圾回收算法

4.1.按照基本回收策略划分

4.1.1.引用计数(Reference Counting)算法

比较古老的回收算法,即:此对象有一个引用,则增加一个计数,删除一个引用,则减少一个计数。

垃圾回收时,只回收引用计数为0的对象。

引用计数(Reference Counting)算法最致命的问题是:无法处理循环引用的问题


4.1.2.标记-清除(Mark-Sweep)算法

算法示例图如下:
这里写图片描述

此算分执行分为两个阶段:

  • 第一阶段:从引用根节点开始标记所有被引用的对象。
  • 第二阶段:遍历整个堆,把未标记的对象清除。

标记-清除(Mark-Sweep)算法需要暂停整个引用,而且会产生内存碎片。


4.1.3.复制(Copy)算法

算法示意图如下:
这里写图片描述

此算法将内存空间划分为两个相等的区域,每次只使用其中一个区域。

垃圾回收时:遍历当前使用区域,将正在使用的对象复制到另一个区域中。

此算法每次只复制正在使用的对象,因此复制成本较小;同时复制过去以后还能进行相应的内存整理,不会出现内存碎片。

当然,算法缺点也很明显:需要两倍的内存空间。


4.1.4.标记-整理(Mark-Compact)算法

算法示意图:
这里写图片描述
此算法集合了“标记清除-”和“复制”两个算法的优点。

此算分执行分为两个阶段:

  • 第一阶段:从引用根节点开始标记所有被引用的对象。
  • 第二阶段:遍历整个堆,清除未标记对象,并且把存活对象“压缩”到堆的其中一块,并按顺序排放。

此算法避免了“标记-清除”算法的内存碎片问题,同时也避免了“复制”算法的双倍内存空间问题。

不过,因为这种算法既要标记所有存活对象,还要整理所有存活对象,所以其效率并不高(低于“复制”算法和“标记-清除”算法)

返回顶部


4.2.按照分区对待的方式划分

4.2.1.增量收集(Incremental Collection)

实时的垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因,JDK5.0中的收集器并没有使用这种算法。

4.2.2.分代收集(Generational Collection)

基于对象生命周期分析后得出的垃圾回收算法,把对象分为:年轻代、年老代、永久代。

对于不同生命周期的对象使用不同的回收算法。现在的垃圾回收器(自J2SE1.2.开始)都是使用此种算法的。

返回顶部


4.3.按照系统线程划分

4.3.1.串行收集

串行收集:使用单线程处理所有回收工作。

因为无需多线程交互,容易实现,而且效率比较高。

但是,其局限性也很明显,即:无法发挥多处理器的优势,因此此收集器适合单处理器机器。

当然,此处理器也可以用于小数量(100M左右)情况下的多处理器机器上。

4.3.2.并行收集

并行收集:使用多线程处理所有回收工作。

因为利用了多核处理器的优势,使用多线程进行并行回收,所以速度快,效率高。

而且,理论上处理器核数越多,越能体现并行收集器的优势。

4.3.3.并发收集

并发收集:GC线程和应用线程大部分都是并发执行,只是在初始标记(initial mark)和二次标记(remark)时需要stop-the-world。

这种方式,可以大大缩短停顿时间(pause time),所以适用于响应优先的应用,减少用户等待时间。

由于GC和应用程序并发执行,只有在多CPU的环境下,才能发挥其价值,在执行过程中,还会产生悬浮垃圾(floating garbage)。如果等空间满了再开始GC,那这些新产生的悬浮垃圾就无处放置了,这是就会启动一次串行GC,等待时间会很长,所以要在空间还未满时,就要启动GC。

mark和sweep操作会引起很多碎片,所以间隔一段时间需要整理整个空间,否则遇到大对象,没有连续空间,会产生一次串行GC。

采用此收集器,收集频率不能大,否则会影响到CPU利用率,进而影响吞吐量。

返回顶部


5.如何区分垃圾

上面说到“引用计数”法,通过统计控制生成和删除对象的引用数来判断。垃圾回收 程序计数器为0的对象即可。但是这种方法无法解决循环引用问题。

所以,在后来的垃圾判断算法中,都是从程序运行的根引用出发,遍历整个对象引用,查找存活对象。那么在这种实现方法中,垃圾回收从哪里开始呢?即,从哪里开始查找哪些对象是正在被当前系统所使用的?

上面分析堆和栈的区别,可知,栈是真正进行程序执行的地方,所以要获取哪些对象正在被引用,则需要从栈开始。同时,一个栈对应一个线程,因此,如果有多个线程,则必须对这些线程的所有栈进行检查。

从栈查找垃圾示意图如下:
这里写图片描述

同时,出了之外,系统还有运行时的寄数器等,也是存储程序运行数据的

这样,以栈或寄数器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到堆中其他对象的引用,这种引用逐步扩展,最终以null引用或基本类型结束。

这就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多个对象树。

在这些对象树上的引用,都是当前系统运行所需要的对象,不能被垃圾回收;而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。

因此,垃圾回收的起点是一些根对象,如java栈、静态变量、寄存器等。而最简单的java栈就是java程序执行的main方法。

这种垃圾查找方式,就是上面提到的“标记-清除算法”和“标记-整理”的垃圾标记方式。

返回顶部


6.如何处理碎片

由于不同的Java对象的存活时间是不一致的,因此在程序运行一段时间后,如果不进行内存整理,就会出现零散的内存碎片。

内存碎片导致的最直接问题就是:无法分配大块的内存空间以及程序运行效率降低。

上面提到的“复制”算法和“标记整理”算法都可以解决碎片问题。

返回顶部


7.如何处理回收内存和分配内存的矛盾

垃圾回收线程时用来回收内存的,而程序运行线程时用来分配内存的,二者一个回收内存,一个分配内存,是相互矛盾的。

因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整个应用(即暂停内存的分配),然后进行垃圾回收,回收完成之后再继续应用。这种方式是最直接,而且最有效的解决二者矛盾的解决方式。

但是这种方式有一个明显弊端:当堆空间持续增大,垃圾回收时间也会相应变大,应用暂停的时间也会相应变长。一些相应时间要求较高的程序,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超出这个限制。在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。

为了解决这种问题,有了并发垃圾回收算法,使用这种算法,GC线程和应用线程同时运行,解决了应用暂停的问题。但是,因为在新生产对象的同时又要回收对象,算法复杂度大大增加,系统的处理能力也会相应降低,同时,内存碎片问题将比较难解决。

返回顶部


8.分代收集算法

分带收集回收策略是基于这用一个事实:不同的对象生命周期不一样。

这是一种分而治之的思想,不同生命周期的对象采用不同的收集方式,以便提高回收效率。

在Java程序运行过程中,会产生大量的对象。

  • 其中有些对象与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这些对象跟业务直接挂钩,因此生命周期较长。
  • 还有一些对象,主要是程序运行过程中产生的临时变量,这些对象生命周期较短。如String对象,由于其不可变性,系统会产生大量的这种对象,有些对象甚至只使用一次就可回收。

试想,在不进行对象生命周期区分的情况下,每次垃圾回收都对整个堆空间进行回收,那么每次回收时间会超长。而且,因为每次回收都会遍历所有对象,而对于那些生命周期长的对象,这种遍历是没有意义的,因为可能进行了多次遍历,这种对象仍然存在。

因此,分代收集垃圾回收策略采用分而治之的思想,进行代的划分,把不同生命周期的对象划分到不同的代上,不同代采用最适合它的方式进行回收。


堆中分代示意图如下:
这里写图片描述
如图所示,JVM中共划分了三个代:年轻代(Young Generation)、年老代(Old Generation)和永久带(Permenet Generation)。其中,永久带(Permenet Generation)主要用来存放Java类的类信息,与垃圾收集要收集的对象关系不大;年轻代(Young Generation)和年老代(Old Generation)的划分是对垃圾回收影响较大的。


8.1.年轻代(Young Generation)

所有新生成的对象首先存放在年轻代,年轻代的目标是尽可能的快速回收掉那些声明周期短的对象。

一般而言,年轻代划分为三个区:一个Eden区,两个Survivor区。

年轻代的垃圾回收采用改进的“复制”算法,机制如下:

  • 大部分对象在Eden区中生成。
  • 当Eden区满时,触发一次Minor GC,将Eden区中存活的对象复制到一个Survivor区A中。
  • 当Eden区再次满时,触发一次Minor GC,将Eden区以及Survivor区A中存活的对象复制到Survivor区B中。
  • 当Eden区再次满时,触发一次Minor GC,将Eden区以及Survivor区B中存活的对象复制到Survivor区A中。
  • 每次当Eden区满,都会触发Minor GC,通过复制算法进行年轻代垃圾回收,回收结果就是Eden区和一个Survivor区将被清空,另一个Survivor区持有所有存活的对象。
  • 两个Survivor区将交替作为存活对象的存放区,交替往复。
  • 当对象的复制次数达到15次,表明该对象生命周期确实很长,下次Minor GC时,将对象移动到年老代中。

注意:

  • 两个Survivor区(From Survivor和To Survivor)是对称的,没有先后顺序。
  • Survivor区总有一个是空的。

为什么要有两个Survivor区?

通过回顾之前我们可以发现,其实一个Survivor区也能够完成上述的垃圾回收流程。但是一个Survivor区有一个很大的问题,会导致碎片化。

如果只是进行一次复制算法,从Eden区复制到Survivor区,并不会产生碎片。但是,如果进行多次复制算法,将可能产生碎片。只有一个Survivor区的GC流程如下:
这里写图片描述

同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

8.2.年老代(Old Generation)

在年轻代中经历了16次垃圾回收之后任然存活的对象,就会被存放到年老代中。因此可以认为,年老代中存放的都是一些生命周期较长的对象。

8.3.永久带(Permenet Generation)

永久代用于存放静态信息,如Java类信息、方法信息等,对垃圾回收并没有显著影响。

但是,有些应用程序肯能会动态生成或者调用一些Class,如Hibernate等,在这种情况下,需要设置一个比较大的永久代空间,用来存放这些运行中新增的类。

永久代大小通过-XX:MaxPermSize = <N>来设置。

8.4.GC分类与触发条件

由于对象进行了分代处理,所以垃圾回收区域、时间也不一样。GC常用的有两种:Young GC和Full GC。

8.4.1.Young GC

一般情况下,当新对象生成,并且在Eden区申请空间失败时,就会触发Young GC,对Eden区进行GC,清除非存活对象,并把尚且存活的对象复制到Survivor区。

Young GC针对的是年轻代的GC,不会影响到年老代。

因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会很频繁。

因此,一般需要在这里使用速度快、效率高的算法,使Eden尽快空闲出来。

8.4.2.Young GC触发条件

Young GC触发条件:Eden区空间不足。

8.4.3.Full GC

对整个堆进行GC,包括young、Tenured和Perm。

Full GC因为要对整个堆进行回收,所以比Young GC要慢,一次应该尽量减少Full GC的次数。

在对JVM的调试过程中,很大一部分工作就是对Full GC的调节。

8.4.4.Full GC触发条件

Full GC触发条件:

  • 1.显示调用System.gc(),建议JVM执行Full GC,但是不是必然执行。
  • 2.年老代空间不足。
  • 3.通过Young GC后进入年老代的对象大小大于年老代的可用内存。
  • 4.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转移到年老代,年老代的可用内存小于对象大小。
  • 5.永久代/方法区空间不足。

8.5.分代回收算法示意图

这里写图片描述

返回顶部


9.垃圾收集器

Java的垃圾回收机制最主要的体现者就是“垃圾收集器”,但是每个厂商提供的垃圾收集器都有很大的区别,而且同一虚拟机也会提供几个不同的垃圾收集器供用户根据项目的不同特点来组合。

下图是Hot Spot 虚拟机包含的收集器。
这里写图片描述

在介绍各种收集器之前,先确认一下在垃圾收集器语境中,并行回收和并发回收的概念:

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

9.1.Serial(年轻代串行收集器)

最基本、最悠久的收集器,它是一个单线程收集器。其中Serial是年轻代收集器,Serial Old是年老代收集器。

当串行收集器进行垃圾回收时,必须暂停所有工作线程,这个过程称之为“Stop The World”

示意图:
这里写图片描述

优点(相对于其他收集器的单个线程):

  • 算法简单
  • 无线程交互开销,最高效

适用于:

  • 运行在Client模式下的虚拟机。
  • 小数量(100M以下)数据的情况下。

配置:

  • 通过-XX:+UseSerialGC打开。

9.2.ParNew(年轻代并行收集器)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余和Serial所有参数一样。

在配置为CMS GC的情况下默认的年轻代收集器。

示意图:
这里写图片描述

优点:

  • 在多CPU环境下可以发挥更高效率
  • 唯一一个可以和年老代CMS(Concurrent Mark Sweep)收集器搭配年轻代并行GC。

适用于:

  • 运行在Server模式下的JVM。

配置:

  • 通过-XX:+UseParNewGC打开。

9.3.Parallel Scavenge(年轻代并行回收收集器)

看上去和ParNew没什么区别,但是Parallel Scavenge最大的特点是它的关注点在CPU的吞吐量上。

CPU吞吐量 = 运行代码时间 / (运行代码时间 + 垃圾回收时间)

较高的吞吐量可以更好的利用CPU的效率。

优点:

  • 被称之为“吞吐量优先”垃圾收集器。有一个自适应调节参数(-XX:+UseAdaptiveSizePolicy)。当打开这个参数之后,无需指定年轻代大小(-Xmn)、Eden和Survivor比例(-XX:SurvivorRatio)、晋升年老代年龄限制(-XX:PretenureSizeThreshold)等细节参数,虚拟机会动态调节这些参数,来提供最合适的停顿时间或最大吞吐量。

缺点:

  • 垃圾回收过程中,相应时间可能加长。

适用于:

  • 不能和年老代的CMS收集器搭配。
  • 吞吐量有高要求的情景,如后台处理、科学计算等。

配置:

  • 本身是Server模式多CPU机器上年轻代的默认GC方式。
  • 通过-XX:+UseParallelGC来指定开启。
  • 通过-XX:ParellelThread = <N>来指定线程数量。
  • 通过-XX:+UseAdaptiveSizePolicy来进行自适应调节。
  • 通过-XX:MaxGCPauseMillis来指定最大垃圾收集停顿时间,可能会降低吞吐量。
  • 通过-XX:GCTimeRatio来配置吞吐量[1/(1+N)]。,例如-XX:GCTimeRatio=19 => 1/20 =>5%,即5%的时间用于垃圾回收。默认为99,即1%的时间用于垃圾回收。

9.4.Serial Old(年老代串行收集器)

Serial Old是Serial收集器的年老代版本,同样是一个单线程收集器,适用“标记-整理”算法。

适用于:

  • Client模式下的虚拟机。
  • Server模式下与Parallel Scavenge搭配适用。
  • Server模式下作为CMS收集器的预备方案。

9.5.Parallel Old(年老代并行回收收集器)

Parallel Old是Parallel Scavenge的年老代版本,为了配合Parallel Scavenge的面向吞吐量特性而开发的对应组合。

Parallel Old产生于JDK1.6。

示意图:
这里写图片描述

适用于:

  • 注重CPU吞吐量的场景。

9.6.CMS(年老代并发收集器)

CMS(Concurrent Mark Sweep)收集器是一种以获取最快响应时间为目的的收集器。

它基于“标记-清除”算法,这个过程分为四个阶段:

  • 初始标记(CMS Initial Mark)
  • 并发标记(CMS Concurrent Mark)
  • 重新标记(CMS Remark)
  • 并发清除(CMS Concurrent sweep)

其中,

  • 初始标记和重新标记仍然需要Stop The World
  • 初始标记仅仅标记一下GC root能够直接关联的对象。
  • 并发标记进行GC Roots Tracing的过程。
  • 重新标记为了修正并发标记期间因用户线程继续执行而导致标记产生变动的那部分对象。

示意图:
这里写图片描述

CMS收集器在Young GC时会暂停所有用户线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用程序,而是使用若干个后台线程定期对年老代进行扫描,及时清除其中不再使用的对象。

优点:

  • 并发收集,效率高。
  • 低停顿。

适用于:

  • 重视服务响应速度、系统停顿时间和用户体验的互联网网站或B/S系统。

配置:

  • 通过-XX:+UseG1GC开启。
  • 通过-XX:CMSInitiatingOccupancyFraction = <N>指定还剩余多少堆空间时,开始执行并发收集。

浮动垃圾:

  • 由于应用运行时进行垃圾回收,所以有些垃圾可能在垃圾回收完成时产生,这种垃圾称之为“悬浮垃圾(floating garbage)”,这些垃圾需要在下次进行GC时才能回收掉。
  • 如果等到空间使用率到达100%再进行并发收集,则可能会因为没有空间存放“悬浮垃圾(floating garbage)”,而导致Full GC。
  • 一般情况下,并发收集器需要预留20%的空间用于存放这些“悬浮垃圾(floating garbage)”

Concurrent Mark Failure:

并发收集器在程序运行时进行GC,所以需要保证GC期间,有足够的内存空间以供程序使用;否则,垃圾还未回收完成,堆空间先满了。这种情况下,将会触发“Concurrent Mark Failure”,此时整个应用将暂停,并进行串行 Full GC。

9.7.G1(垃圾先行收集器)

G1(Garbage First)收集器是当前收集器技术的最前沿成果,它的设计原则是:简单可行的性能调优。

相较于CMS有两个显著改进:

  • 基于“标记-整理”算法实现收集器和精确停顿。
  • 能够在不牺牲吞吐量的前提下完成低停顿的内存回收。

示意图:
这里写图片描述

9.8GC组合

9.8.1.默认配置

模式年轻代GC年老代和持久带GC
-client串行GC/Serial串行GC/Serial Old
-server并行回收GC/Parallel Scavenge并发回收/Concurrent Mark Sweep

9.8.2.推荐组合

适用场景示例命令年轻代GC年老代和持久带GC缺点
最简配置数据量小(100M下)
对响应时间无要求
-XX:+UseSerialGC
-XX:+UseSerialOldGC
串行回收GC/Serial串行回收GC/SerialOld只适用于小系统
最短响应时间Web服务器/应用服务器、
电信交换、集成开发环境
-XX:+UseParallelGC
-XX:+UseParallelOldGC
并行回收GC/Parallel Scavenge并行回收GC/Parallel OldGC期间响应时间可能加长
最大吞吐量后台处理、科学计算等-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
并行GC/ParNew并发GC/CMS(Concurrent Mark Sweep)
当出现concurrent mode failure时采用备用的串行GC
整体吞吐量可能下降

返回顶部


10.JVM配置

以下配置主要针对分代垃圾回收算法而言。

10.1.堆大小设置

JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bit 还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。

32位系统下,一般限制在1.5G~2G;64位操作系统对内存无限制。在Windows Server 2003系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

配置举例: java -Xmx3550m -Xms3550m -Xmn1g -Xss128k

  • -Xmx3550m :JVM的最大堆内存为3550MB。
  • -Xms3550m :JVM的初始对内存为3550MB。此值可设置与-Xmx相同,以避免每次GC之后JVM重新分配内存。
  • -Xmn1g :JVM的年轻代大小为1GB。
    • 堆 = 年轻代 + 年老代 + 永久代,永久代一般固定为64m,所以增大年轻代,将会减小年老代的大小。
    • 此值对系统性能影响较大,Sun官方推荐为整个堆的3/8。
    • 此值不宜设置过大,如果设置过大,将导致年老代过小,年老代很容易发生空间不足问题,从而导致频繁Full GC。
  • -Xss128k :JVM每个线程栈的大小。
    • JDK5.0之后,每个线程栈大小为1M,以前每个线程栈大小为256k。
    • 根据应用所需的线程所需内存大小进行调整。在相同的物理条件下,减少这个值能够生成更多的线程。
    • 但是操作系统对一个进程内的线程数本身是有限制的,不能无限生成,经验值在3000~5000左右。

配置举例: java -Xmx3550m -Xms3550m -Xss128k-XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

  • -XX:NewRatio=5 :年老代/年轻代 = 4。年轻代占栈大小(除去永久代)的1/5。
  • -XX:SurvivorRatio=4 :Eden/Survivor = 4。Eden区占年轻代的4/6,每个Survivor区栈年轻代的1/6。
  • -XX:MaxPermSize=16m :即持久区大小为16m。
  • -XX:MaxTenuringThreshold=0 :垃圾最大复制次数。
    • 如果设置为0的话,则年轻代的存活对象不会进入Survivor区,而是直接进入年老代。对于年老代对象较多的应用,可以提高效率。
    • 如果设置为一个较大值,则年轻代会在Survivor区进行多次复制,增加了对象在年轻代的存活时间,增加年轻代被回收的概率。
    • 默认为-XX:MaxTenuringThreshold=15,即对象在年轻代最多经过15此GC,在第16次时,将被移动到年老代。

10.2.收集器选择

JVM给出了三类收集器:串行收集器、并行收集器和并发收集器。但是串行收集器只适合小数据量的情况,所以这里选择的主要是针对并行收集器和并发收集器。

默认情况下,JDK5之前都是使用串行收集器,如果想要使用其他收集器需要在启动参数处加入相应参数。JDK5.0之后,JVM会根据当前系统配置进行判断。

10.2.1.吞吐量优先的配置

如上文所示,并行收集器主要以达到一定吞吐量为目标,适用于科学计算和后台处理等。

配置举例: java -Xmx3550m -Xms3550m -Xmn1g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=4

  • -XX:+UseParallelGC :选择年轻代收集器为并行收集器。此配置只对年轻代生效,年老代依然是串行收集器。
  • -XX:ParallelGCThreads=4:配置并行收集器的线程数量为4。即同时多少个线程在进行GC,此值最好与CPU核数相等。

配置举例: java -Xmx3550m -Xms3550m -Xmn1g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=4 -XX:+UseParallelOldGC

  • -UseParallelGC :配置年老代垃圾收集方式为并行收集,JDK6开始支持年老代的并行收集。

配置举例: java -Xmx3550m -Xms3550m -Xmn1g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100

  • -XX:MaxGCPauseMillis=100 :设置每次年轻代GC的最长时间为100毫秒,如果当前配置无法满足要求,则JVM会自动调整年轻代大小,以满足此值。

配置举例: java -Xmx3550m -Xms3550m -Xmn1g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

  • -XX:+UseAdaptiveSizePolicy :启用自适应大小配置。设置此选项后,并行收集器会自动调整年轻代大小和相应Survivor比例,以达到目标系统规定的最低相应时间或收集频率。此值建议使用并行收集去器时,一直开启。

10.2.2.相应时间优先的配置

如上文所示,并发收集器主要 是保证系统的响应时间,减少GC时的停顿时间,适用于应用服务器、电信领域。

配置举例: java -Xmx3550m -Xms3550m -Xmn1g -Xss128k -XX:+UseConcMarkSweepGC -XX:UseParNewGC

  • -XX:UseConcMarkSweepGC :设置年老代为并发收集器。配置这个之后,-XX:NewRatio=4失效,原因不明。所以,此时年轻代最好通过-Xmn设置。
  • -XX:UseParNewGC :设置年轻代为并行收集。可与CMS收集器同时使用。JDK5以上,JVM会根据系统配置自行设置,所以无需再设置此值。

配置举例: java -Xmx3550m -Xms3550m -Xmn1g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

--XX:CMSFullGCsBeforeCompaction=5 :由于并发收集器不对内存进行压缩整理,所以运行一段时间后会产生内存碎片,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩整理。

  • -XX:+UseCmsCompactAtFullCollection :打开对年老代的压缩整理,可能会影响性能,但是可以消除内存碎片。

10.2.3.常见配置汇总

  • 堆设置
    • -Xms:堆初始大小
    • -Xmx:堆最大大小
    • -XX:NewSize=N:年轻代大小
    • -XX:NewRatio=N:年老代与年轻代的比例。
    • -XX:SurvivorRation=N:Eden区域一个Survivor的比例。
    • -XX:MaxPermSize:永久代大小
  • 收集器选择
    • -XX:+UseSerialGC:设置年轻代和年老代都为串行收集器。
    • -XX:+UseParallelGC:设置年轻代为并行收集器。
    • -XX:+UseParallelOlDGC:设置年老代为并行收集器。
    • -XX:+UseConcMarkSweepGC:设置年轻代为ParNew并行收集器,年老代为CMS并发收集器。
  • 并行收集器
    • -XX:ParallelGCThreads=N:设置并行收集使用的线程数量。
    • -XX:MaxGCPauseMillis=N:设置并行收集最大暂停时间
    • -XX:GCTimeRatio=N:设置垃圾回收所占的比例,公式为1/(1+N)。
  • 并发收集器
    • -XX:+CMSIncrementMode:设置为增量CMS模式,适用于单CPU的情况。
    • -XX:+ParalleGCThreads=N:设置年轻代为Parallel Scavenge并行收集器时,使用的CPU核数。
  • 垃圾回收信息统计
    • -XX:+PrintGC:开启GC打印。
    • -XX:+PrintGCDetails:打印GC详细信息。
    • -XX:+PrintGCTimeStamps:打印GC信息时,加上时间戳。
    • -Xloggc:filename:GC日志文件名。

返回顶部


最后

以上就是笑点低钻石为你收集整理的JVM结构、垃圾回收、常用参数与调优汇总的全部内容,希望文章能够帮你解决JVM结构、垃圾回收、常用参数与调优汇总所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(53)

评论列表共有 0 条评论

立即
投稿
返回
顶部