JVM调优与垃圾回收器详解

这张是jdk8的jvm模型:
黄色框的是线程共享区域、蓝色框的是线程私有(也就是每个线程单独一份)
jvm模型从大的角度说有:类装载子系统、字节码执行引擎、运行时数据区。我这里主要讲运行时数据区。
一、JVM内存模型
1、名词解释
虚拟机栈
描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储 局部变量、操作数栈、动态链接、方法出口等信息.生命周期与线程相同。栈里面存放着各种基本数据类型和对象的引用
本地方法栈:
本地方法栈则为虚拟机使用到的Native方法服务(非java代码的接口,比如C++的方法:Runtime.getRuntime().exec()是执行shell脚本的命令)
程序计数器:
当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响
堆:
其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。包含:新生代(Eden区、S0、S1)、老年代。官方推荐配置为年轻代大小占整个堆的3/8。-XX:NewRatio=3/5表示新生代和老年代的比值
方法区(元空间):
存储已被虚拟机加载的类信息。jdk8的JVM不再有永久代(PermGen),原永久代存储的信息被分成两部分: 1、虚拟机加载的类信息(放在元空间) 2、运行时常量池(放在堆中)
元空间和方法区:第一个是hotspot的具体实现技术,第二个是JVM规范的抽象定义,不能说元数据区就是方法区,但可以说元空间用来实现了方法区
2、jvm参数类型有
①标配参数
例如:java -version/java -help /java -showversion
②x参数
java -Xint解释执行(java -Xint -version)、java -Xcomp第一次使用就编译成本地代码、java -Xmixed混合模式
③XX参数
-XX:+或者-某个属性值,其中+表示开启、-表示关闭 例如:jinfo -flag PrintGCDetails 进程号 ===>>>如果出现-XX:-PrintGCDetails 代表关闭了GC回收参数配置,如果是-XX:+PrintGCDetails 带表已配置了GC回收参数
例如一个设置内存的示例: java -server -Xms1024m -Xmx1024m -XX:+UseG1GC -jar springboot2019-SNAPSHOT.war
总结公式:java -server jvm的各种配置参数 -jar jar包或者war包的名字
3、查看服务器启动了那些(查看jvm初始值 或者当前值的办法)
①执行 jps:(Java Virtual Machine Process Status
Tool)是java提供的一个显示当前所有java进程pid的命令,适合在linux/unix平台上简单察看当前java进程的一些简单情况
②查看当前进程有哪些参数: 打印命令行参数 jinfo -flags 进程号。 ==>>例如打印GC信息:jinfo -flags
例如,打印指定进程全部参数:jinfo -flags 14857
例如,打印指定进程指定参数内容:jinfo -flag PrintGCDetails 14857
常用jvm调参语法:
1、jmap 用来查看内存信息,实例个数以及占用内存大小jmap ‐histo 14660
#查看历史生成的实例,14660是进程id用jps查 jmap ‐histo:live 14660 #查看当前存活的实例,,14660是进程id用jps查。执行过程中可能会触发一次full gc 堆信息:jmap -heap 14660 也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./ (路径)
例如:
‐Xms10M ‐Xmx10M ‐XX:+PrintGCDetail‐XX:+HeapDumpOnOutOfMemoryError ‐XX:HeapDumpPath=D:\jvm.dump
jmap命令查看堆内存jmap -heap 进程ID
查看内存使用情况jmap -heap 9939 jmap ‐histo <pid> | more
例如: 查看内存中对象数量及大小
jmap -histo:live 11927 | more
2、Jstack 用jstack加进程id查找死锁(查找递归,死循环等得位置)行 jstack >19663|grep 4cd0
-A60 //19663是进程号,4cd0是十六进制的线程号,-A60是打印错误附近的60行代码
3、jinfo 查看正在运行的Java应用程序的扩展参数 查看jvm的参数
jinfo -flags 13573
jinfo -flag MaxHeapSize 9939
4、 jstat命令可以查看堆内存各部分的使用量,以及加载类的数量
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]例如:
jstat -class 9939垃圾回收统计:
jstat -gc 9939
4、内存调优参数
设置jvm调优的两种方法:
①在tomcat的bin下面的catalina.sh 里,位置cygwin=false前JAVA_OPTS=‘-server -Xms512m -Xmx768m -XX:NewSize=128m -XX:MaxNewSize=192m -XX:SurvivorRatio=8’
②使用jar包启动的话java -server -Xms1024m -Xmx1024m -XX:+UseG1GC -jar springboot2019-SNAPSHOT.jar
war包也可以这样:java -server -Xms1024m -Xmx1024m -XX:+UseG1GC -jar springboot2019-SNAPSHOT.war
参数解释:
-Xms 初始堆内存大小
-Xmx 最大堆内存大小
-Xss 单个线程栈大小
-XX:NewSize 初始新生代堆大小
-XX:MaxNewSize 生代最大堆大小
-XX:MetaspaceSize 元数据区初始值(JDK1.8)
-XX:MaxMetaspaceSize 元数据区最大值(JDK1.8)
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
含以-XX:SurvivorRatio=eden/from=den/to
总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等, 这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
例如:linux设置tomcat的catalina.sh
JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
或者:java -server -Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m -jar springboot2019-SNAPSHOT.jar调整完查看,打印JVM所有参数列表的方法:
java -XX:+PrintFlagsFinal -version
查看JVM初始化参数:java -XX:+PrintFlagsInitial
5、调优实例
①错误:java.lang.OutOfMemoryError:Metaspace
元空间大小内存溢出?
设置元空间大小方法:-XX:MetaspaceSize=1024m* 就是设置元空间的大小
实战中设置元空间大小:
java -server -Xms1024m -Xmx1024m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal -jar springboot2019-SNAPSHOT.jar
元空间的本质和永久代类似,都是对JVM规范中的方法区的实现。不过元空间与永久代的最大区别是:元空间并不在虚拟机中,而是使用本地内存,默认情况下元空间仅受本地内存大小限制
查看jvm所有配置信息:java -XX:+PrintFlagsFinal -version
这样找到MetaspaceSize对应的大小是21807104 字节 换算过来才 20.79m。也就是说元空间虽然只跟本地内存有关,但它初始值只有21m,可以调大②栈空间的大小调整?
-Xss等价于:-XX:ThreadStackSizejava -XX:+PrintFlagsFinal -version查找ThreadStackSize是栈空间大小,会发现初始值为0
栈空间默认值跟平台有关:
Linux(x64):1024kb
OS X(64-bit):1024kb
Windows:虚拟内存的默认值
例如:java -server -Xss128k -jar springboot2019-SNAPSHOT.jar
③查看垃圾回收多少次后才会由新生代进入老年代?
3.1、查看程序进程 jps -l
3.2、查看默认垃圾回收次数,才进入老年代 jinfo -flag
MaxTenuringThreshold 进程号 会看到默认是15次(最大设置为15)
二、垃圾回收
查看当前服务器用的垃圾回收器:java -XX:+PrintCommandLineFlags -version
1、GC算法:
引用计数:
复制:发生在新生代(优点:无内存碎片,效率高 缺点:需要两倍的空间)
标记清理:发生在老年代(优点:占用空间少 缺点:会产生内存碎片)
标记整理:发生在老年代(优点:占用空间少,无碎片 缺点:对象移动,会消耗资源)
2、垃圾回收器的种类:
1、Serial:串行(-XX:+UseSerialGC)>为单线程环境设计,且使用一个线程回收垃圾,会暂停所有的用户线程,不适合服务器环境(例如:用户用餐,餐厅叫出去要叫一个清洁工打扫,打扫完再回来吃)
2、Parallel:并行(-XX:+UseParallelGC)>多个并行垃圾收集线程工作,此时用户线程是暂停的,适用于科学计算、大数据处理首台处理等若交互环境(例如:用户用餐,餐厅叫出去要叫多个清洁工打扫,打扫完再回来吃)
3、CMS:(-XX:UseConcMarkSweepGC)>用户线程和垃圾收集线程同时执行(并不一定是并行,可能交替执行),不需要停顿用户线程
,适用对响应时间有要求的场景(例如:用户用餐,餐厅叫出去要叫多个清洁工打扫,边吃边打扫)
4、G1:(garbage first)(-XX:UseG1GC)>G1垃圾回收器将堆内存分割成不通的区域然后并发的对其进行垃圾回收 java11默认GC回收器是ZGC
3、如何判断一个对象是否应该被回收?
判断一个对象是否可达,不可达对象就将被回收,所谓可达就是从GCROOT开始是否是可以找到该对象 GCROOT是什么?
1、虚拟机栈中引用的对象(本地变量表)
2、方法区中静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中引用的对象(Native Object)
4、设置垃圾回收器?
-XX:+UseSerialGC
-XX:+UseConcMarkSweepGC
-XX:+UseParallelGC
-XX:+UseG1GC
例如:java -server -Xms1024m -Xmx1024m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal -XX:+UseG1GC -jar springboot2019-SNAPSHOT.jar
5、什么是强引用、软引用、弱引用、虚引用?
强引用:>只要还有强引用指向对象,就算OOM也不会回收该对象。Object o1=new Object(),就算强引用
软引用:>内存足够的情况不回收,内存不足时就回收的对象。SoftReference,用于内存敏感的地方
Object o1=new Object();
SoftReference sf = new SoftReference<>(o1);
弱引用:>不管内存是否够用,GC时一律回收 Object o1=new Object(); WeakReference sf = new WeakReference<>(o1);
虚引用:>PhantomReference 虚引用并不会决定生命周期,如果一个对象仅持有虚引用,那么他就和没有引用是一样的,任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列联合使用
主要作用是:跟踪对象被垃圾回收的状态
三、调优示例:
1、 cpu占用过高,定位java代码中的办法?
1、使用 top查看占用cpu高的程序
例如:top
假如,cpu占用最高的就是 elasticsearch (一般是业务jar包)
37 root 20 0 0 0 0 S 0.3 0.0 0:01.82 elasticsearch
2、使用jps或ps -ef|grep “elasticsearch” 去找出这个占用高的程序的进程号
例如:jps -l
1541 Elasticsearch
3、定位到具体的线程或代码:ps -mp 进程 -o THREAD,tid,time
-m:显示所有线程
-p: pid进程使用cpu时间
-o:该参数后是用户自定义格式
例如:ps -mp 1541 -o THREAD,tid,time
输出很多(最后一列是时间,假如这个线程耗时最长):esuser 0.0 19 - futex_ - - 1541 00:00:08
找到时间最长的那个线程号
4、把需要的线程id转换为16进制的格式(要英文小写的)
方法一:执行命令:printf “%x\n” 有问题的线程id。例如:printf "%x\n" 2242
输出:8c2
方法二:用计算器转换为16进制
5、执行命令:jstack 进程ID |grep tid(16进制的线程id英文小写) -A60
例如:jstack 2242 |grep 8c2 -A60
tid(16进制的线程id英文小写):是一个整体是指上面换算后的线程id(要16进制那个值)
-A60是指打印最近的60行
在打印信息中找到包名就是java代码对应的哪一行报出来的错!
四、老年代垃圾回收详情
1、垃圾进入老年代的情况
1、当对象的年龄达到15岁时
默认的设置下,也就是躲过15次GC的时候,他就会转移到老年代里去 具体是多少岁进入老年代,可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
2、动态对象年龄判断
假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,
那么此时大于等于这批对象年龄的最大值对象,就可以直接进入老年代了
例如:年龄1+年龄2+年龄n,的多个年龄对象总和超过了Survivor区的50%,此时就会把年龄n以上的对象都放入老年代
3.大对象直接进入老年代
如果你要创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放到老年代里去,压根不会经过年轻代,有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把它的值设置为字节数,比如“1048576”字节,就是1MB
4.Minor GC后的对象太多
Minor GC后的对象太多无法放入Survivor区 这个时候就必须得把这些对象直接转移到老年代去
2 、full GC的情况
老年代采取的垃圾回收算法是
标记整理算法 老年代触发垃圾回收的机制,一般就是两个;
①在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下, 此时需要提前触发Full
GC再然后再带着进行Minor GC;
②在Minor GC之后,发现剩余对象太多放入老年代都放不下了
五 、JVM 类加载机制
1、JVM 类加载机制分为五个部分:

五部分:
加载、连接、初始化、使用、卸载
连接又包括:验证、准备、解析
加载:这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口
验证:确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备:是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间.
例如:public static int v = 8080;实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是
程序被编译后,存放于类构造器<client>方法之中
解析:虚拟机将常量池中的符号引用替换为直接引用的过程。
初始化:到了初始阶段,才开始真正执行类中定义的 Java 程序代码
使用:
卸载:
2、类加载模式
类加载器的双亲委派加载机制(重点)
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中, 只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试自己去加载。 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就
保证了使用不同的类加载 器最终得到的都是同样一个 Object 对象。