性能调优--JVM

JVM


运行时数据区域

程序计数器

指向当前线程正在执行的字节码指令的地址 和 行号

虚拟机栈

存储当前线程运行方法所需要的数据、指令、返回地址

局部变量表

操作数栈

动态链接


这个准确应该不叫常量池,不只有常量,还有类、方法、属性等的符号描述

接口可能有多个实现类,并且是可以去改变的,所以在真正执行方法的之后才会去解析这个接口类指向的实例,会去Constant pool中找到正确的实例,这是个动态的过程

返回

  • 正常的返回
  • 异常的抛出

本地方法栈

类似虚拟机栈,只不过本地方法是指native表示的方法,是指调用外部的实现的方法,比如jvm自己本地方法就是调用底层C实现的方法

方法区

永久代(PermGen)

存储:类信息、常量、静态变量、JIT

  • 类的基本信息:
    1. 每个类的全限定名
    2. 每个类的直接超类的全限定名(可约束类型转换)
    3. 该类是类还是接口
    4. 该类型的访问修饰符
    5. 直接超接口的全限定名的有序列表
  • 已装载类的详细信息
    1. 运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。
    2. 字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。
    3. 方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区。)
    4. 静态变量:类变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块。
    5. 到类classloader的引用:到该类的类装载器的引用。
    6. 到类class 的引用:虚拟机为每一个被装载的类型创建一个class实例,用来代表这个被装载的类。

元数据空间(Metaspace)

他们最大区别是:元空间并不在JVM中,而是使用本地内存

为什么要元数据空间

是因为这里面存储的是类的元数据信息

元数据(Meta Date)

关于数据的数据或者叫做用来描述数据的数据或者叫做信息的信息。这些定义都很是抽象,我们可以把元数据简单的理解成,最小的数据单位。元数据可以为数据说明其元素或属性(名称、大小、数据类型、等),或其结构(长度、字段、数据列),或其相关数据(位于何处、如何联系、拥有者)

永久代被MetaSpace取代的原因:

永久代和元数据空间都是方法区的实现。

  • 字符串常量存在永久代中,容易出现性能问题和内存溢出。
  • 增加GC负担,且回收率低,
  • 占用堆内存,且永久代空间无法进去预估,导致无法平衡三个分代
  • 最重要的原因:要合并HotSpot和JRockit的代码, JRockit从来没有一个叫永久代的东西, 但是运行良好, 也不需要开发运维人员设置这么一个永久代的大小.

堆Heap

为什么要有survivor

如果没有survivor区,把edn区中存活的直接放入老年代中,那么,老年代会被频繁塞满,会导致频繁fullGC。基于这种考虑,虚拟机引进了“幸存区”的概念:如果对象在某次新生代 gc 之后任然存活,让它暂时进入幸存区;以后每熬过一次 gc ,让对象的年龄+1,直到其年龄达到某个设定的值(比如15岁), JVM 认为它很有可能是个“老不死的”对象,再呆在幸存区没有必要(而且老是在两个幸存区之间反复地复制也需要消耗资源),才会把它转移到老年代。
总之,设置 Survivor 空间的目的是让那些中等寿命的对象尽量在 Minor GC 时被干掉,最终在总体上减少虚拟机的垃圾收集过程对用户程序的影响。

为什么新生代两个survivor

若只分一块Survivor,在清除Survivor区已死亡的对象时,因为此刻的Survivor区还有存活的对象,清除要比分两块Survivor麻烦,因为大部分也是要诶清除的,同时清除的话还要考虑数据碎片化的问题,而两块的情况,回收时只需将存活的对象移走,剩下的对象直接清理, 并且重新对存活对象进行整理,保证对象空间存储的连续性,避免碎片化。
好比,一个数组,把其中大部分都给删除,赋值为null,同时还要把剩下的重新分配下表,从头开始放,这种情况,直接使用新的数组直接按顺序存放,比直接操作当前数组来的方便,清晰

为什么新生代的空间比例是8:1:1

这个其实没有什么特殊的意义,是根据大量测试出来的结果,进行youngGC后大部分的对象都会被回收

为什么新生代和老年代的空间比例是1:2,为什么老年代比新生代大

  1. 新生代中的对象死的很快,所以没有必要吧空间设置的很大,进行一次minorGC后空间就会释放出来
  2. 因为使用的是复制回收算法,如果空间太大,会造成性能消耗

启动参数

-Xms starting, 堆起始大小
-Xmx max, 堆最大大小
-Xmn new, 堆新生代大小
JVM启动参数大全

GC - 垃圾回收

回收判断

引用

强引用
1
Object obj = new Object();

正常的new对象就是强引用
在可达性分析不可达的时候会被回收

软引用


比如去写缓存的时候可以去使用软引用

因为缓存可能是全局的,那么可能一直都不会被回收
当在内存不足的时候,在下一次GC就会被回收掉

弱引用

只要GC就会回收

虚引用

虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

引用计数法

问题:两个对象的相互循环引用,导致计数不准确,无法被回收

可达性分析

该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

在Java中,可作为GC Root的对象包括以下几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中JNI(即一般说的Native方法)引用的对象

为什么他们能够作为GC ROOT?

仔细看,可以看出这些数据都是我们文章开始说的,这些数据都是运行时数据区域中的数据,这些数据都是正在被使用的数据,所有可以作为GC ROOT

不可达是否就一定会被回收呢?

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  • 第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

  • 第二次标记

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

STW

STW(stop the world): 即挂起所有正在执行的业务线程

安全点

jvm是如何做到让业务线程都挂起的呢
在程序执行的时候并非所有时候都能停顿下来开始GC ,只有到达安全点才能暂停。安全点的选择标准:是否具有让程序长时间执行的特征,例如:方法调用,循环跳转,异常跳动等。具有这些功能的指令才会产生安全点SafePoint.
如何让GC发生的时候让所有线程都跑到”安全点“停下来。方式有两种:

  1. 抢先中断式(弃用):当发生GC时让所有线程都停下来,如果有线程没有到安全点,就让该线程继续跑到安全点
  2. 主动式抢断:当线程执行GC需要中断时,不需要对线程进行操作,而是设置一个标志位,各线程注定去轮询这个标志位。标志位和安全点在同一个位置,发现标志位为真是就将自己这个线程中断挂起,避免了抢占式:中断—启动—中断的过程

安全点可以保证大部分线程停顿,但是当GC请求中断时线程并没有获取CPU执行权,线程无法响应JVM”跑到“安全点,但是JVM的GC不会等待线程获得CPU执行权再对他进行可达性分析或者回收。也就是说该线程现在在非安全点停止(线程属于sleep或者blocked状态),并且GC对他进行了操作,这样的操作是不满足一致性的,当线程苏醒之后就会发生:线程在继续执行,GC也在操作该线程(这个过程对象的引用关系有可能会改变)

安全区

安全区就可以很好的解决这样的问题:是指在一段代码片段中,引用关系不会发生变化。在这个区域任何地方开始gc都是安全的。我们可以把saferegion 看成扩展版的 safepoint。

当线程执行到 saferegion 时,会标识自己进入了saferegion,当线程要离开saferegion时,就检查系统是否已经完成了根节点枚举(或者整个gc过程),如果完成了就继续执行,如果没有,就等到可以安全离开saferegion的信号为止。

回收算法

标记-清除算法


假设红色对象是已经被创建的对象,如果第一个对象需要被回收,那么GC就会变成右边的样子。

  • 优点:没啥优点
  • 缺点:效率不高(需要去定位哪些是占用的哪些不占用的)、空间碎片化严重

复制算法


有两块内存区域,两块区域交换使用

  • 优点:实现相对简单,没有空间碎片,之间是复制过来的
  • 缺点:浪费空间

标记-整理-清除算法


标记清除的升级版,在标记清除算法的基础上添加整理部分,避免空间碎片

  • 优点;空间利用率高,没有空间碎片
  • 缺点:

垃圾回收器

其实就是对以上回收算法的实现
查看当前使用的垃圾回收器

1
java -XX:+PrintCommandLineFlags -version


G1


G1收集器把堆分成多个区域,大小从1MB到32MB,并使用多个后台线程来扫描这些区域,优先会扫描最多垃圾的区域,这就是它名称的由来,垃圾优先Garbage First。

如果在后台线程完成扫描之前堆空间耗光的话,才会进行STW收集。它另外一个优点是它在处理的同时会整理压缩堆空间,相比CMS只会在完全STW收集的时候才会这么做。

jdk8上又对string做了优化,String对象和它内部使用的char[]数组会占用比较多的内存,因此优化过的G1收集器会把重复的String对象指向同一个char[]数组,避免多个副本存在在堆里。可以使用-XX:+UseStringDeduplication参数来打开这一功能。

个人认为他就是分区版的CMS + 空间压缩,解决的CMS大批量空间碎片的问题,因为分成了多个区域,所以把停顿的范围进一步缩小了。

新生代
Serial(串行)

复制回收算法,最老版本的收集器, 是单线程的,会触发STW, 停止所有的业务线程,执行GC线程。

  • 优点:低配置机器或者单线程环境使用性能较好
  • 缺点:单线程收集导致无法发挥出机器的性能,时间长
ParNew(串行)

复制回收算法,多线程版的serial
可以通过-xx:ParallelGCThreads来置顶GC的线程个数,默认值根据不同的平台是不一样的

Parallel Scavenge(并行)

复制回收算法
jdk7、jdk8 默认收集器
吞吐量 = 运行用户代码时间 / (运行用户代码 + 垃圾收集时间)
个人理解:serial和parnew的关注点只收对象回收,而该收集器关注的是一个吞吐量,达到一个可控的吞吐量,不仅仅为了垃圾回收,还要提交CPU使用率,我认为是人工智能版的ParNew
-XX:MaxGCPauseMillis (默认值有jvm给出最好的值)虚拟机将尽力保证每次MinorGC耗时不超过所设时长
-XX:GCTimeRatio 花费在GC上的时间占比
-XX:UseAdaptiveSizePolicy (默认是打开的)打开之后,就不需要设置新生代大小(-Xmn),Edian,survivor比例及(-XX:SurvivorRatio)晋升老年代年龄(-XX:PretenureSizeThreshold),虚拟机根据系统运行状况,调整停顿时间,吞吐量, GC自适应调节策略,区别parnew

老年代
CMS

标记-清除算法
为什么没有整理呢? 为了实现减少停顿时间
一种以获取最短回收停顿时间为目标的收集器。
CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:
(1)初始标记:独占PUC,仅标记GCroots能直接关联的对象
(2)并发标记:可以和用户线程并行执行,标记所有可达对象
(3)重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正
(4)并发清除:可以和用户线程并行执行,清理垃圾
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”
-XX:CMSFullGCsBeforeCompaction 执行多少次不压缩fullgc后来一次带压缩的。0 表示每次都压缩
-XX:UseConcMarkSweep 显示的使用CMS

  • 优点:相对parnew来说减少了停顿时间,并发
  • 缺点:并行标记和并行清除需要更多的线程去执行,同时业务线程也在执行,那么这个垃圾回收占用的cpu就要提高;同时垃圾回收线程和业务线程同时进行业务产生浮动垃圾;没有压缩整理算法,会产生大量碎片,可能导致FullGC,以致于停顿时间增加;

CMS 出现FullGC的原因:

  • 年轻带晋升到老年带没有足够的连续空间,很有可能是内存碎片导致的
  • 在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC
Serial Old

CMS 的备用预案,在并发收集发生Concurrent Mode Failure时使用
标记-整理-清除算法,单线程, serial的old版

Parallel Old

标记-整理-清除算法,Parallel Scvenge的old版

常用参数

不同的垃圾收集器他们各自都有优缺点,通常来说你需要根据你的业务,进行基于垃圾回收器的性能测试,然后再做选择。下面给出配置回收器时,经常使用的参数:

  • -XX:+UseSerialGC:在新生代和老年代使用串行收集器
  • -XX:+UseParNewGC:在新生代使用并行收集器
  • -XX:+UseParallelGC :新生代使用并行回收收集器,更加关注吞吐量
  • -XX:+UseParallelOldGC:老年代使用并行回收收集器
  • -XX:ParallelGCThreads:设置用于垃圾回收的线程数
  • -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
  • -XX:ParallelCMSThreads:设定CMS的线程数量
  • -XX:+UseG1GC:启用G1垃圾回收器
新生代 (别名)老年代 (别名)JVM 参数
Serial (DefNew)Serial Old(PSOldGen)-XX:+UseSerialGC
Parallel Scavenge (PSYoungGen)Serial Old(PSOldGen)-XX:+UseParallelGC
Parallel Scavenge (PSYoungGen)Parallel Old (ParOldGen)-XX:+UseParallelOldGC
ParNew (ParNew)Serial Old(PSOldGen)-XX:-UseParNewGC
ParNew (ParNew)CMS+Serial Old(PSOldGen)-XX:+UseConcMarkSweepGC
G1G1-XX:+UseG1GC

栈上分配

对那些作用于不会逃逸出方法的对象(线程私有,不是多线程共享),在分配内存时,不在将对象分配在堆内存中,而是将对象属性打散后分配在线程私有栈内存上,这样随着方法调用结束,栈上分配打散的对象也被回收掉,不在增加 GC 额外压力。

栈上分配有什么好处

不需要GC介入去回收这个对象,出栈即释放资源,可以提高性能,原理:由于我们GC每次回收对象的时候,都会触发Stop The World(停止世界),这时候所有线程都停止了,然后我们的GC去进行垃圾回收,如果对象频繁创建在我们的堆中,也就意味这我们也要频繁的暂停所有线程,这对于用户无非是非常影响体验的,栈上分配就是为了减少垃圾回收的次数。

TLAB

new对象

1
2
3
4
public void dome(){
User user=new user();
user.sayhi();
}

java中我们要创建一个对象,用关键字new就可以了。但是,在我们日常中,有很多生命周期很短的对象.
这种对象的作用域都不会逃逸出方法外,也就是说该对象的生命周期会随着方法的调用开始而开始,方法的调用结束而结束。
假设JVM所有的对象都放在堆内存中(为什么用假设,因为JVM并不是这样)一旦方法结束,没有了指向该对象的引用,该对象就需要被GC回收,如果存在很多这样的情况,对GC来说压力山大呀。

指针碰撞

假设JVM虚拟机上,堆内存都是规整的。堆内存被一个指针一分为二。指针的左边都被塞满了对象,指针的右变是未使用的区域。每一次有新的对象创建,指针就会向右移动一个对象size的距离。这就被称为指针碰撞。

问题来了。如果我们多线程执行刚才那个dome方法,一个线程正在给A对象分配内存,指针还没有来的及修改,其它为B对象分配内存的线程,而且还是引用这之前的指针指向。这样就出现毛病了。

TLAB的出现

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

TLAB的本质其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。从这一点看,它被翻译为 线程私有分配区 更为合理一点
当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

TLAB的缺点

TLAB也又自己的缺点。因为TLAB通常很小,所以放不下大对象。

  1. TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)
  2. TLAB空间还剩一点点没有用到,有点舍不得。(比如100kb的TLAB,装了80KB,又来了个30KB的对象)
    所以JVM开发人员做了以下处理,设置了最大浪费空间。
    1. 当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间。进行对象创建,还是空间不够,那你这个对象太大了,去Eden区直接创建吧!
    2. 当剩余的空间大于最大浪费空间,那这个大对象请你直接去Eden区创建,我TLAB放不下没有使用完的空间。
  3. Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC.
  4. TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理。

MinorGC 和 MajorGC 和 FullGC

minorGC (youngGC)

新生代的一个GC

大对象如何处理

如果对象很大,超过eden或者survivor,怎么办?
设置:-XX:PretenureSizeThreshold=3145728(这个值得单位是Byte)
如果超过这个大小,直接进去老年代

长期存活的对象

设置:-XX:MaxTenuringThreshold=15
这个就是新生代中的存活对象的最大年龄,超过这个年龄就直接进去老年代,每经理一次GC增加1岁年龄

动态对象年龄判定

如果相同年龄所有对象的大小总和 > survivor空间的一半
那么这些对象会直接进入到老年代

空间分配担保机制

首先会判断老年代是否有足够的空间

  1. 老年代最大可用连续空间是否 > 新生代所有对象总空间
  2. 老年代最大可用连续空间是否 > 历代晋升的平均大小
  3. 决定在MinorGC前是否需要FullGC

MajorGC (oldGC)

老年代的一个GC

FullGC (MinorGC + MajorGC)

新生代GC + 老年代GC

什么时候回触发FullGC
  1. 统计得到的YoungGC晋升老年代的平均大小 > 老年代剩余空间
  2. 主动触发(执行 jmap -histo:live [pid])
  3. PermGen 空间不足(jdk1.8之前)
  4. CMS GC时出现promo failed 和 concurrent mode failure(concurrent mode failure发生的原因一般是CMS正在运行,但是由于,需要尽快回收老年代里面不在被使用的对象,这是停止所有的线程,同时终止CMS,直接进行Serial Old GC);

实践

常用JDK自带的监控工具

官网文档地址
直接搜索命令

jstatck 线程数据查询

jstatck [pid] > 文件路径和文件名









一般情况下要关注:DeadLock 和 Blocked 这两种情况

jstat gc数据查询

常用命令
jstat -gcutil [pid] 实时更新时间:GC数据统计
jstat -gccause [pid] 实时更新时间:GC原因统计

jmap 内存数据查询

常用命令

jvisualvm/jconsole

命令的图形化展示

添加GC日志输出

1
2
3
4
5
6
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径

查看GC垃圾回收器

命令行

1
2
-XX:+PrintCommandLineFlags 打印java启动命令
-XX:+PrintFlagsFinal 将启动命令会在Catalina.log文件中打印出来

代码

查看GC日志

MinorGC

日志结构图:

例如:

FullGC(MinorGC + MajorGC + MataSpace)


什么时候会触发:Ergonomics , 比如图上的Full GC (Ergonomics)?
会在使用了CMS垃圾收集器并且老年代内存长期占用一定的比例并且无法释放掉的时候,同时开启了UseAdaptiveSizePolicy参数,所以jvm自己进行自适应调整引发的full gc。

MAT(Memory Analyzer Tool)

官网文档
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintHeapAtGC
-XX:HeapDumpPath=/file/path/name.hprof

参数基本策略

参考文章