技术自查第一篇:JVM调优入门篇

12 篇文章 0 订阅

前言

JVM是Java项目运行的基础,但实际上对它了解知之甚少。一个网站的吞吐量和响应速度,其实都跟JVM有重大关系的,有时候增加多几台机子,还不如优化JVM更实际,当然有钱额外另说。

说到JVM调优就肯定跟垃圾回收(GC)有关,所以首先回顾下GC的发展史。

GC的发展史

  1. JDK1.3 推出串行垃圾回收器(Serial GC)
  2. JDK1.4 推出并行垃圾回收器(ParallerGC)和并发垃圾回收器(CMS)
  3. JDK1.6 默认并行垃圾回收器
  4. JDK1.7 推出G1回收器
  5. JDK1.9 默认G1回收器
  6. JDK10 G1的并行完整垃圾回收,实现并行改善最坏情况的延迟
  7. JDK11 引入Espilon GC ,又成为"No-Op无操作"回收器,同时推出ZGC垃圾回收器
  8. JDK12 增加G1 自动返回未使用的堆内存给系统,同时推出Shenandoah GC
  9. JDK13 增强ZGC,自动返回未使用堆内存给系统
  10. JDK14 删除CMS,扩展ZGC在mac和window使用

总结:G1垃圾回收器在JDK1.7就推出,截止JDK14,还在不断完善,相对比目前的ZGC来说,还是很成熟的垃圾回收器,更别说Espilon GC在JDK11推出之后,Espilon GC就没什么动静了。而且如果你有了解过以上全部垃圾回收器的算法,G1简直是个宝剑。

JVM调优其实就是看菜吃饭,假如内存足够大,JVM其实不调优也行,但实际上,内存都是有限制的,所以JVM调优,还需要先了解对象的大小。

对象的大小

总结:一个空对象的占用的空间大小是16Byte,原因是什么,先了解基本类型空间大小

基本类型占用空间

基本数据类型占用空间大小(byte)
byte1
short2
int4
long8
float4
doublt8
boolean1

对象的构成

一个空对象

Object o = new Object();

空对象组成 

  • 对象头(8byte)
  •  类对象指针(4byte)
  • 实例数据(0byte)

空对象:8+4=12 byte -> 16 byte

从上面可以看出空对象是12byte,但实际上为什么是16byte。

原因:涉及到计算机信息存储规范,当占用空间不是8的整数倍时,该存储信息就会自动补零,方便存储和获取。

额外知识点

数组的占用空间大小

数组的组成

  • 对象头(8)
  • 类对象指针(4)
  • 数组长度
  • 实例数据
object[] array = {};

8+4=12 byte -> 16 byte

int[] array = {1,2,3,4};

8 + 4 + 4*4= 28byte - > 32 byte

示例

以下对象占用空间大小

class AAAAA {

}
8+4=12->16

class BBBBB {
  int a = 1; 
}
8+4+4=16
int是基本数据类型,故直接算4

class CCCCC {
  long a = 1l;
}
8+4+8=20->24
long是基本数据类型,故直接算8

class DDDDD {
   String s = "Hello";
}
8+4+4=16
String是引用类型,故算4

栈(Stack)和堆(Heap)

JVM分为栈(Stack)和堆(Heap)-请记住英文单词

图1:栈和堆

 为什么JVM把内存分成栈和堆?

原因:

  1.  分而治之,专门的事,专门的人负责,栈负责程序的运行,堆负责数据的存储(但不可以认为栈不能存储数据,基础数据类型是放到栈的)
  2. 堆是负责数据的存储,那么一些共享的数据都可以放到堆,方便栈调用。
  3. 栈因为是程序运行基础,需要保存系统上下文,所以进行了地址段划分,只能向上增长,限制了栈的存储能力。而堆不同,堆里的对象会因为程序的运行,变成动态增长,而栈只需要保存对象在堆的地址(对象的引用指针/对象),当栈程序运行时需要该对象,使用该对象引用地址,即可获取该对象内容
  4. 栈也可以保存数据,但保存的是基本数据类型,占用的数据空间大小是固定,并不会出现动态增加,如果对象保存在栈就会容易造成栈内存溢出(java.lang.StackOverflowError)

常见的内存报错

栈溢出:java.lang.StackOverflowError

堆溢出:java.lang.OutOfMemoryError: Java heap space 

持久区溢出:java.lang.OutOfMemoryError: PermGen space

GC为释放很小的空间占用大量的时间:java.lang.OutOfMemoryError:GCoverheadlimitexceeded

基本类型已经说完了,那么下面说引用类型

引用类型

引用类型分为:强引用>软引用>弱引用>虚引用

强引用:指栈程序运行时,对象是必不可少的时候,JVM虚拟机不会回收的对象。打个比喻,我们的生活必需品,例如衣服,手机,这些都是必不可少的,我们肯定不会回收掉,但当衣服,手机坏了(可以理解为服务器宕机了,必需品都坏了,不宕机才怪),我们就会扔掉,回收掉。

// 堆生成对象o,同时在栈创建了一个对象引用值,占用4byte
private Object o ;
....
// 栈运行程序,引用对象o,这个就是强引用
public void demo(Object o){
    syso(o.toString());
}

软引用:当内存不足时,才会回收的对象(注意:垃圾回收和内存不足是两个概念,软引用是即使被垃圾回收,也不一定会被回收的)

软引用又涉及到一个java类:SoftReference类,使用方法

// 软引用一般配合SoftReference类使用
String s = "123";
SoftReference<String> softRef = new SoftReference<String>(s);
以上写法等于下面
//当内存不足,启动垃圾回收时
if(内存不足){
    s = null;//转成软引用
    System.gc();// 垃圾回收,清楚软引用
}

弱引用:垃圾回收时,回收的对象。

同样,也涉及到一个java类:WeekReference类,使用方法

String s = "123";
WeakReference<String> softRef = new WeakReference<String>(s);
System.out.println(sr.get());
System.gc();                //手工模拟JVM的gc进行垃圾回收
System.out.println(sr.get());
//输出为
123
null

虚引用:用于跟踪对象被回收的活动的对象

同理也涉及到一个java类:PhantomReference类,使用方法:

Object obj = new Object();
ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
PhantomReference<Object> pf = new PhantomReference<Object>(obj,rq);
obj=null;
System.out.println(pf.get());//永远返回null
System.out.println(pf.isEnqueued());//返回是否从内存中已经删除
System.gc();
TimeUnit.SECONDS.sleep(6);
System.out.println(pf.isEnqueued());
//打印结果
null
false
true

总结:如下图

基本数据类型、引用数据类型、对象的组成和计算方式都说完,接下来说下GC的算法

GC的算法

  1. 引用计数法
  2. 标记清除法
  3. 复制法
  4. 标记整理法

引用计算法

顾名思义,对象的引用每被引用一次,增加一次计数,当进行GC时,只会删除计数为0的对象

标记清除法

分两个阶段,第一阶段:遍历所有对象,标记被引用的对象,第二阶段,遍历整个堆,会停止整个应用进行GC,清除未被标记的对象,缺点:会出现内存碎片(原因:没有对内存进行压缩整理)。

复制法

把内存分为两个区,两个区一模一样,同一时间仅使用某个区,当进行GC,会遍历整个堆内存,把正在使用的对象复制到另一个区,同时对另一个进行整理压缩,优点:故不会产生内存碎片。缺点:需要的内存空间较大,两个区一模一样

标记复制法

就是标记清除法和复制法,集合在一起,取两者优点。第一阶段遍历对象,标记被引用的对象,第二遍,复制移动被引用的对象,且对内存进行整理压缩。优点:不需要较大内存,不需要停止应用进行GC,不会产生内存碎片。

按分区划分

  1. 增量收集:实时回收垃圾,即不停止应用进行垃圾回收
  2. 分区收集:把生命周期不同的对象划分到不同的区,年轻区,年老区和持久区。不同的区使用不同的算法。JDK1.9之前都是使用该分区收集,JDK1.9包含1.9默认使用G1垃圾回收算法

在理解按线程回收区分和收集器区分两个概念前,麻烦先搞清楚并行和并发

  1. 并行:两件或多件事情在同一时间点/某一刻发送,注意是时间点/某一刻。打个比方,你在吃饭的时候,有电话打来,你一个人只有一张嘴,而这个时间点,你只能选择用嘴讲电话或者吃饭。
  2. 并发:两件或多件事情在同一段时段发送,注意是一段时间。打个比方,你在吃饭的时候,有电话打来,每一个人只有一张嘴,而这个时间段,你可以边吃饭边接电话,或者先吃饭后接电话,或先接电话再吃饭,但每一个时间点,你只能用嘴做一件事。

按线程回收

线程分为用户线程和垃圾回收线程

  1. 串行收集:使用单条垃圾回收线程回收垃圾,所有用户线程处于等待状态(SWT情况)
  2. 并行收集:使用多条垃圾回收线程并行处理,所有用户线程处于等待状态(SWT情况)
  3. 并发收集:垃圾回收线程和用户线程同时进行,他们运行在不同的CPU上(并不一定是并行,有可能是交替,不会造成SWT情况)

按收集器

1.串行收集器

使用单线程回收垃圾,会暂停应用进行GC,简称:SWT

使用场景:数据量少的应用,且对响应时间无要求的应用

开启方式:-XX:UseSerialGC

 从上图可知,串行垃圾回收器使用的标记清除法算法,但会压缩整理内存,故不会产生内存碎片

2.并行收集器

使用多线程回收垃圾,会暂停应用进行GC

使用场景:吞吐量高,且对响应时间无要求的应用,简称:SWT

开启方式:-XX:UseParalleGC

额外补充:

-XX:UseOldParalleGC:年老区开启并行收集(JDK1.6之后才可以使用)

-XX:MaxGCPauseMillies=n:最大垃圾回收暂停时间,单位毫秒

-XX:MaxParalleThreads=n:并行垃圾回收线程,n一般等于处理器数量(CPU)

-XX:GCTimeRatio=n:设置垃圾回收时间与非垃圾回收时间比例(程序正常运行时间),公式是:1/(1+n),例如,n=19,那么时间占比是5%

 上图可知,并行收集器使用的标记清楚算法,但同时也使用了串行线程来执行,但1.6之后,可以开启并行线程处理

3.并发收集器

可以保证运行大部分时间都是正常工作,垃圾回收时间只是占用很低。且不会暂停应用进行GC。但有缺点(下面再说)

使用场景:对响应时间有要求的应用

开启方式:

-XX:UseConcMarkSweep:开启并发收集器(CMS),主要针对年老区,进行内存压缩整理

额外补充:

-XX:MaxGCPauseMillis,同上

-XX:CMSFullGCBefore=n ,设置FullGC后,对年老代进行压缩整理

-XX:CMSInitiatingOccupancyFraction,开启剩余多少堆内存,开启并发收集

-XX:+CMSIncreamentModel:设置为增量模式,适合单CPU。

缺点,造成浮点垃圾

它的优点正是它产生缺点的原因:由于它不会暂停应用回收垃圾,所以应用是边正常运行边进行垃圾回收,这段时间内肯定会产生新的垃圾,但前一刻垃圾回收已进行完毕,这部分新的垃圾只能等到下次垃圾回收进行回收。这些垃圾称为浮点垃圾。

上图可知,并行垃圾回收器,内部其实也是使用着并行线程,但它并发的处理了垃圾回收,但没有整理压缩内存。

4.G1垃圾回收器

G1垃圾回收器是JDK1.7推出,JDK1.9默认成为垃圾回收器

存在目的是:

        1.更简单配置JVM,只需要通过是三个设置

                1. 开启G1回收器:-XX:Use G1

                2. 设置堆内存大小:-Xms -Xmx

                3. 配置垃圾回收停顿时间:-XX:MaxGCPauseMillils           

        2. 优化垃圾回收器

  概念:取消了年轻区、年老区和元空区的物理划分,相对应的把堆内存划分成一个个区(Region),每个区默认值512K,每个区有可能是年老区,年轻区,元空区或者巨型区。同时每个区都会存在一个RSet集合。(两个重要单词,区(Region)和集合(RSet))

巨型区(Humongous)简称H区

有时候对象占用的空间过大,占用该区的50%以上,当GC时,该对象会直接复制到年老区,但如果该对象生命周期短,那么也会对年老区造成负面影响(占用空间大,容易使年老区空间满,生命周期短,又需要多次进行GC),所以为了解决该问题,就创建一个H区,专门存放这些对象。

如下图

RSet集合

每个区(Region)创建的时候,同时会划分成多个Card,同时会对应生成一个RSet的集合,上文说过,对象里通常会含有另一个对象,该对象所在Region区,为了跟踪记录其他区(Region)被引用的对象,这是用就要使用RSet,记录被引用对象的地址,记录形式为:XXRegion的XXCard,如下图

三个垃圾回收模式

 G1垃圾回收器又分为三个垃圾回收模式

1. Young GC

2. Mixed GC

3. FULL GC

Young GC

G1的年轻区的垃圾回收还是会暂停应用,实现垃圾回收,它会把存回的对象复制到Survivor区或者年老区(类似复制法,但不需要两个相同的内存区,不会产生内存碎片)

年老区的垃圾回收采用并发收集器,采用标记整理法(会产生内存碎片)。

 Mixed GC

当越来越多对象升级到年老区,年老区的占比达到45%(默认值),为了避免堆内存被耗尽和频繁调用FULL GC,就产生改回收模式,该回收模式,除了回收年轻区还会回收部分年老区,注意是部分,不是全部

开启方式:-XX:InitatingHeapOccupancyPerent=n

FULL GC

没啥好说的了,就是停止整个应用,扫描整个堆,标记清除所有垃圾

额外补充:

-XX:UseG1GC 使用G1GC

-XX:MaxGCPauseMillis,GC回收时间的设置,默认是200毫秒

-XX:G1HeapRegionSize=n 设置G1区域大小,值是2的幂数,范围在1M到32M之间,默认是堆内存的1/2000

-XX:ParalleGCThreads=n

-XX:ConcGCTreads=n 设置并行标记的线程数,n设置为并行垃圾回收线程数(ParallerTreads)的1/4

-XX:InitiatingHeapOccupanyPerent=n 设置触发标记周期的java堆占用阈值,默认占堆45%

垃圾回收算法,垃圾回收线程,垃圾收集器三个概念已学习完毕,那么这三者怎么串联起来呢

简单总结:不同的垃圾收集器使用不同的垃圾回收线程在不同的区使用不同的垃圾回收算法 

上面这句话很绕口,配合下面表格就可以很简单的理解:

回收器名称能回收器类型算法作用位置(区)特点使用场景
SerialGC串行垃圾回收器复制法年轻区相应速度快适合单核的客户端应用程序(已被淘汰)
SerialOldGC串行垃圾回收器标记清除法年老区响应速度快适合单核的客户端的应用程序(已被淘汰)
ParNew并行垃圾回收器复制法年轻区响应速度快适合后端多CPU,默认搭配CMS一起使用
ParalleGC并行垃圾回收器复制法年轻区吞吐量高适合后端多CPU,堆内存不是很大的应用程序
ParalleOldGC并行垃圾回收法标记清除法年老区吞吐量高适合后端多CPU,堆内存不是很大的应用程序
CMS并发垃圾回收器标记清除法年老区响应速度快大型企业应用
G1并行/并发垃圾回收器复制法/标记整理法年轻区/年老区响应速度快大型企业应用

垃圾回收器的搭配使用

基本准则

年轻区与年老区:串行配串行,并行配串行,并行配并行,并行配并发,G1很特殊

如下图

问题集锦 

如何区分垃圾?

无法被引用的对象就被当做垃圾进行回收。

怎么寻找垃圾?

上文也说过,栈是程序运行的地方,堆是存储数据的地方,要寻找垃圾(无法被引用的对象)当然要从栈开始,以栈中的对象为根节点,逐步查询被引用的对象,如果被引用的对象也含有其他被引用的对象,以这个被引用的对象为枝节点,继续查询被引用的对象,如此循环.....如下图

 内存碎片?

上文提到过标记整理法会造成内存碎片,看下图,堆存放数据的空间不一定要摆放在上一个数据的隔壁,是根据数据存储的规范而存储数据的。这样就会造成垃圾回收完毕后,剩余的堆空间不足以摆放其他对象(两个数据之间不能存放数据)。

 如何解决同时解决垃圾回收和创建对象的问题?

垃圾回收是为了让回收内存,创建对象是分配内存的,两者是矛盾的,最好的解决办法。创建对象时,停止垃圾回收或者垃圾回收时,禁止创建对象。例如:引用计数法,标记清除法。

为什么分代?

堆内存分为:年轻代,年老代和持久代。每个对象的生命周期都不一致,生命周期短的对象可以直接在年轻代被回收,这样就可以减少垃圾回收的次数。

为什么对象占用空间一定是8的倍数?

根据计算机存储信息规范,为了方便读取和存储数据,当数据信息占用空间不是8的倍数时,会自动补零。(https://www.zhihu.com/question/375626478

什么是内存碎片?

内存碎片分为:

内部碎片:已分配给当前线程的内存,存在当前线程不能使用的内存

外部碎片:未分配给任何线程的内存大小,不足以供任意线程使用

堆内存碎片:同理内部碎片,当堆剩余内存空间且没有整理压缩过,导致不足以存放对象。(https://blog.csdn.net/qq_43012792/article/details/107501545

线程与进程的关系?

打个比喻,进程好比火车,线程好比火车的每节车厢,一个进程可以有多个线程。

但每个进程是互不影响的,但线程之间可以互相影响的。火车之间是不会影响到他们的行驶路线的,但每节火车之间的食物、饮料和服务员是可以共享的。

为什么说程序运行可以没有堆,但不能没有栈?

说白了堆只是存储数据的地方,栈其实也可以保存数据,但如果保存数据量大的数据就会严重影响程序的运行,所以就把内存分成栈和堆

施工完毕,下一篇:JVM调优案例篇

参考文章

  1. JVM调优总结https://www.cnblogs.com/andy-zhou/p/5327288.html#_caption_0

  2. JVM垃圾回收器的发展历程及使用场景汇总:https://www.jianshu.com/p/9916ae406f56

  3. JVM 调优实战--垃圾收集器(串行、ParNew并行、ParallelNew并行、CMS、G1):https://zhangxueliang.blog.csdn.net/article/details/104004153

额外知识点

不同的JDK区的划分和叫法不同

堆内存:年轻区、年老区、持久区

JDK1.8和1.8之后

堆内存:年轻区、年老区、元空区

G1之后

堆内存:划分成一个个region区,每个区都有可能是年轻区,年老区,元空区和巨型区

传统GC回收流程

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值