JVM深入理解及性能调优

jvm内存结构及性能调优

一、jvm简介

java虚拟机(java virtual machine jvm)一种能够运行java字节码的虚拟机。java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行,这实现了java的跨平台机制。
三种JVM:① Sun公司的HotSpot ② BEA公司的JRockit ③ IBM公司的J9 JVM

二、jvm内存结构

jvm由三个子系统组成:

  1. 内加载子系统
  2. 运行时数据区(内存结构)
  3. 执行引擎

jvm内存结构:
jvm内存结构
2.1 方法区(Method Area)
方法区用于存储虚拟机加载的:静态变量+常量+类信息+运行时常量池 (类信息:类的版本、字段、方法、接口、构造函数等描述信息 )方法区是全局共享的,在一定条件下也会被GC。当方法区超过它允许的大小时,就会抛出OutOfMemory:PermGen Space异常。
在Hotspot虚拟机中,这块区域对应持久代或永久代(Permanent Generation),一般来说,方法区上执行GC的情况很少,因此方法区被称为持久代的原因之一,但这并不代表方法区上完全没有GC,其上的GC主要针对常量池的回收和已加载类的卸载。在方法区上进行GC,条件相当苛刻而且困难。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译器生成的常量和引用。一般来说,常量的分配在编译时就能确定,但也不全是,也可以存储在运行时期产生的常量。比如String类的intern()方法,作用是String类维护了一个常量池,如果调用的字符”hello”已经在常量池中,则直接返回常量池中的地址,否则新建一个常量加入池中,并返回地址。

2.2 堆 (Heap)
堆区是GC最频繁的,也是理解GC机制最重要的区域。堆区由所有线程共享,在虚拟机启动时创建。堆区主要用于存放对象实例及数组,所有new出来的对象都存储在该区域。

2.3 虚拟机栈(VM Stack)
虚拟机栈占用的是操作系统内存,每个线程对应一个虚拟机栈,它是线程私有的,生命周期和线程一样,每个方法被执行时产生一个栈帧(Statck Frame),栈帧用于存储局部变量表、动态链接、操作数和方法出口等信息,当方法被调用时,栈帧入栈,当方法调用结束时,栈帧出栈。
局部变量表中存储着方法相关的局部变量,包括各种基本数据类型及对象的引用地址等,因此他有个特点:内存空间可以在编译期间就确定,运行时不再改变。
虚拟机栈定义了两种异常类型:StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)。如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError;不过大多数虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足时,抛出OutOfMemoryError。

2.4 本地方法栈(Native Method Stack)
本地方法栈用于支持native方法的执行,存储了每个native方法的执行状态。本地方法栈和虚拟机栈他们的运行机制一致,唯一的区别是,虚拟机栈执行Java方法,本地方法栈执行native方法。在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一起使用。

2.5 程序计数器(Program Counter Register)
程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,程序猿无法操作它,它的作用是:JVM在解释字节码(.class)文件时,存储当前线程执行的字节码行号(不是下一行),只是一种概念模型,各种JVM所采用的方式不一样。字节码解释器工作时,就是通过改变程序计数器的值来取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术区完成的。
每个程序计数器只能记录一个线程的行号,因此它是线程私有的。
如果程序当前正在执行的是一个java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是native方法,则计数器的值为空,此内存区是唯一不会抛出OutOfMemoryError的区域。

运行时栈帧结构
在这里插入图片描述
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈道出栈的过程。

在编译程序代码的时候,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入到方法的code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

(1)局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。

局部变量表的变量以变量槽(Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static方法),那么局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

局部变量表中的Slot是可重用的,方法体中定义的变量,其作用于并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用。

对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。

(2)操作数栈
操作数栈也常被称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也是在编译的时候被写入到Code属性的max_stacks数据项中。32位数据类型所占的栈容量位1,64位数据类型所占的栈容量位2.

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。举个例子:整数加法字节码指令iadd在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int类型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。

(3)动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

(4)方法出口
当一个方法被执行后,有两种方式退出这个方法。第一种方式是:执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值类型将根据遇到各种方法返回指令来决定,这种退出方法的方式称为正常完成出口。

另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器。就会导致方法退出,这种退出方法的方式称为异常完成出口。

无论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:回复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

在这里插入图片描述
Eden:尹甸区 , 新创建的对象
From,To:S0,S1区也称为幸存区,是两块大小相等,并且可以互换角色的空间
幸存区包括S0、S1区,或者说from区、to区
再经过一次GC之后,对象从from区,到to区
再经过一次GC之后,对象从to区,到form区
再经过一次GC之后,对象从from区,到to区
如此反复……

直到对象的年龄到达15岁之后,就会被放到老年区
这也是,为什么幸存区中的S0、S1区
两个区大小相等,并且可以互换角色

Young Generation:新生代 发生的GC 称为minor GC (young GC) ,
Old Generation: 老年代 发生的GC 称为MajorGC(Full GC ) ,如发生GC后内存仍然不够用将抛出 OutOfMemoryError异常
Meta Data:元空间 jdk1.8之前叫Permanent Generation 永久代 ,是非堆内存

三、垃圾回收常见算法及收集器

垃圾回收算法
1、引用计算法
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

2、复制算法
此算法把内存空间划分为两个相等的区域,每次只使用一个区域。垃圾回收时,遍历当前使用区域,把正在使用的对象复制到另外一个区域。此算法每次只处理正在使用的对象,因此复制成本小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然缺点也很明显需要两倍内存空间。
在这里插入图片描述

3、标记-清除法
此算法分为两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时会产生内存碎片。
在这里插入图片描述

4、标记-整理算法
此算法结合了标记-清除和复制两个算法的优点,也分为两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除并且把存活的对象“压缩”到堆的其中一块,按顺序存放。
在这里插入图片描述
5、分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。所以年轻代以复制算法为主,老年代以标记整理算法为主。
在这里插入图片描述
垃圾收集器
1、Serial收集器(复制算法) 串行
新生代单线程收集器,标记和清理都是单线程,优点是简单高效,但是会暂停其他所有工作线程。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
2、Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
3、ParNew收集器(停止-复制算法) 并行
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
4、Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用
-XX:ParallelGCThreads=4来指定线程数。
5、Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
6、CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
7、G1(Garbage-First)
在JDK 7u4版本之后发布的垃圾收集器,并在jdk9中成为默认垃圾收集器,其特点是高效且并发的,不需要分代配合不同的垃圾收集器,因为G1中的垃圾收集区域是“分区”(Region)的。启用G1 -XX:+UserG1GC
完整示例:

java -Xmx50m -Xms50m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

GC是什么时候触发的(面试最常见的问题之一)
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC
  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC
  对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
a) 老年代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;

四、jvm性能调优

1、jvm监控工具

a、jps
用来查看基于HotSpot JVM里面所有进程的具体状态, 包括进程ID,进程启动的路径等等
在这里插入图片描述
b、jstat
查看classloader,compiler,gc相关信息,实时监控资源和性能 。jstat工具特别强大,可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
语法: jstat -<option> [-t] [-h<lines>] <pid> [<interval> [<count>]]
Options — 通常使用 -gcutil 查看gc情况
interval – 间隔时间,单位为秒或者毫秒
count — 打印次数,如果缺省则打印无数次
示例:
在这里插入图片描述

c、jconsole 可以监控Java应用程序(如jar应用、tomcat等),但被监视的应用程序必须和jconsole是用同一个用户运行的。jvisualvm的使用和jconsole类似。
本地监控: jconsole pid
远程监控: jconsole [ hostname:portNum ]
使用远程监控需要配置jmx代理信息,修改Tomcat的bin目录下的catalina.bat。

set JAVA_OPTS= %JAVA_OPTS% -Djava.rmi.server.hostname=HostIP set
JAVA_OPTS= %JAVA_OPTS% -Dcom.sun.management.jmxremote.port=8888 set
JAVA_OPTS= %JAVA_OPTS% -Dcom.sun.management.jmxremote.ssl=false set
JAVA_OPTS= %JAVA_OPTS%
-Dcom.sun.management.jmxremote.authenticate=false

连接成功后可以在overview中查看内存、线程、类及CPU使用情况:
在这里插入图片描述
d、 jmap
打印java进程的堆内存信息。

jmap -heap pid         查看heap的概要信息,GC使用的算法、heap的配置及wise heap的使用情况.
jmap -histo[:live] pid      查看堆内存中的每个类的类名、实例数量、内存占用大小
jmap -dump:live, format=b, file=fileName pid       将内存使用情况导出到文件中,再用jhat、MAT、VisualVM分析查看,以便查找内存溢出原因

在这里插入图片描述
e、jstack
jstack用于生成java虚拟机当前时刻的线程快照,主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
语法:

 jstack [ option ] pid
    -F 当 jstack [-l] pid 没有响应的时候强制打印栈信息
    -l 长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表.

f、VisualVM
VisualVM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序(Java 应用程序)的详细信息。
VisualVM 是java jdk自带的最牛逼的调优工具了,最常使用的调优工具,几乎涉及了jvm调优的方方面面。
启动方式:直接在jdk/bin目录下面双击jvisualvm.exe既可使用
在这里插入图片描述
VisualVM可以根据需要安装不同的插件,每个插件的关注点都不同,有的主要监控GC,有的主要监控内存,有的监控线程等。
这里介绍下安装Visual GC插件

  1. 首先下载对应jdk版本的插件,https://visualvm.github.io/pluginscenters.html

  2. 找到对应自己jdk版本的更新地址
    在这里插入图片描述

  3. 在VisualVM 主菜单中选择工具 --》 插件–》设置 编辑插件地址, 将刚刚的地址复制上去,验证之后会发现可用插件栏显示了各自插件
    在这里插入图片描述

  4. 选择Visual GC 安装即可

在这里插入图片描述
5、安装完成后须重启才能生效
在这里插入图片描述
VisualVM更全面更直观一些,另外VisualVM非常多的其它功能,可以分析dump的内存快照,dump出来的线程快照并且进行分析等,还有其它很多的插件大家可以去探索。
在这里插入图片描述
2、JVM调优常用参数配置
堆配置

-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3表示Eden: 3 Survivor:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小

收集器设置

-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
-XX:+UseG1GC:设置G1并发收集器

垃圾回收统计信息
打印GC回收的过程日志信息

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

3、回收器选择
JVM给了三种选择:串行收集器,并行收集器,并发收集器,但是串行收集器只适用于小数据量的情况,一般不考虑使用了,所以这里只针对并行收集器和并发收集器。默认情况下,JDK5.0以前是使用的串行收集器,如果想使用其他收集器需要在启动时加入相应的参数,JDK5.0以后,JVM会根据系统当前的配置进行判断

吞吐量优先的并行收集器
并行收集器主要以到达一定的吞吐量为目标,适用于后台处理

java -Xmx3550m-Xms3550m-Xss128k-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。次配置仅对年轻代有效。即上述配置下,年轻代使用并行收集,而年老代仍旧使用串行收集。
-XX:PARALLELgcThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相同。
-XX:+UseParallelOldGC:配置年老代来及收集方式为并行收集,JDK6.0支持对年老代并行收集
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
-XX:+UseAdaptiveSizePolicy:设置此选项以后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开

响应时间优先的并发收集器
并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理、所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

调优总结

年轻代大小选择
响应时间优先的应用:尽可能设置大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达GB的程度,因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8核CPU以上应用。

老年代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可能会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考一下数据获得:
1、并发垃圾收集信息
2、持久代并发收集次数
3、传统GC信息
4、花在年轻代和年老代回收上的时间比例减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期对象,而年老代尽存放长期存活的对象

较小堆引起的碎片问题
因为年老代的并发收集器使用标记-清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次FullGc后,对年老代进行压缩

结语:之前也没怎么写过博客,最近开始慢慢把自己学习的知识总结一遍加深理解,如有错误遗漏之处还请一起讨论指出。 文章摘取了各大博主的片段及自己的总结,已表明参考文章,还请勿怪!

参考文章:
https://blog.csdn.net/xiaojie_570/article/details/80395066
https://www.cnblogs.com/aspirant/p/8662690.html
https://www.liangzl.com/get-article-detail-17354.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值