JVM
1 为什么需要JVM,不要JVM可以吗?
-
JVM可以帮助我们屏蔽底层的操作系统 一次编译,到处运行
-
JVM可以运行Class文件
2 JDK,JRE以及JVM的关系
3 我们的编译器到底干了什么事?
仅仅是将我们的 .java 文件转换成了 .class 文件,实际上就是文件格式的转换,对等信息转换。
4 类加载器的层次
由图可知,上一层加载器都会以父类的形式传入下一次加载。
5 双亲委派机制
当类加载进⾏加载类的时候,类的加载需要向上委托给上⼀级的类加载器,上⼀级继续向上委托,直到启动类加载器。启动类加载器去核⼼类库中找,如果没有该类则向下委派,由下⼀级扩展类加载器去扩展类库中,如果也没有继续向下委派,直到找不到为⽌,则报类找不到的异常。
双亲委派机制的流程如下:
6 如何打破双亲委派
7 运行时数据区
运⾏时数据区也就是JVM在运行时产生的数据存放的区域,这块区域就是JVM的内存区域,也称为JVM的内存模型——JMM(Java Memory Model)
JMM分成了这么几个部分
-
堆空间(线程共享):存放new出来的对象
-
元空间(线程共享):存放类元信息、类的模版、常量池、静态部分
-
线程栈(线程独享):⽅法的栈帧
-
本地方法栈(线程独享):本地方法产生的数据
-
程序计数器(线程独享):配合执⾏引擎来执行指令
**注:**堆、元空间(含方法区)是线程共享的,线程栈、本地⽅法栈、程序计数器是独享的。
堆空间
Java虚拟机有一个在所有Java虚拟机线程之间共享区域为堆
,堆是为所有类实例和数组分配内存的运行时数据区域,它是在虚拟机启动的时候创建的。
Java堆是虚拟机所管理的内存中最大的一块,我们常说的垃圾回收操作的区域就是堆。堆是为所有类实例和数组分配内存的运行时数据区域.
如果是普通对象并且是局部变量,那么在局部变量表
(下面会讲述)中存放的只是对象的引用,也就是存储的是对象的地址,实例还是存放在堆区。
堆可以是固定大小的,也可以根据计算的需要进行扩展,如果不需要更大的堆,则可以收缩。堆的内存在物理上不需要是连续的,逻辑上是连续的即可,可通过参
数-Xms(设置堆内存初始值或最小值)和-Xmx(设置堆内存最大值)来对堆内存大小进行扩展。
Java虚拟机实现可以让程序员或用户控制堆的初始大小,如果堆可以动态扩展或收缩,还可以控制最大和最小堆大小。以下异常情况与堆相关联:
- 如果对象没有足够的内存去分配的话,Java虚拟机会抛出一个OutOfMemoryError。
元空间(方法区)
方法区是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
- 方法区是各个线程共享的内存区域,在虚拟机启动时创建
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
线程栈
线程栈描述的是Java方法执行的线程内存模型,也被称为虚拟机堆栈:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧
用于存储局部变量表
、操作数栈
、动态连接
、方法出口
等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。虚拟机栈是一个栈结构,属于后进先出(FILO)的数据结构。
JVM规范允许Java线程栈具有固定大小或根据计算要求动态扩展和收缩。如果Java线程栈具有固定大小,则每个Java虚拟机堆栈的大小可以在创建堆栈时独立选择。
Java虚拟机实现可以让程序员或用户控制Java虚拟机堆栈的初始大小,以及在动态扩展或收缩Java线程栈的情况下,控制最大和最小大小。
以下异常情况与 Java 线程栈相关:
- 如果线程中的计算需要比允许的更大的Java线程栈,则Java虚拟机会抛出一个StackOverflowError。
- 如果Java线程栈可以动态扩展,并且尝试进行扩展,但没有足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始Java线程栈,则Java Virtual机器抛出一个OutOfMemoryError。
线程栈和栈帧及栈帧内部结构图如下:
总结:
-
虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
-
每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
-
图解栈和栈帧
void a(){ b(); } void b(){ c(); } void c(){ }
局部变量表
栈帧包含一个变量数组,称为局部变量表。局部变量表顾名思义就是局部变量的表,局部变量表存放着8大基本类型、对象引用和returnAddress类型。
操作数栈
每个栈帧都包含一个后进先出 (LIFO) 栈,称为其操作数栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。操作数栈上的每个条目都可以保存任何Java虚拟机类型的值,操作数栈中的值必须以适合其类型的方式进行操作,例如整数加法的字节码指令iadd。它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。
操作数栈本质上是JVM执行引擎的一个工作区,也就是方法在执行时才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,控制下一条指令执行什么。由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响, 独立存储, 我们称这类内存区域为“线程私有”的内存。
因为JVM内部有完整的指令与执行的一套流程,所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native方法),这个方法不是JVM来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址,所以在执行native方法时,JVM 中程序计数器的值为空(Undefined)。
另外程序计数器也是JVM中唯一不会 OOM(OutOfMemory)的内存区域。
返回地址
方法调用完成分为两种方式:
- 方法正常调用完成,即遇到方法返回的字节码指令
- 方法遇见异常,并且这个异常没有在方法体内得到处理
方法正常调用完成:如果直接从Java虚拟机或作为执行显式语句的结果,该调用不会导致抛出异常,则该方法调用正常完成。如果当前方法的调用正常完成,则可能会向调用方法返回一个值。这发生在被调用的方法执行返回指令之一时,选择的返回指令必须适合返回值的类型(如果有)。在这种情况下,当前帧栈用于恢复调用者的状态,包括其局部变量和操作数栈,调用者的程序计数器会适当增加以跳过方法调用指令。然后在调用方法的帧中正常继续执行,并将返回值(如果有)推送到该帧栈的操作数栈中。
方法异常调用完成:如果在方法内执行Java虚拟机指令导致Java虚拟机抛出异常,并且该异常不在方法内处理,则方法调用会突然完成。执行athrow指令 也会导致显式抛出异常,如果当前方法未捕获到异常,则会导致方法调用突然完成。突然完成的方法调用永远不会向其调用者返回值。
方法完成的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:
- 恢复上层方法的局部变量表和操作数栈
- 把返回值(如果有的话)压入调用者栈帧的操作数栈中
- 调整PC计数器的值以指向方法调用指令后面的一条指令等
栈的优化技术——栈帧之间数据的共享
在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的 JVM 在实现中会进行一些优化,使得两个栈帧出现一部分重叠(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了。
7.2 程序在执行时运行时数据区中的内存变化及解析
-
线程栈:执行一个方法就会在线程栈中创建一个栈帧。
-
栈帧包含如下四个内容:
- 局部变量表:存放方法中的局部变量。
-
操作数栈:⽤来存放方法中要操作的数据。
- 动态链接:存放方法名和方法内容的映射关系,通过方法名找到方法内容。方法的内容是存在堆里的。
- 方法出口:记录方法执行完后调用此方法的位置。
8 简述一下对象创建过程
一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。
9 内存分区情况
10 为什么需要Survivor区?只有Eden不行吗?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发Full GC。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
执行时间长有什么坏处?
频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。因为Full GC过程中会伴随事件较长的Stop the world。
可能你会说,那就对老年代的空间进行增加或者较少咯。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长。
假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
11 为什么需要两个survivor区
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。
所以两个区,可以在一个区将碎片整理成连续的,再移至另一个。
12 新生代中Eden:S1:S2为什么是8:1:1?
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的。即绝大部分都会在Eden区被回收。
13 堆内存中都是线程共享的区域吗?
JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
14 如何确定一个对象是垃圾
在堆空间和元空间中,GC这条守护线程会对这些空间开展垃圾回收⼯作,那么GC如何判断这些空间的对象是否是垃圾,有两种算法引用计数法
和可达性分析算法
:
6.3.1 引用计数法
每个对象有个引用计数器属性,记录被引用的情况。对象被引⽤,则计数器+1,如果计数器是0,那么对象将被判定为是垃圾,于是被回收。
这种算法,实现简单,判定效率高。但缺点是需要需要单独的字段存储计数器,每次赋值都需要更新计数器,增加了空间和时间的开销。缺最严重的是无法解决循环依赖问题。因此JVM⽬前的主流⼚商Hotspot没有使用这种算法。
注:什么是循环依赖问题?
如下图所示,p引用了A,对象A间接引用了C,C又引用了A。方法执行完p不再需要引用A,但A和C的引用没有消失,引用计数器还都是1,不会被回收。
6.3.2 可达性分析算法:GC Roots根
该算法的基本思想就是:
通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC
Roots没有任何引用链相连,就说明该对象不可达,即可以被回收。
哪些会被认定为GC Roots根呢?
- 方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。 - 方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。 - 方法栈中栈帧局部变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的局部变量表中。只要方法还在运行,还没出栈,就意味这局部变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。 - 本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。 - 被同步锁持有的对象
被synchronized
锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了。
五种变量的位置如下:
静态变量会在方法区中存一个引用,市级指向堆内存,局部变量表也是如此。
总结
可达性分析就是JVM首先枚举根节点,找到一些为了保证程序能正常运行所必须要存活的对象,然后以这些对象为根,根据引用关系开始向下搜寻,存在直接或间接引用链的对象就存活,不存在引用链的对象就回收。
GC再扫描堆空间的某个节点时,会向上遍历,看看能不能遍历到gc roots根节点,如果不能,那么意味着这个对象是垃圾。
例如下图,对象4、5、6都没有和GC Root根节点相连,会被判定为垃圾回收。
15 什么时候会垃圾回收
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。但是不建议手动调用该方法,因为GC消耗的资源比较大。
具体什么时候回收,采用哪种回收算法及回收器。
16 垃圾回收算法
已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?得要有对应的算法,
下面介绍常见的垃圾回收算法。
有四种垃圾回收算法:
- 标记清除算法
- 复制算法
- 标记整理算法
- 分代回收算法
7.1 标记清除算法
- 标记:遍历内存区域,对需要回收的对象打上标记。
- 清除:再次遍历内存,对已经标记过的内存进行回收。
缺点:
- 效率问题:遍历了两次内存空间(第一次标记,第二次清除)。
- 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。
7.2 复制算法
将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,或经过一定时间,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。
有点:
- 相对于标记–清理算法解决了内存的碎片化问题。
- 效率更高(清理内存时,记住首尾地址,一次性抹掉)。
缺点:
内存利用率不高,每次只能使用一半内存,浪费空间。
7.3 标记整理算法
因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。
- 标记:对需要回收的进行标记
- 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。
如下图可以看到整理后对象都集中在一起,腾出连续的空间。
优点:不会产生碎片
缺点:每次标记再前移效率偏低
7.4 分代回收算法(最常用)
分代回收算法本质是上述各算法的结合优化,当前大多商用虚拟机都采用这种分代回收算法。其实大多数对象生命周期非常短,所以在发生GC时,需要回收的对象特别多,存活的特别少,因此需要搬移到另一块内存的对象非常少,所以不需要1:1划分内存空间。而是将整个空间划分为新生代
(1/3)和老年代
(2/3)。新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区
,较小的两块分别称为To Survivor
和From Survivor
(幸存者区或存活区)。
首次GC时,只需要将Eden存活的对象复制到To Survivor
。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到Form Survivor
,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。
但不能保证每次存活的对象就永远少于新生代整体的10%,有可能复制过去存不下,所以会有老年代作最后担保,若还不够就会抛出OOM。
堆空间的结构及详细回收流程如下:
-
堆空间被分成了新⽣代(1/3)和⽼年代(2/3),新⽣代中被分成了eden(8/10)、survivor1(1/10)、survivor2(1/10)
-
对象的创建在
Eden
,如果放不下则触发minor gc
。(minor gc时,会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作)。 -
对象经过⼀次
minor GC
后存活的对象会被放⼊到survivor区,并且年龄+1。如果再次执行minor GC
,会统一挪到另一个survivor区。如果复制时存不下,则进入老年代。 -
当survivor区中对象年龄到达
15
,进⼊到⽼年代。 -
如果⽼年代内存都满了。会先尝试触发
minor gc
,再触发Full GC
。Full GC
执行过程中,STW的时间更长(因为老年代的存活数量比较多)。 -
如果老年代满了且没有可回收的垃圾,会报
Out Of memory
。
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行
这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常
可以打开jdk自带的监视器查看内存分配情况:cmd窗口执行jvisualvm
流程图如下:
注:不是只有幸存区对象年龄超过15才进入老年代,还存在很多其他复杂情况。
7.4.1 对象进入老年代的条件
-
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,直接进入老年代:⼤对象可以通过参数设置大小,多⼤的对象被认为是⼤对象。
-XX:PretenureSizeThreshold
-
当对象的年龄到达15岁时将进⼊到⽼年代,这个年龄可以通过这个参数设置:``-XX:MaxTenuringThreshold`
-
根据对象动态年龄判断,如果s区中的对象总和超过了s区中的50%,那么下⼀次做复制的时候,把年龄⼤于等于这次最⼤年龄的对象都⼀次性全部放⼊到⽼年代。
-
⽼年代空间分配担保机制 :在
minor gc
时,检查⽼年代剩余可⽤空间是否⼤于新生代⾥现有的所有对象(包含垃圾)。如果⼤于等于,则做minor gc
。如果小于,看下是否配置了担保参数的配置:-XX: -HandlePromotionFailure
,如果配置了担保,那么判断⽼年代剩余的空间是否⼩于历史每次minor gc
后进⼊⽼年代的对象的平均⼤⼩。如果是,则直接full gc
,减少⼀次minor gc
。如果不是,执行minor gc
。如果没有担保机制,直接full gc
。**解释:**说白了就是要判断下老年代的剩余空间,是否还能承受的住下一次新生代的对象存入老年代。
17 有哪些垃圾回收器
那么谁来负责回收垃圾呢?
17.1 Serial收集器
-XX:+UseSerialGC -XX:+UseSerialOldGC
单线程执行垃圾收集,收集过程中会有较长的STW(stop the world),在GC时⼯作线程不能⼯作。虽然STW较长,但简单、直接。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
由于Serial垃圾回收器是单线程的,因此它的优点是简单且占用资源较少;它适用于小型应用程序,例如移动应用程序和桌面应用程序。
17.2 Parallel收集器
-XX:+UseParallelGC,-XX:+UseParallelOldGC
使⽤多线程并行进行GC,会充分利用cpu,但是依然会有stw,这是jdk8默认使⽤的新⽣代和⽼年代的垃圾收集器。充分利用CPU资源,吞吐量高。
新生代采用复制算法,⽼年代采⽤标记-整理算法。
Parallel垃圾回收器适用于中等大小的应用程序,特别是那些需要高吞吐量的应用程序,例如: Web应用程序和大规模企业应用程
序。
17.3 ParNew收集器
-XX:+UseParNewGC
⼯作原理和Parallel收集器⼀样,都是使用多线程进行GC,但是区别在于ParNew收集器可以和CMS收集器配合⼯作。主流的方案:
ParNew收集器负责收集新生代,CMS负责收集老年代。
17.4 CMS收集器
-XX:+UseConcMarkSweepGC
⽬标:尽量减少stw的时间,提升⽤户的体验。真正做到gc线程和⽤户线程⼏乎同时⼯作。CMS采⽤标记-清除算法。
此处标记的是有用的对象。
-
初始标记:暂停所有的其他线程(STW),并记录gc roots直接能引⽤的对象。
例:线程栈帧的局部变量表中有个引用指向堆空间对象A,堆空间变量又引用了另一个对象B,则只记录A,不算B。
-
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较⻓但是不需要STW,可以与应用线程⼀起并发运⾏。这个过程中,⽤户线程和GC线程并发,可能会有导致已经标记过的对象状态发⽣改变,所以下一步需要
重新标记
最后一句话是说会造成标记的遗漏:
-
重新标记:为了修正并发标记期间因为⽤户程序继续运⾏而导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远比并发标记阶段时间短。主要⽤到三色标记里的算法做重新标记。
-
并发清理:开启⽤户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。(最终被标记为白色的都是垃圾)。
-
并发重置:重置本次GC过程中的标记数据。
总结:
- 这几步中,最费时间的是并发清理,所以采用了并发处理
- 初始标记和重新标记两处采用STW形式,因为标记的速度很快
- 重新标记采用STW模式,因为是最后一步标记,要确保标记到的必须都是用到的
17.5 G1回收器
用于大对象的回收,且jdk8版本对G1不是很完整。
18 吞吐量和停顿时间是什么
-
停顿时间:垃圾收集器 进行垃圾回收终端应用执行响应的时间
-
吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
小结:这两个指标也是评价垃圾回收器好处的标准。
19 如何选择合适的垃圾收集器
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
- 如果允许停顿时间超过1秒,选择并行或JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
20 对于G1收集
JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代。
21 设置参数的常见方式
-
开发工具中设置比如IDEA,eclipse
-
运行jar包的时候:java -XX:+UseG1GC xxx.jar
-
web容器比如tomcat,可以在脚本中的进行设置
-
通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)
22 常用参数含义
参数 | 含义 | 说明 |
---|---|---|
-XX:CICompilerCount=3 | 最大并行编译数 | 如果设置大于1,虽然编译速度会提高,但是同样影响系统稳定性,会增加JVM崩溃的可能 |
-XX:InitialHeapSize=100M | 初始化堆大小 | 简写-Xms100M |
-XX:MaxHeapSize=100M | 最大堆大小 | 简写-Xms100M |
-XX:NewSize=20M | 设置年轻代的大小 | |
-XX:MaxNewSize=50M | 年轻代最大大小 | |
-XX:OldSize=50M | 设置老年代大小 | |
-XX:MetaspaceSize=50M | 设置方法区大小 | |
-XX:MaxMetaspaceSize=50M | 方法区最大大小 | |
-XX:+UseParallelGC | 使用UseParallelGC | 新生代,吞吐量优先 |
-XX:+UseParallelOldGC | 使用UseParallelOldGC | 老年代,吞吐量优先 |
-XX:+UseConcMarkSweepGC | 使用CMS | 老年代,停顿时间优先 |
-XX:+UseG1GC | 使用G1GC | 新生代,老年代,停顿时间优先 |
-XX:NewRatio | 新老生代的比值 | 比如-XX:Ratio=4,则表示新生代:老年代=1:4,也就是新生代占整个堆内存的1/5 |
-XX:SurvivorRatio | 两个S区和Eden区的比值 | 比如-XX:SurvivorRatio=8,也就是(S0+S1):Eden=2:8,也就是一个S占整个新生代的1/10 |
-XX:+HeapDumpOnOutOfMemoryError | 启动堆内存溢出打印 | 当JVM堆内存发生溢出时,也就是OOM,自动生成dump文件 |
-XX:HeapDumpPath=heap.hprof | 指定堆内存溢出打印目录 | 表示在当前目录生成一个heap.hprof文件 |
-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-XX:+PrintGCDateStamps-Xloggc:g1-gc.log | 打印出GC日志 | 可以使用不同的垃圾收集器,对比查看GC情况 |
-Xss128k | 设置每个线程的堆栈大小 | 经验值是3000-5000最佳 |
-XX:MaxTenuringThreshold=6 | 提升年老代的最大临界值 | 默认值为15 |
-XX:InitiatingHeapOccupancyPercent | 启动并发GC周期时堆内存使用占比 | G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比值为0则表示”一直执行GC循环”默认值为45 |
-XX:G1HeapWastePercent | 允许的浪费堆空间的占比 | 默认是10%,如果并发标记可回收的空间小于10%,则不会触发MixedGC |
-XX:MaxGCPauseMillis=200ms | G1最大停顿时间 | 暂停时间不能太小,太小的话就会导致出现G1跟不上垃圾产生的速度。最终退化成FullGC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。 |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量 | 默认值随JVM运行的平台不同而不同 |
-XX:G1MixedGCLiveThresholdPercent=65 | 混合垃圾回收周期中要包括的旧区域设置占用率阈值 | 默认占用率为65% |
-XX:G1MixedGCCountTarget=8 | 设置标记周期完成后,对存活数据上限为G1MixedGCLIveThresholdPercent的旧区域执行混合垃圾回收的目标次数 | 默认8次混合垃圾回收,混合回收的目标是要控制在此目标次数以内 |
-XX:G1OldCSetRegionThresholdPercent=1 | 描述MixedGC时,OldRegion被加入到CSet中 | 默认情况下,G1只把10%的OldRegion加入到CSet中 |
23 性能优化
JVM的性能优化可以分为代码层面和非代码层面。
在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。
在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。
注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身已经做了很多的内部优化操作。
那今天我们就从内存、gc以及cpu这3个方面和大家一起探讨一下JVM的优化,但是大家要注意的是不要为了调优和调优。
24 内存溢出的原因有哪些?通用解决方案有哪些?
一般会有两个原因:
(1)大并发情况下
(2)内存泄露导致内存溢出
-
大并发
浏览器缓存、本地缓存、验证码
CDN静态资源服务器
集群+负载均衡
动静态资源分离、限流[基于令牌桶、漏桶算法]
应用级别缓存、接口防刷限流、队列、Tomcat性能优化
异步消息中间件
Redis热点数据对象缓存
分布式锁、数据库锁
5分钟之内没有支付,取消订单、恢复库存等
25 什么时候会用G1
(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
26 G1调优
27 G2调优实践
-
不要手动设置新生代和老年代的大小,只要设置整个堆的大小
G1收集器在运行过程中,会自己调整新生代和老年代的大小。其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标,如果手动设置了大小就意味着放弃了G1的自动调优。
-
不断调优暂停时间目标
一般情况下这个值设置到
100ms
或者200ms
都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。 -
使用-XX:ConcGCThreads=n来增加标记线程的数量
IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高ConcGCThreads。
-
MixedGC调优
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarger
-XX:G1OldCSetRegionThresholdPercent
-
适当增加堆内存大小(常用)
-
不正常的Full GC
有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是由Metaspace区域引起的。可以通过MetaspaceSize适当增加其大家,比如256M。
调优指南
28 内存泄漏与内存溢出的区别
内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。
29 young gc会有stw吗?
不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时间相对来说就小很多了。
30 major gc和full gc的区别
Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触发 Full GC 的原因有很多:
- 当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时,会触发 Full GC
- 当老年代的空间使用率超过某阈值时,会触发 Full GC
- 当元空间不足时(JDK1.7永久代不足),也会触发 Full GC
- 当调用 System.gc() 也会安排一次 Full GC
31 什么是直接内存
Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
32 垃圾判断的方式
引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。
引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。
33 不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
34 为什么要区分新生代和老年代?
当前虚拟机的垃圾收集都采用
分代收集算法
,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代
中,每次收集都会有大量对象死去,所以可以选择复制算法
,只需要付出少量对象
的复制成本就可以完成每次垃圾收集。而老年代
的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”
或“标记-整理”
算法进行垃圾收集。
35 G1与CMS的区别是什么
CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 MixGC;G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。
36 方法区中的无用类怎么回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。
类需要同时满足下面 3 个条件才能算是 “无用的类” :
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。