JVM是Java Virtual Machine的英文简称,JVM是Java虚拟机,通过在实际的计算机上仿真模拟各种计算机功能来实现的,Java语言引用了JVM使得Java语言编译的字节码文件跨平台使用,因为不同平台只要安装不同JVM就可以运行,不需要重新编译源文件。JVM的本质是运行在操作系统上的一个程序,一个进程,Java虚拟机启动后就开始执行保存在字节码文件中的指令,其内部组成结果如下图所示。
类加载器(Class Loader)负责将编译好的.class字节码文件编译到内存中,使得JVM可以实例化或以其他方式使用加载后的类。 类加载器支持在运行时动态加载,动态加载可以节省内存空间。
类加载器加载类的过程
Java虚拟机栈(Java JVM Stack )是Java方法执行的内存模型,是线程私有的,和线程直接相关。每创建一个新的线程,JVM就会为该线程分配一个对应的Java虚拟机栈,各个线程的Java虚拟机栈的内存区域是不能直接被访问的,以保证线程并发运行时线程的安全性。每调用一个方法,Java虚拟机栈就会为每一个方法生成一个栈帧 (Stack Frame),调用方法时压入栈帧,叫入栈, 方法返回时弹出栈帧并抛弃,叫出栈。栈帧中存储方法的局部变量,操作数栈,动态链接,中间运算结果,方法返回值等信息。 每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程,虚拟机的生命周期和线程是一样的,栈帧中的存储的局部变量随着线程运行的结束而结束。虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError栈溢出,不过大多数虚拟机都允许动态扩展虚拟机栈的大小, 所以线程可以一直申请栈,直到内存不足抛出OutOfMemoryError内存溢出。
原生方法栈 (Native Method Stack),又叫做本地方法栈,主要存储了原生方法,即native修饰的方法,是为了JVM调用去调用原生方法和接口的栈区。native关键字 :修饰的方法可以简单理解为非 java语言实现的代码接口,由于java语言无法直接访问操作系统底层信息,需要借助C/C++语言来实现,并且被编译成了DDL,由java调用,不同平台的c/c++的实现也是不一样的,native修饰的方法可以被C语言重写,所以native方法的可移植性不高,主要用于加载文件和动态链接库。
程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。
从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。
首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。
特点 :
JDK 7 中原来是有方法区,也就是常说的永久代区域,存储着Java类信息,常量池,静态变量等,方法区占用的内存区域是线程共享的。由于方法区的大小是启动时就设置好的,有限制大小,虽然可以触发永久代OOM会动态调整,会发生OOM,此外永久代的GC特别难搞,加载类过多时严重影响Full GC的性能,于是Java8内存结构抛弃永久代,使用元数据区,解决这些问题。
Java 8 采用了元数据区和本地内存,常量池,包括字符串常量(JVM独此一份),运行时常量(一个类加载到 JVM 中后对应一个运行时常量) 和静态变量等数据则存放到了Java堆Heap 中 。 元数据区就是保存类的元数据,如方法、字段、类,包的描述信息,这些信息被类加载器加载类的时候写入,可以用于创建文档,跟踪代码的依赖性,执行编译时检查。元数据信息直接存放到JVM管理的本地内存中,本地内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义中的内存区域。 本机内存如果够大就不会出现OOM了,可以通过:-XX:MetaspaceSize
来控制它的初始大小,达到该值就会触发垃圾收集进行类型卸载,如不规定大小,会耗尽全部的机器内存。
每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元数据区。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。
元数据区如何提高性能
Java是一门面向对象的程序设计语言,而JVM堆区是真正存储Java对象实例的内存区域,并且是所有线程共享的,所以程序在进行实例化对象等操作时,需要解决同步和线程安全问题。Java 中的堆是 JVM 所管理的最大的一块内存空间,java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx
和-Xms
设定),如果堆无法扩展或者无法分配内存时也会报OOM。存储的数据类型如下 :
string table
线程私有
,但是不影响java堆的共性新生代区域和老年代区域
Java堆区可以细分为新生代(Young Generation)区域和老年代(Old Generation) 区域,新生代区域还可以分为Eden(伊甸园)空间区域、From Survivor(幸存者)区域,To Survivor(幸存者)区域,
Java语言和别的变成语言不一样,程序运行时内存回收是不需要开发者自己在代码中进行手动回收和释放,而是JVM自动进行内存回收,内存回收时会将不再使用的对象实例等从内存中移除掉,以释放出更多的内存空间,这个过程就是常说的JVM垃圾回收机制。
垃圾回收机制一般简称为GC,新生代垃圾回收一般称为 Minor GC,老年代垃圾回收一般称为 Major GC或者Full GC。垃圾回收如此重要,是因为发生垃圾回收时,一般都会伴随着应用程序的暂停运行,一般发生垃圾回收时除GC所需的线程外,所有其他线程都进入等待状态,直到GC执行完成,GC调优最重要就是减少引用程序的暂停执行时间。
JVM 垃圾回收常见的算法有根搜索算法,标记清除算法,复制算法,标记-整理算法和增量回收算法
JVM垃圾回收有多种方式,比较常用的有CMS和G1
https://www.cnblogs.com/cxxjohnson/p/8625713.html
https://blog.csdn.net/jisuanjiguoba/article/details/80156781
https://www.cnblogs.com/sidesky/p/10797382.html
jconsole是JDK自带的、基于jmx协议的、对JVM进行可视化监视和管理的工具,window在JDK的bin目录下,jconsole.exe直接启动,找到运行的main函数 ,远程连接需要远程服务开启jmx协议
测试方法,本地启动了一个springboot服务,用jmeter对一个接口做一小时的压力测试。
概览
堆内存使用量 : JVM堆内存的使用量随着时间变化的曲线,从图中看到曲线一般是波动的呈现锯齿状,一般波动时都是发生了GC垃圾回收,已用表示当前JVM真实使用的堆内存大小,已提交表示已经申请了多少堆内存来使用,最大表示JVM中堆内存的最大上限为多少。
线程 : 显示当前活动线程随着时间的变化曲线
类 : 表示当前加载的类的数量
CPU占用率 : 显示了CPU的占用率随时间变化的曲线,
内存
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
堆内存的使用呈现标准的锯齿状,并且内存呈现持平或者下降趋势,是比较正常的内存使用,如果内存使用一直呈现缓慢上升,如上图,那么很有可能存在内存泄露,需要分析是不是内存泄露的问题;
线程
显示当前获得线程数,如果线程数一直呈现上升趋势,需要检查应用程序是否有线程泄露情况,如果线程一直上升,最终会耗尽内存资源
查看线程堆栈
检测死
JVM概要
JDK自带的工具 ,bin目录下 jvisualm.exe 执行,可以本地也可以远程
监视页面 :
Metaspace: java8使用元空间替代了持久代,元空间直接占用物理内存。
线程 :
可以查看每个线程在不同状态下的停留时间,以及线程的在运行状态下的累积时长等信息
通过线程dump按钮可以生成线程当前的堆栈信息,从dump出来的线程堆栈中,可以定位到某个线程当前在执行那段代码,处于何种状态。
抽样器 :
CPU样例:查看那些方法占用CPU,占用CPU时长的时间
线程CPU时间 : 显示那些线程正在占用CPU,占用CPU时长占比等数据信息,点击增量可以实时CPU实时增量占用数据信息;
内存抽样分析 :
堆柱状图 :可以看到实例对象占用堆内存的大小,实例对象堆内存的占比,实例对象的数量等数据信息
每个线程分配 : 切换到每个线程使用的堆内存的监控视图,可以看到所有线程累计内存大小,每个线程占用的内存大小以及占比,每个线程每秒正在使用的内存大小等数据信息;这些信息对定位内存泄露提供很多分析依据。
profiler : 分析内存和CPU的使用
垃圾收集器插件 Visual GC : 需要在工具->插件- 可用插件-选择安装即可
整个区域分为三部分:spaces、graphs、histogram
1,spaces区域:代表虚拟机内存分布情况。从图中可以看出,虚拟机被分为Perm、Old、Eden、S0、S1
注意:如果对每个区域基本概念不是很熟悉的可以先了解下java虚拟机运行时数据区这篇文字。
1.1)perm:英文叫做Permanent Generation,我们称之为永久代。(根据深入java虚拟机作者说明,这里说法不是不是很正确,因为hotspot虚拟机的设计团队选择把GC分代收集扩展至此而已,正确的应该叫做方法区或者非堆)。
1.1.1)通过VM Args:-XX:PermSize=128m -XX:MaxPermSize=256m 设置初始值与最大值
1.2)heap:java堆(Java heap)。它包括老年代(图中Old区域)和新生代(图中Eden/S0/S1三个统称新生代,分为Eden区和两个Survivor区域),他们默认是8:1分配内存
1.2.1)通过VM Args:-xms512m -Xmx512m -XX:+HeapDumpOnOutofMemoryError -Xmn100m -XX:SurvivorRatio=8 设置初始堆内存、最大堆内存、内存异常打印dump、新生代内存、新生代内存分配比例(8:1:1),因为Heap分为新生代跟老年代,所以512M-100M=412M,老年代就是412M(初始内存跟最大内存最好相等,防止内存不够时扩充内存或者Full GC,导致性能降低)
2,Graphs区域:内存使用详细介绍
2.1)Compile Time(编译时间):6368compiles 表示编译总数,4.407s表示编译累计时间。一个脉冲表示一次JIT编译,窄脉冲表示持续时间短,宽脉冲表示持续时间长。
2.2)Class Loader Time(类加载时间): 20869loaded表示加载类数量, 139 unloaded表示卸载的类数量,40.630s表示类加载花费的时间
2.3)GC Time(GC Time):2392collections表示垃圾收集的总次数,37.454s表示垃圾收集花费的时间,last cause表示最近垃圾收集的原因
2.4)Eden Space(Eden 区):括号内的31.500M表示最大容量,9.750M表示当前容量,后面的4.362M表示当前使用情况,2313collections表示垃圾收集次数,8.458s表示垃圾收集花费时间
2.5)Survivor 0/Survivor 1(S0和S1区):括号内的3.938M表示最大容量,1.188M表示当前容量,之后的值是当前使用情况
2.6)Old Gen(老年代):括号内的472.625M表示最大容量,145.031M表示当前容量,之后的87.031表示当前使用情况,79collections表示垃圾收集次数 ,28.996s表示垃圾收集花费时间
2.7)Perm Gen(永久代):括号内的256.000M表示最大容量,105.250M表示当前容量,之后的105.032M表示当前使用情况
3,Histogram区域:survivor区域参数跟年龄柱状图
3.1)Tenuring Threshold:表示新生代年龄大于当前值则进入老年代
3.2)Max Tenuring Threshold:表示新生代最大年龄值。
3.3)Tenuring Threshold与Max Tenuring Threshold区别:Max Tenuring Threshold是一个最大限定,所有的新生代年龄都不能超过当前值,而Tenuring Threshold是个动态计算出来的临时值,一般情况与Max Tenuring Threshold相等,如果在Suivivor空间中,相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或者等于该年龄的对象就都可以直接进入老年代(如果计算出来年龄段是5,则Tenuring Threshold=5,age>=5的Suivivor对象都符合要求),它才是新生代是否进入老年代判断的依据。
3.4)Desired Survivor Size:Survivor空间大小验证阙值(默认是survivor空间的一半),用于Tenuring Threshold判断对象是否提前进入老年代。
3.5)Current Survivor Size:当前survivor空间大小
3.6)histogram柱状图:表示年龄段对象的存储柱状图
3.7)如果显示指定-XX:+UseParallelGC --新生代并行、老年代串行收集器 ,则histogram柱状图不支持当前收集器
内存溢出( out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 (memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。其实说白了就是该内存空间使用完毕之后未回收。
JVM的配置
JAVA_OPTS=-server -Xmx10g -Xms10g -XX:+DisableExplicitGC -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:+ExplicitGCInvokesConcurrent -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Xloggc:/app/deploy/logs/gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M
-XX:+UseG1GC : G1垃圾收集器
https://my.oschina.net/dabird/blog/710444
Java HotSpot(TM) 64-Bit Server VM (25.172-b03) for linux-amd64 JRE (1.8.0_172-ea-b03), built on Jan 18 2018 10:27:25 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 791223624k(275476136k free), swap 0k(0k free)
CommandLine flags: -XX:+DisableExplicitGC -XX:+ExplicitGCInvokesConcurrent -XX:GCLogFileSize=104857600 -XX:InitialHeapSize=10737418240 -XX:InitiatingHeapOccupancyPercent=45 -XX:LargePageSizeInBytes=134217728 -XX:+ManagementServer -XX:MaxGCPauseMillis=200 -XX:MaxHeapSize=10737418240 -XX:NumberOfGCLogFiles=10 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastAccessorMethods -XX:+UseG1GC -XX:+UseGCLogFileRotation
2021-01-04T16:43:24.600+0800: 3.568: [GC pause (G1 Evacuation Pause) (young), 0.0362113 secs]
[Parallel Time: 25.9 ms, GC Workers: 33]
[GC Worker Start (ms): Min: 3568.6, Avg: 3568.9, Max: 3569.3, Diff: 0.8]
[Ext Root Scanning (ms): Min: 0.0, Avg: 1.2, Max: 22.8, Diff: 22.8, Sum: 40.3]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.6]
[Code Root Scanning (ms): Min: 0.0, Avg: 9.4, Max: 24.1, Diff: 24.1, Sum: 311.4]
[Object Copy (ms): Min: 0.0, Avg: 7.5, Max: 17.1, Diff: 17.1, Sum: 248.0]
[Termination (ms): Min: 0.0, Avg: 6.5, Max: 7.7, Diff: 7.7, Sum: 213.3]
[Termination Attempts: Min: 1, Avg: 107.2, Max: 224, Diff: 223, Sum: 3539]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.5]
[GC Worker Total (ms): Min: 24.1, Avg: 24.7, Max: 25.0, Diff: 0.9, Sum: 814.0]
[GC Worker End (ms): Min: 3593.5, Avg: 3593.5, Max: 3593.7, Diff: 0.2]
[Code Root Fixup: 0.2 ms]
[Code Root Purge: 0.1 ms]
[Clear CT: 1.0 ms]
[Other: 9.0 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 6.9 ms]
[Ref Enq: 0.1 ms]
[Redirty Cards: 0.6 ms]
[Humongous Register: 0.2 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.5 ms]
[Eden: 512.0M(512.0M)->0.0B(488.0M) Survivors: 0.0B->24.0M Heap: 512.0M(10.0G)->23.7M(10.0G)]
[Times: user=0.42 sys=0.05, real=0.03 secs]
2021-01-04T16:43:24.657+0800: 3.625: [GC pause (Metadata GC Threshold) (young) (initial-mark), 0.0268358 secs]
[Parallel Time: 20.4 ms, GC Workers: 33]
[GC Worker Start (ms): Min: 3625.7, Avg: 3626.2, Max: 3626.7, Diff: 1.0]
[Ext Root Scanning (ms): Min: 0.4, Avg: 6.5, Max: 18.8, Diff: 18.4, Sum: 214.2]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 1.4, Max: 14.8, Diff: 14.8, Sum: 47.6]
[Object Copy (ms): Min: 0.0, Avg: 10.0, Max: 18.5, Diff: 18.5, Sum: 330.6]
[Termination (ms): Min: 0.0, Avg: 1.3, Max: 1.7, Diff: 1.7, Sum: 41.4]
[Termination Attempts: Min: 1, Avg: 15.4, Max: 30, Diff: 29, Sum: 509]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3]
[GC Worker Total (ms): Min: 18.8, Avg: 19.2, Max: 19.8, Diff: 1.0, Sum: 634.3]
[GC Worker End (ms): Min: 3645.4, Avg: 3645.5, Max: 3645.6, Diff: 0.1]
[Code Root Fixup: 0.2 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.8 ms]
[Other: 5.5 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 3.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 1.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.1 ms]
[Eden: 20.0M(488.0M)->0.0B(488.0M) Survivors: 24.0M->24.0M Heap: 41.4M(10.0G)->23.8M(10.0G)]
[Times: user=0.21 sys=0.00, real=0.03 secs]
2021-01-04T16:43:24.684+0800: 3.652: [GC concurrent-root-region-scan-start]
2021-01-04T16:43:24.689+0800: 3.657: [GC concurrent-root-region-scan-end, 0.0044182 secs]
2021-01-04T16:43:24.689+0800: 3.657: [GC concurrent-mark-start]
2021-01-04T16:43:24.690+0800: 3.658: [GC concurrent-mark-end, 0.0014218 secs]
2021-01-04T16:43:24.690+0800: 3.658: [GC remark 2021-01-04T16:43:24.690+0800: 3.658: [Finalize Marking, 0.0104477 secs] 2021-01-04T16:43:24.701+0800: 3.669: [GC ref-proc, 0.0008959 secs] 2021-01-04T16:43:24.702+0800: 3.670: [Unloading, 0.0049210 secs], 0.0176170 secs]
[Times: user=0.37 sys=0.00, real=0.02 secs]
2021-01-04T16:43:24.708+0800: 3.676: [GC cleanup 30M->30M(10G), 0.0044527 secs]
[Times: user=0.05 sys=0.01, real=0.00 secs]
5.4.1 如何减少GC