JVM定义:

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
是java程序实现跨平台的⼀个重要的⼯具
在这里插入图片描述

栈、本地方法栈、程序计数器不会发生gc。
jvm调优主要在堆,方法区有一小部分。

常见的几种jvm

  • HotSpot (我们一般使用的)

  • JRockit BEA

  • J9 vm IBM

JDK,JRE,JVM区别

在这里插入图片描述
说明:

三者关系: JDK > JRE > JVM

类加载过程

⼀个类被加载进JVM中要经历哪⼏个过程

加载: 通过io流的⽅式把字节码⽂件读⼊到jvm中(⽅法区)
校验:通过校验字节码⽂件的头8位的16进制是否是cafebabe
准备:为类中的静态部分开辟空间并赋初始化值
解析:将符号引⽤转换成直接引⽤。——静态链接
初始化:为类中的静态部分赋指定值并执⾏静态代码块。
类被加载后,类中的类型信息、⽅法信息、属性信息、运⾏时常量池、类加载器的引⽤等信息会被加载到元空间中。

类加载器

作用

加载.class文件

新建的对象放入堆里面,引用(地址)放到栈,其中引用指向堆里面对应的对象。
在这里插入图片描述

加载器分类

1)虚拟机自带的加载器
2)启动类(根)加载器 Bootstrap ClassLoader
3)扩展类加载器 Extension ClassLoader
4)应用程序(系统类)加载器 Application ClassLoader

  • Bootstrap ClassLoader 启动类加载器:负载加载jre/lib下的核⼼类库中的类,⽐如rt.jar、charsets.jar
  • ExtClassLoader 扩展类加载器:负载加载jre/lib下的ext⽬录内的类
  • AppClassLoader 应⽤类加载器:负载加载⽤户⾃⼰写的类
  • ⾃定义类加载器:⾃⼰定义的类加载器,可以打破双亲委派机制

双亲委派机制

检查顺序从下至上,加载顺序从上到下。

如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载,直到找不到为⽌,则报类找不到的异常。

在这里插入图片描述

好处

可以避免重复加载,父类已经加载了,子类就不需要再次加载

更加安全,防⽌核⼼类库中的类被随意篡改,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患

全盘委托机制

当⼀个类被当前的ClassLoader加载时,该类中的其他类也会被当前该ClassLoader加载。除⾮指明其他由其他类加载器加载。

对象的创建流程

在这里插入图片描述

类加载校验

校验该类是否已被加载。主要是检查常量池中是否存在该类的类元信息。如果没有,则需要进⾏加载。

分配内存

为对象分配内存。具体的分配策略如下:

  • Bump the Pointer(指针碰撞):如果内存空间的分配是绝对规整的,则JVM记录当前剩余内存的指针,在已⽤内存分配
  • Free List(空闲列表):如果内存空间的分配不规整,那么JVM会维护⼀个可⽤内存空间的列表⽤于分配。

对象并发分配存在的问题:

  • Compare And Swap: ⾃旋分配,如果并发分配失败则重试分配之后的地址
  • Thread Local Allocation Buffer(TLAB):本地线程分配缓冲,JVM被每个线程分配⼀块空间,每个线程在⾃⼰的空间中创建对象(jdk8默认使⽤,之前版本需要通过-XX:+UseTLAB开启)

设置初值

根据数据类型,为对象空间赋初始化值。

设置对象头

为对象设置对象头信息,对象头信息包含以下内容:类元信息、对象哈希码、对象年龄、锁状态标志等
在这里插入图片描述
对象填充必须是8字节的整数,如果对象头+实例数据没有达到8字节的数据,就要用对象填充到8字节

对象头中的Mark Word 字段(32位)

在这里插入图片描述

对象头中的类型指针(Klass Pointer)

类型指针⽤于指向元空间当前类的类元信息。⽐如调⽤类中的⽅法,通过类型指针找到元空间中的该类,再找到相应的⽅法。
开启指针压缩后,类型指针只⽤4个字节存储,否则需要8个字节存储

指针压缩

过⼤的对象地址,会占⽤更⼤的带宽和增加GC的压⼒
对象中指向其他对象所使⽤的指针:8字节被压缩成4字节,在保存时⽤4个字节,再使⽤时使⽤8个字节,这样8个字节的对象,实际上使⽤32位来保存,这样64位就能表示2个对象。
如果内存⼤于32G,指针压缩会失效,会强制使⽤64位来表示对象地址。因此jvm堆内存最好不要⼤于32G。
Jdk1.6之后默认开启指针压缩,可通过配置jvm参数关闭指针要锁 -XX:-UseCompressedOops

执⾏init⽅法

为对象中的属性赋值和执⾏构造⽅法

运行时数据区

在这里插入图片描述

在这里插入图片描述

  • 堆空间(线程共享):存放new出来的对象
  • 元空间(线程共享):存放类元信息、类的模版、常量池、静态部分
  • 线程栈(线程独享):⽅法的栈帧
  • 本地⽅法区(线程独享):本地⽅法产⽣的数据
  • 程序计数器(线程独享):配合执⾏引擎来执⾏指令

程序计数器

Program Counter Register 程序计数器(寄存器)

每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计

作用

是记住下一条jvm指令的执行地址

特点

是线程私有的
不会存在内存溢出

特点及作用

1、程序计数器是一块较小的内存空间,几乎可以忽略;
2、是当前线程所执行的字节码的行号指示器;
3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;
4、该区域是“线程私有”的内存,每个线程独立存储;
5、该区域不存在OutOfMemoryError;
6、无GC回收

本地方法栈 Native Method Stack

它的具体做法是Native Method Stack中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。[本地库]

Native

native :凡是带了native关键字的,说明java的作用范围达不到了,回去调用底层c语言的库
会进入本地方法栈 去调用本地方法接口将native方法引入执行
调用本地方法本地接口 JNI (Java Native Interface)
JNI作用:开拓Java的使用,融合不同的编程语言为Java所用
Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
private native void start0();

方法区 Method Area

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;  静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

在这里插入图片描述

方法区内存溢出

1.8 以前会导致永久代内存溢出

  • 永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
  • -XX:MaxPermSize=8m

1.8 之后会导致元空间内存溢出

  • 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
  • -XX:MaxMetaspaceSize=8m

运行时常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实地址

String s1 = "a";
		String s2 = "b";
		String s3 = "a" + "b";
		String s4 = s1 + s2;
		String s5 = "ab";
		String s6 = s4.intern();
		// 问
		System.out.println(s3 == s4);	//false
		System.out.println(s3 == s5);	//true
		System.out.println(s3 == s6);	//true
		
		
		
		String x2 = new String("c") + new String("d");
		String x1 = "cd";
		x2.intern();//将这个字符串对象尝试放入字符串常量池中,如果有则不会放入,如果没有则放入字符串常量池,会把常量池中的对象返回
		
		// 问,如果调换了【最后两行代码】的位置呢
		System.out.println(x1 == x2); //false
String x2 = new String("c") + new String("d");
		x2.intern();//将这个字符串对象尝试放入字符串常量池中,如果有则不会放入,如果没有则放入字符串常量池,会把常量池中的对象返回
		String x1 = "cd";
		
		
		// 问,如果调换了【最后两行代码】的位置呢
		System.out.println(x1 == x2); //true

字符串常量池

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串
    池中的对象返回
    1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,
    放入串池, 会把串池中的对象返回
字符串常量池位置

jdk8之前,在永久代的常量池中
jdk8,在堆内存中

字符串常量池调优
  • 调整 -XX:StringTableSize=桶个数

因为字符串常量池本质上也是hash表,数组加链表,增加数组长度,可以减少链表长度,增加效率

  • 考虑将字符串对象是否入池

栈stack

线程栈:一个线程就是一个线程栈执⾏⼀个⽅法就会在线程栈中创建⼀个栈帧。
栈:先进后出,栈内存主管程序的运行,生命周期和线程同步,线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就结束.

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

栈帧包含如下四个内容:

  • 局部变量表:存放⽅法中的局部变量
  • 操作数栈:⽤来存放⽅法中要操作的数据
  • 动态链接:存放⽅法名和⽅法内容的映射关系,通过⽅法名找到⽅法内容
  • ⽅法出⼝:记录⽅法执⾏完后调⽤次⽅法的位置。

栈内存中运行:8大基本类型+对象引用+实例的方法.
栈运行原理:栈桢

栈满了:StackOverflowError

  • 栈帧过多导致栈内存溢出 (递归无出口)
  • 栈帧过大导致栈内存溢出

垃圾回收是否涉及栈内存?

不涉及,栈内存在每次方法调用完后都会被弹出栈,都会被自动回收,不需要垃圾回收

栈内存分配越大越好吗?

不是,因为物理内存的大小是一定的,如果栈内存变大,那么线程数量就会变少,效率不一定变高

方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全(例如,作为方法的参数,和方法的返回值)

线程运行诊断

案例1: cpu 占用过多
  1. 定位用top定位哪个进程对cpu的占用过高
  2. ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  3. jstack 进程id
    可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

它是线程共享的,堆中对象都需要考虑线程安全的问题

一个JVM只有一个堆内存,堆内存的大小是可以调节的.类加载器读取类文件后,一般会把类,方法,常量,变量,保存我们所有引用类型的真实对象.

堆内存诊断

  1. jps 工具
    查看当前系统中有哪些 java 进程
  2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测

堆内存细分为三个区域:

新生区(伊甸园区):Young/New
养老区old
永久区Perm
在这里插入图片描述

新生区

目的:控制对象的诞生,成长和死亡

分为:

伊甸园区:所有对象都在伊甸园区new出来

幸存0去和幸存1区:轻GC之后存下来的

老年区

永久存在的对象放在老年区,真理:经过研究,99%的对象都是临时对象!

步骤:

当伊甸园区满了之后进行轻GC幸存下来的放到幸存0区或幸存1区
当伊甸园区,幸存0区和幸存1区都满了进行重GC,幸存下来的放到养老区
当伊甸园区,幸存0区和幸存1区和养老区都满了,会出现OOM

永久区(元空间)

元空间使用的是直接内存,与新生代和老年代分开。
在这里插入图片描述

堆内存调优

OOM:

  1. 尝试扩大堆内存看结果
  2. 分析内存,看一下哪个地方出了问题(专业工具)
    Xms1024m Xmx1024m -XX:+PrintGCDetails
  3. 在一个项目中,突然出现了OOM故障,那么该如何排除?研究为什么出错
    jprofiler作用:
    1)分析dump内存文件,快速定位内存泄露
    2)获得堆中的数据
    3)获得大的对象

垃圾回收机制GC

在这里插入图片描述
Eden与Survivor的内存大小比例为8:1:1

GC算法

  1. 引用计数法(Java没有采用)

  2. 根可达算法

  3. 标记-清除法 (jvm老年代回收)

  4. 标记-压缩法 (jvm老年代回收)

  5. 复制算法 (jvm新生代回收)

引用计数法

原理:实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数+1,如果删除对该对象的引用,那么它的引用计数就-1,当该对象的引用计数为0时,那么该对象就会被回收。

GC的时候会将计数器为0的对象C给销毁.

引用计数法无法解决循环引用的问题

循环依赖问题:
A a = new A()
B b = new B()
a.x=b
b.x=a
a=null
b=null
很难判断 然后 怎么去标记为0 去回收

根搜索算法(可达性分析算法)

根搜索算法。它的处理方式就是,设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的。

在这里插入图片描述
ObjectD和ObjectE是互相关联的,但是由于GC roots到这两个对象不可达,所以最终D和E还是会被当做GC的对象,上图若是采用引用计数法,则A-E五个对象都不会被回收。

在这里插入图片描述

说到GC roots(GC根),在JAVA语言中,可以当做GC roots的对象有以下几种:

1、虚拟机栈中的引用的对象。
2、方法区中的类静态属性引用的对象。
3、方法区中的常量引用的对象。
4、本地方法栈中JNI的引用的对象。
第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。

复制算法

在这里插入图片描述
GC 复制算法是利用 From 空间进行分配的。当 From 空间被完全占满时,GC 会将存活
对象全部复制到 To 空间,并且年龄加一。当复制完成后,该算法会把 From 空间和 To 空间互换,GC 也就结束了。From 空间和 To 空间大小必须一致。这是为了保证能把 From 空间中的所有活动对象
都收纳到 To 空间里

在这里插入图片描述
在这里插入图片描述

  • 不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC)

  • 幸存区from和幸存区to中谁空谁是to,我们会将to中的数据复制到from中保持to中数据为空;

  • from和to区实际上为逻辑上的概念,保证to区一直空;

  • 默认对象经过15次GC后还没有被销毁就会进入养老区

流程:

将Eden区进行GC存活对象放入空的to区,将from区存活的放到空的to区

此时from区为空变成了to区,to区有数据变为from区

经过15次GCfrom区还存活的对象会被移动到养老区

好处:没有内存碎片
坏处:浪费内存空间,多了一半to空间永远是空的。
复制算法最佳使用场景:对象存活度较低的时候 -> 新生区 (如果存活度较高,则from区空间全部被占满导致会将全部内容复制到to区)

标记清除算法

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。

它的做法是当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

需要两次扫描,第一次扫描标记存活对象,第二次扫描清除没有被标记的对象
在这里插入图片描述
优点:不需要额外的空间

缺点:两次扫描严重浪费时间,并且还会产生内存碎片,(内存碎片会导致明明有空间,但是无法存储大对象)

标记-整理

在这里插入图片描述

标记整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记;但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间。

在这里插入图片描述
优点 不会产生内存碎片
缺点 效率低

总结
  • 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
  • 内存整齐度:复制算法=标记压缩算法>标记清除算法
  • 内存利用率:标记压缩算法=标记清除算法>复制算法
  • 没有最好的算法,只有最合适的算法 GC:分代收集算法
分代收集算法

在这里插入图片描述
堆空间被分成了新⽣代(1/3)和⽼年代(2/3),新⽣代中被分成了eden(8/10)、
survivor1(1/10)、survivor2(1/10)

对象首先分配在伊甸园区域
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的
对象年龄加 1并且交换 from to
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时
间更长

一般是把Java堆分为新生代和老年代:短命对象归为新生代,长命对象归为老年代。

少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。
大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。
注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。

对象进入到老年代的条件
  1. ⼤对象直接进⼊到⽼年代:⼤对象可以通过参数设置⼤⼩,多⼤的对象被认为是⼤对象。
    -XX:PretenureSizeThreshold
  2. 当对象的年龄到达15岁时将进⼊到⽼年代,这个年龄可以通过这个参数设置:
    -XX:MaxTenuringThreshold
  3. 根据对象动态年龄判断,如果s区中的对象总和超过了s区中的50%,那么下⼀次做复制的时候,把年龄⼤于等于这次年龄的对象都⼀次性全部放⼊到⽼年代。
  4. ⽼年代空间分配担保机制 :在minor gc时,检查⽼年代剩余可⽤空间是否⼤于年轻代⾥现有的所有对象(包含垃圾)。如果⼤于等于,则做minor gc。如果⼩于,看下是否配置了担保参数的配置:-XX: -HandlePromotionFailure ,如果配置了,那么判断⽼年代剩余的空间是否⼩于历史每次minor gc 后进⼊⽼年代的对象的平均⼤⼩。如果是,则直接full gc,减少⼀次minor gc。如果不是,执⾏minor gc。如果没有担保机制,直接fullgc

在这里插入图片描述

对象中的finalize⽅法

Object类中有⼀个finalize⽅法,也就是说任何⼀个对象都有finalize⽅法。这个⽅法是对象被回收之前的最后⼀根救命稻草。

  • GC在垃圾对象回收之前,先标记垃圾对象,被标记的对象的finalize⽅法将被调⽤
  • 调⽤finalize⽅法如果对象被引⽤,那么第⼆次标记该对象,被标记的对象将移除出即将被回收的集合,继续存活
  • 调⽤finalize⽅法如果对象没有被引⽤,那么将会被回收
  • 注意,finalize⽅法只会被调⽤⼀次

对象的逃逸分析

在jdk1.7之前,对象的创建都是在堆空间中创建,但是会有个问题,⽅法中的未被外部访问的对象

public void test1() {
  User user = new User();
  user.setId(1);
  user.setName("xiaoming");
}
public User test2() {
 User user = new User();
 user.setId(1);
 user.setName("xiaoming");
 return user;
}

这种对象没有被外部访问,且在堆空间上频繁创建,当⽅法结束,需要被gc,浪费了性能。所以在1.7之后,就会进⾏⼀次逃逸分析(默认开启),于是这样的对象就直接在栈上创建,随着⽅法的出栈⽽被销毁,不需要进⾏gc。

在栈上分配内存的时候:会把聚合量替换成标量,来减少栈空间的开销,也为了防⽌栈上没有⾜够连续的空间直接存放对象。

  • 标量:java中的基本数据类型(不可再分)
  • 聚合量:引⽤数据类型。

标量就是不可分割的量,java中基本数据类型是标量。相对的一个数据可以继续分解,它就是聚合量(aggregate)。
如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换。
如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那么程序真正执行的时候将可能不创建这个对象,而改为直接在>栈上创建若干个成员变量

实例对象是怎样存储的

实例对象存放在堆区,对实例的引用存在线程栈上,而实例的元数据存在方法区或者元空间,如果实例对象没有发生线程逃逸行为,就会被存储在线程栈中

垃圾回收器

Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

单线程执⾏垃圾收集,收集过程中会有较⻓的STW(stop the world),在GC时⼯作线程不能⼯作。虽然STW较⻓,但简单、直接。

在这里插入图片描述

Parallel收集器(-XX:+UseParallelGC,-XX:+UseParallelOldGC)

使⽤多线程进⾏GC,会充分利⽤cpu,但是依然会有stw,这是jdk8默认使⽤的新⽣代和⽼年代的垃圾收集器。充分利⽤CPU资源,吞吐量⾼
在这里插入图片描述

ParNew收集器(-XX:+UseParNewGC)

⼯作原理和Parallel收集器⼀样,都是使⽤多线程进⾏GC,但是区别在于ParNew收集器可以
和CMS收集器配合⼯作。主流的⽅案:
ParNew收集器负责收集新⽣代。CMS负责收集⽼年代
在这里插入图片描述

CMS收集器(-XX:+UseConcMarkSweepGC)

⽬标:尽量减少stw的时间,提升⽤户的体验。真正做到gc线程和⽤户线程⼏乎同时⼯作。
CMS采⽤标记-清除算法。

  • 初始标记: 暂停所有的其他线程(STW),并记录gc roots直接能引⽤的对象。
  • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较⻓但是不需要STW,可以与垃圾收集线程⼀起并发运⾏。这个过程中,⽤户线程和GC线程并发,可能会有导致已经标记过的对象状态发⽣改变。
  • 重新标记:为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。主要⽤到三⾊标记⾥的算法做重新标记。
  • 并发清理:开启⽤户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为⿊⾊不做任何处理。
  • 并发重置:重置本次GC过程中的标记数据。
    在这里插入图片描述

三⾊标记算法

在并发标记阶段,对象的状态可能发⽣改变,GC在进⾏可达性分析算法分析对象时,⽤三⾊来标识对象的状态

  • ⿊⾊:这个对象及其所有引⽤都已被GC Roots遍历,⿊⾊的对象不会被回收
  • 灰⾊:这个对象被GC Roots遍历过但其部分的引⽤没有被GC Roots遍历。在重新标记时
    重新遍历灰⾊对象。
  • ⽩⾊:这个对象没有被GC Roots遍历过。在重新标记时该对象如果是⽩⾊的话,那么将
    会被回收

垃圾收集器组合⽅案

不同的垃圾收集器可以组合使⽤,在使⽤时选择适合当前业务场景的组合
在这里插入图片描述

JVM调优实战

JVM调优的核⼼参数

  • -Xss:每个线程的栈⼤⼩。设置越⼩,说明⼀个线程栈⾥能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多
  • -Xms:设置堆的初始可⽤⼤⼩,默认物理内存的1/64
  • -Xmx:设置堆的最⼤可⽤⼤⼩,默认物理内存的1/4
  • -Xmn:新⽣代⼤⼩
  • -XX:NewRatio:默认2表示新⽣代占年⽼代的1/2,占整个堆内存的1/3 (堆空间老年代占三分之二,新生代占三分之一)
  • -XX:SurvivorRatio:默认8表示⼀个survivor区占⽤1/8的Eden内存,即1/10的新⽣代内存(Eden占新生代十分之八,from和to两个survivor区各占十分之一)

以下两个参数设置元空间⼤⼩建议值相同,且写死,防⽌在程序启动时因为需要元空间的空间不够⽽频繁full gc

  • -XX:MaxMetaspaceSize:最⼤元空间⼤⼩
  • -XX:MetaspaceSize:元空间⼤⼩,默认是21M,达到该值后会触发Full GC,同时会按100%进⾏动态调整(扩大到原来两倍),为了减少⼤数据量占满元空间,频繁触发Full GC,建议在初始化时设置为跟MaxMetaspaceSize相同的值

在这里插入图片描述

Xms3072MXmx3072MXss1M ‐XX:MetaspaceSize=256M
‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8

在这里插入图片描述

-Xms3072MXmx3072MXmn2048MXss1M ‐XX:MetaspaceSize=256M
‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8

在这里插入图片描述

调优的关键点

  • 设置元空间⼤⼩,最⼤值和初始化值相同
  • 根据业务场景计算出每秒产⽣多少的对象。这些对象间隔多⻓时间会成为垃圾(⼀般根据接⼝响应时间来判断)
  • 计算出堆中新⽣代中eden、survivor所需要的⼤⼩:根据上⼀条每条产⽣的对象和多少时间成为垃圾来计算出,依据是尽量减少full gc

结合垃圾收集器的调优策略

结合垃圾收集器:PraNew+CMS,对于CMS的垃圾收集器,还需要加上相关的配置:

  • 对于⼀些年龄较⼤的bean,⽐如缓存对象、spring相关的容器对象,配置相关的对象,这些对象需要尽快的进⼊到⽼年代,因此需要配置:-XX:MaxTenuringThreshold=5
  • ⼤对象直接进⼊到⽼年代:-XX:PretenureSizeThreshold=1M
  • CMS垃圾收集器会有并发模式失败的⻛险(转换为使⽤serialOld垃圾收集器),如何避免这种⻛险:将full gc的触发点调低:
    -XX:CMSInitiatingOccupancyFraction=85 (默认是92),相当于⽼年代使⽤率达到85%就触发full gc,于是还剩15%的空间允许在cms进⾏gc的过程中产⽣新的对象
  • CMS垃圾收集器收集完后会产⽣碎⽚,碎⽚需要整理,但不是每次收集完就整理,设置做了3次Full GC之后整理⼀次碎⽚
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3

PraNew+CMS的具体JVM参数配置

java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -
XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -
XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=85 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=3 -jar device-service.jar
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐