【JVM调优】JVM内存管理&调优浅谈

什么是JVM

Java Virtual Machine,Java虚拟机

Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。

Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

Java虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种API、优化技术等。Java技术使同一种应用可以运行在不同的平台上。Java平台可分为两部分,即Java虚拟机(Java virtual machine,JVM)和Java API类库。

作用:JVM是java字节码执行的引擎,解析字节码文件的内容,并将其翻译为各种操作系统能理解的机器码

运作过程:解析字节码文件的时候,会先加载字节码文件,将其存入java虚拟机的内存空间中,进行一系列的动作,最后运行程序得出结果

JVM内存空间

字节码数据在java虚拟机内存中如何存放的?
在这里插入图片描述

虚拟机栈

用于执行java方法,每个方法执行都会创建一个栈帧,存储局部变量表,操作数栈,动态链接等信息,程序执行时,栈帧入栈,执行完成后栈帧出栈
在这里插入图片描述

程序计数器

或者叫PC寄存器,记载着每一个线程当前运行的JAVA方法的地址,指示当前程序执行到了哪个位置:

1、执行Java方法时记录正在执行的虚拟机字节码指令地址;
2、执行本地方法时,计数器值为null;

每一个线程都有一个PC寄存器,也就是说PC寄存器是线程独有的。
生命周期跟随线程,线程启动而产生,线程结束而消亡。
是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。

本地方法区

如果程序有使用到native方法,加载和执行就在这个区域。

方法区

存储 Java 类字节码数据的一块区域,存储类,常量相关的信息。

Java堆

Java虚拟机管理的内存中最大的一块,Java虚拟机启动的时候建立,所有线程共享,几乎所有的对象实例都在这里分配内存。

GC主要就是在Java堆中进行的。

Java 堆根据对象存活时间的不同,Java 堆还被分为新生代(或叫 年轻代)、老年代两个区域,新生代还被进一步划分为 Eden 区、Survivor 0、Survivor 1 区。
在这里插入图片描述

新生代
  1. 新生代内存按照8:1:1的比例分为一个Eden区和两个survivor区(survivor0和survivor1);
  2. 当有对象需要分配时,优先被分配在年轻代的 Eden 区;
  3. 当Eden区满了,就需要进行垃圾回收(Minor GC),回收 Eden 区中没有被引用的对象(可达性分析);
  4. 在年轻代的对象经过了指定次数(默认15次)的 GC 后,将在下次 GC 时进入老年代;
  5. 两个survivor区(survivor0和survivor1)
老年代
  1. 老年代存放的都是一些生命周期较长的对象,或者是很大的对象(大于Eden区空间);
  2. 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小,老年代 ( Old ) = 2/3 的堆空间大小;
  3. 将对象从新生代移到老年代时,如果此时老年代空间不够,那么触发 Major GC。

GC介绍

GC,即垃圾回收
英语:Garbage Collection,缩写为GC

GC是一种自动的内存管理机制

垃圾回收的术语:

Minor GC
从新生代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。

Major GC
从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。

Full GC
Full GC 是清理整个堆空间 —— 包括新生代、老年代。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。

Stop-The-World
翻译为全世界暂停,简称 STW,是指在进行垃圾回收时,因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程完成回收垃圾的时间。

垃圾回收机制的重点:

哪些内存需要回收?

垃圾收集器会对堆进行回收前,确定对象中哪些是“存活”,哪些是“死亡”(不可能再被任何途径使用的对象);

当一个对象到GC Roots没有任何引用链相连,即不可达时,则证明此对象时不可用的。

举例:一颗树有很多丫枝,其中一个分支断了,跟树上没有任何联系,那就说明这个分支没有用了,就可以当垃圾回收去烧了。

什么时候回收?

主要的场景:
(1)JVM 无法为一个新的对象分配空间时会触发 Minor GC

(2)老年代空间不够,那么触发 Major GC

(3)当准备要触发一次Minor GC时,如果出现历史Minor GC的平均晋升大小比目前 “老年代”剩余的空间大,老年代剩余空间不够用于新生代晋升,则不会触发young GC,而是转为触发full GC

如何回收?

垃圾收集算法:

1)标记—清除算法

标记—清除算法(Mark-Sweep),是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程是通过可达性分析算法实现的。

回收前状态
在这里插入图片描述
回收后状态
在这里插入图片描述
算法缺点:
标记和清除过程的效率都不高;标记清除后会产生大量不连续的内存碎片。

2)复制算法

复制算法将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。

回收前状态
在这里插入图片描述

回收后状态
在这里插入图片描述

复制算法优点:
每次只对一块内存进行回收,运行高效;大大减少了内存碎片的出现;

缺点:
可初始分配的最大内存缩小了一半;

3)标记—整理算法

分为“标记”、“整理”、“清除”三个阶段
先对内存区域的对象进行标记,区分出“存活对象”和“可回收对象”,
让所有的对象都向内存区域一端移动,直接清理掉可回收的对象;

回收前状态
在这里插入图片描述
回收后状态
在这里插入图片描述

4)分代回收

根据对象的存活周期的不同,将内存划分为新生代和老年代。

在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集;

新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间(8:1:1的比例),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被预留。

老年代中因为对象存活率高、没有额外空间对它进行分配担保,就使用标记—清除算法或标记—整理算法来进行回收

新生代回收
在这里插入图片描述

JVM常用参数

Jvm启动参数共分为三类:

一、是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;

二、是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;

三、是非Stable参数(-XX),此类参数各个jvm实现会有所不同;

-Xms256m
为jvm启动时分配的内存

-Xmx256m
为jvm运行过程中分配的最大内存,建议和Xms保持一致

-Xmn128m
设置年轻代大小为128M
此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8-Xss256k
为jvm启动的每个线程分配的内存大小

-XX:NewSize 和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

-XX:PermSize=1024M 和 -XX:MaxPermSize=1024M
JVM初始分配的非堆内存, 不会被回收, 建议与maxPermSize相同

-XX:SurvivorRatio=4
年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。
设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:NewRatio=4
设置年轻代(EC+S0C+S1C)和年老代(OC)的比值。如:4,表示年轻代与年老代比值为14,年轻代占整个年轻代年老代和的1/5

-XX:InitialTenuringThreshol 和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800-Xloggc:../logs/gc.log 日志文件的输出路径

-XX:+UseGCLogFileRotation 
-XX:NumberOfGCLogFiles=5 
-XX:GCLogFileSize=20M 
JVM的一个日志文件达到了20M以后,就会写入另一个新的文件,最多会有5个日志文件,他们的名字分别是:gc.log.0、gc.log.1-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

2020-06-29T03:45:25.512+0800: 76.642: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 402653184 bytes, new threshold 2 (max 15)
- age   1:  169351248 bytes,  169351248 total
- age   2:  331023392 bytes,  500374640 total
- age   3:   64201192 bytes,  564575832 total
, 1.2957512 secs]

-XX:MaxTenuringThreshold
用于调整对象晋升老年代的所需经历的GC次数,默认15次,即在年轻代的对象经过了指定次数的 GC 后,将在下次 GC 时进入老年代。

JVM调优浅谈

JVM内存管理很差会出现什么情况?

1,内存溢出
2,频繁Full GC
….

内存溢出:

  1. OutOfMemoryError: Java heap space 堆溢出

    最常见的内存溢出,当在JVM中如果98%的时间是用于GC且可用的 Heap size 不足2%的时候将抛出此异常信息。

    优化建议:

    Heap Size 最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx选项设置为相同,而-Xmn为3/8的-Xmx值。

  2. OutOfMemoryError: PermGen space 非堆溢出(永久保存区域溢出)

    这块内存主要是被JVM存放Class和Meta信息的,是 JVM初始分配的非堆内存,没有GC回收,一般发生在程序的启动阶段。

    优化建议:

    通过-XX:PermSize和 -XX:MaxPermSize设置合适的内存大小

  3. OutOfMemoryError: unable to create new native thread. 无法创建新的线程

    常见在高并发的业务场景,线程池一直新建线程,每个线程都有自己的Stack Space,Stack Space的空间是独立分配的,当超出jvm的栈内存大小时, 就会报出无法再创建线程的错误.

    优化建议:

    设置合适的-Xss参数,优化线程池资源分配。

  4. java.lang.StackOverflowError : Thread Stack space

    栈溢出了,常见于代码递归的层次过多。

    优化建议:

    修改程序、设置合适的-Xss参数。

MinorGC 日志

2020-06-23T04:35:59.604+0800: 463.092: [GC (Allocation Failure) 
2020-06-23T04:35:59.604+0800: 463.092: [ParNew: 43296K->7006K(47808K), 0.0136826 secs] 
44992K->8702K(252608K), 0.0137904 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 

2020-06-23T04:35:59.604+0800  日志文件的时间戳

463.092  JVM记录的时间戳:GC开始,相对JVM启动的相对时间,单位是秒

GC  触发了YoungGC/MinorGC

Allocation Failure – MinorGC的原因,由于年轻代不满足申请的空间,因此触发了MinorGC

[ParNew(使用ParNew作为新生代的垃圾回收器,采用的是复制算法): 43296K(年轻代垃圾回收前的大小)->7006K(年轻代垃圾回收以后的大小)(47808K)
(年轻代的总大小), 0.0136826 secs(回收时间)] 44992K(堆区垃圾回收前的大小)->8702K(堆区垃圾回收后的大小)(252608K)(堆区总大小), 0.0137904 secs(回收时间)]

[Times: user=0.03 sys=0.00, real=0.02 secs]  
user:GC 线程在垃圾收集期间所使用的 CPU 总时间;
sys:系统调用或者等待系统事件花费的时间;
real:应用被暂停的时钟时间,由于 GC 线程是多线程的,导致了 real 小于 (user+real),如果是 gc 线程是单线程的话,real 是接近于 (user+real) 时间。

FullGC 日志

2018-04-12T13:48:26.233+0800: 15578.148: [GC [1 CMS-initial-mark: 6294851K(20971520K)] 
6354687K(24746432K), 0.0466580 secs] [Times: user=0.04 sys=0.00, real=0.04 secs]
2018-04-12T13:48:26.280+0800: 15578.195: [CMS-concurrent-mark-start]
2018-04-12T13:48:26.418+0800: 15578.333: [CMS-concurrent-mark: 0.138/0.138 secs] 
[Times: user=1.01 sys=0.21, real=0.14 secs]
2018-04-12T13:48:26.418+0800: 15578.334: [CMS-concurrent-preclean-start]
2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-preclean: 0.056/0.057 secs] 
[Times: user=0.20 sys=0.12, real=0.06 secs]
2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-abortable-preclean-start]
2018-04-12T13:48:29.989+0800: 15581.905: [CMS-concurrent-abortable-preclean: 3.506/3.514 secs] 
[Times: user=11.93 sys=6.77, real=3.51 secs]
2018-04-12T13:48:29.991+0800: 15581.906: [GC[YG occupancy: 1805641 K (3774912 K)]
2018-04-12T13:48:29.991+0800: 15581.906: [GC2018-04-12T13:48:29.991+0800: 15581.906: 
[ParNew: 1805641K->48395K(3774912K), 0.0826620 secs] 8100493K->6348225K(24746432K), 
0.0829480 secs] [Times: user=0.81 sys=0.00, real=0.09 secs]2018-04-12T13:48:30.074+0800: 
15581.989: [Rescan (parallel) , 0.0429390 secs]2018-04-12T13:48:30.117+0800: 15582.032: 
[weak refs processing, 0.0027800 secs]2018-04-12T13:48:30.119+0800: 15582.035: [class 
unloading, 0.0033120 secs]2018-04-12T13:48:30.123+0800: 15582.038: [scrub symbol table, 
0.0016780 secs]2018-04-12T13:48:30.124+0800: 15582.040: [scrub string table, 0.0004780 
secs] [1 CMS-remark: 6299829K(20971520K)] 6348225K(24746432K), 0.1365130 secs] 
[Times: user=1.24 sys=0.00, real=0.14 secs]
2018-04-12T13:48:30.128+0800: 15582.043: [CMS-concurrent-sweep-start]
2018-04-12T13:48:38.412+0800: 15590.327: [CMS-concurrent-sweep: 8.193/8.284 secs] 
[Times: user=30.34 sys=16.44, real=8.28 secs]
2018-04-12T13:48:38.419+0800: 15590.334: [CMS-concurrent-reset-start]
2018-04-12T13:48:38.462+0800: 15590.377: [CMS-concurrent-reset: 0.044/0.044 secs] 
[Times: user=0.15 sys=0.10, real=0.04 secs]
阶段1:Initial Mark
2018-04-12T13:48:26.233+0800: 15578.148: [GC [1 CMS-initial-mark: 6294851K(20971520K)] 
6354687K(24746432K), 0.0466580 secs] [Times: user=0.04 sys=0.00, real=0.04 secs]

CMS-initial-mark:初始标记阶段,CMS是老年代垃圾回收器,基于标记-清除算法实现,
它会收集所有 GC Roots 以及其直接引用的对象;
6294851K:当前老年代使用的容量,这里是 6G;
(20971520K):老年代可用的最大容量,这里是 20G;
6354687K:整个堆目前使用的容量,这里是 6.06G;
(24746432K):堆可用的容量,这里是 23.6G;
0.0466580 secs:这个阶段的持续时间;


这个是 CMS 两次 stop-the-wolrd 事件的其中一次,这个阶段的目标是:标记那些直接被 GC root 引用
或者被年轻代存活对象所引用的所有对象
阶段2:CMS-concurrent-mark
2018-04-12T13:48:26.280+0800: 15578.195: [CMS-concurrent-mark-start]
2018-04-12T13:48:26.418+0800: 15578.333: [CMS-concurrent-mark: 0.138/0.138 secs] 
[Times: user=1.01 sys=0.21, real=0.14 secs]

CMS-concurrent-mark:并发标记阶段,遍历老年代,标记所有存活的对象,由第一阶段标记过的对象出发,
所有可达的对象都在本阶段标记;
0.138/0.138 secs:这个阶段的持续时间与时钟时间;


2018-04-12T13:48:26.418+0800: 15578.334: [CMS-concurrent-preclean-start]
2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-preclean: 0.056/0.057 secs] 
[Times: user=0.20 sys=0.12, real=0.06 secs]
阶段3:Concurrent Preclean
Concurrent Preclean :并发预清理阶段,对在前面并发标记阶段中引用发生变化的对象进行标记,
包含:从新生代晋升、新分配、被更新的对象,并发地重新扫描这些对象;
0.056/0.057 secs:这个阶段的持续时间与时钟时间;


2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-abortable-preclean-start]
2018-04-12T13:48:29.989+0800: 15581.905: [CMS-concurrent-abortable-preclean: 
3.506/3.514 secs] [Times: user=11.93 sys=6.77, real=3.51 secs]
阶段4:Concurrent Abortable Preclean
Concurrent Abortable Preclean :并发可中止的预清理阶段,和上一阶段工作内容一样,但可以控制结束
时间,这个阶段持续时间依赖于很多的因素:完成的工作量、扫描持续时间等;

2018-04-12T13:48:29.991+0800: 15581.906: [GC[YG occupancy: 1805641 K (3774912 K)]
2018-04-12T13:48:29.991+0800: 15581.906: [GC2018-04-12T13:48:29.991+0800: 15581.906: 
[ParNew: 1805641K->48395K(3774912K), 0.0826620 secs] 8100493K->6348225K(24746432K), 
0.0829480 secs] [Times: user=0.81 sys=0.00, real=0.09 secs]
2018-04-12T13:48:30.074+0800: 15581.989: [Rescan (parallel) , 0.0429390 secs]
2018-04-12T13:48:30.117+0800: 15582.032: [weak refs processing, 0.0027800 secs]
2018-04-12T13:48:30.119+0800: 15582.035: [class unloading, 0.0033120 secs]
2018-04-12T13:48:30.123+0800: 15582.038: [scrub symbol table, 0.0016780 secs]
2018-04-12T13:48:30.124+0800: 15582.040: [scrub string table, 0.0004780 secs] 
[1 CMS-remark: 6299829K(20971520K)] 6348225K(24746432K), 0.1365130 secs] 
[Times: user=1.24 sys=0.00, real=0.14 secs]
阶段5:remark
remark 重标记阶段
	重标记阶段(CMS的第二个STW阶段),暂停所有用户线程,从GC Root开始重新扫描整堆,标记存活的对象。
虽然CMS只回收老年代的垃圾对象,但是这个阶段依然需要扫描新生代,因为很多GC Root都在新生代,
而这些GC Root指向的对象又在老年代,这称为“跨代引用”。

YG occupancy: 1805641 K (3774912 K):年轻代当前占用量及总容量,这里分别是 1.71G 和 3.6G;
ParNew:触发了一次 young GC,原因是为了减少年轻代的存活对象,尽量使年轻代更干净一些;
[Rescan (parallel) , 0.0429390 secs]:这个 Rescan 是当应用暂停的情况下完成对所有存活对象的标记,
这个阶段是并行处理的,这里花费了 0.0429390s;
[weak refs processing, 0.0027800 secs]:第一个子阶段,它的工作是处理弱引用;
[class unloading, 0.0033120 secs]:第二个子阶段,它的工作是:unloading the unused classes;
[scrub symbol table, 0.0016780 secs][scrub string table, 0.0004780 secs]:
最后一个子阶段,它的目的是:cleaning up symbol and string tables which hold class-level 
metadata and internalized string respectively
CMS-remark:remark结束,输出当前老年代的使用量与总量6299829K(20971520K),
堆的使用量与总量6348225K(24746432K)

2018-04-12T13:48:30.128+0800: 15582.043: [CMS-concurrent-sweep-start]
2018-04-12T13:48:38.412+0800: 15590.327: [CMS-concurrent-sweep: 8.193/8.284 secs] 
[Times: user=30.34 sys=16.44, real=8.28 secs]
阶段6:并发清理阶段

这个阶段主要是清除那些没有被标记的对象,回收它们的占用空间;这里不需要 STW,
它是与用户的应用程序并发运行。


2018-04-12T13:48:38.419+0800: 15590.334: [CMS-concurrent-reset-start]
2018-04-12T13:48:38.462+0800: 15590.377: [CMS-concurrent-reset: 0.044/0.044 secs] 
[Times: user=0.15 sys=0.10, real=0.04 secs]
阶段7:Concurrent Reset阶段
这个阶段也是并发执行的,它会重设 CMS 内部的数据结构,为下次的 GC 做准备。

Full GC频繁发生怎么办?

排查方向:
1、根据FULLGC日志分析,消耗在什么阶段;
2、JVM参数设置不合理;
3、是否有内存泄露;
4、对象的内存分配存在问题,大对象过多;
5、使用jstat、jstack、jmap命令定位;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值