JVM及调优
程序员文章站
2022-06-01 12:05:55
...
Java虚拟机原理
所谓虚拟机,就是一台虚拟的机器。
它是一款软件,用来执行一系列虚拟计算机指令。
虚拟机大体上可以分为:
系统虚拟机(VMware):完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
程序虚拟机(Java虚拟机):专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令,称为Java字节码指令。
无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。
处理SUN的HotSpot虚拟机,还有BEA的JRockit虚拟机,现都属于Oracle。
JVM内部结构
1.类加载子系统
负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。
2.方法区:
存放类信息,常量信息,常量池信息,包括字符串字面量和数字常量等。
3.Java堆:
在Java启动的时候建立Java堆,它是Java最主要的内存工作区域,几乎所有的Java对象都存放到Java堆中。
堆空间是所有线程共享的。
4.直接内存:
Java的NIO库允许Java程序使用直接内存,从而提高性能。
通常直接内存速度会优于Java堆。
读写频繁的场合可能会考虑使用。
5.Java栈:
每个虚拟机程序都有一个私有的栈,一个线程的Java栈在线程创建的时候被创建。
Java栈中保存着局部变量,方法参数,同时Java的方法调用,返回值等。
栈解决程序的
6.本地方法栈:
和Java栈很类似。最大的不同为本地方法栈用于本地方法调用。
Java虚拟机允许Java直接调用本地方法(通常用C编写)
7.垃圾回收系统:
是Java的核心。
8.PC(Program Counter):
寄存器也是每个线程私有的空间,Java虚拟机会为每个线程创建PC寄存器。
在任意时刻,一个Java线程总是在执行一个方法,这个方法被称为当前方法。如果当前方法不是本地方法,PC寄存器就会执行当前正在被处理的指令;如果是本地方法,PC寄存器的值为undfined。
寄存器存放如当前执行环境指针,程序计数器,操作栈指针,计算的变量指针等信息。
9.虚拟机执行引擎:
虚拟机最核心的组件就是执行引擎了,它负责执行虚拟机的字节码。
一般会先进行编译成机器码后执行
堆、栈、方法区概念和联系
堆解决数据的存储问题,即数据怎么放,放在哪儿。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
方法区则是辅助堆栈的永久区,解决堆栈信息的产生,是先决条件。
用例说明:
我们创建一个新的对象User,那么User类的一些信息(类信息、静态信息都存于方法区中)。
而User类被实例化后,被存储于Java堆中的一块内存空间。
当我们去使用的时候,都是使用User对象的引用,形如:User user=new User()。
这里的user就存放在Java栈中,即User真实对象的一个引用。
Java堆
Java堆是和Java应用程序关系最密切的内存空间,几乎所有的对象都存放于堆中。
Java堆完全是自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理。,不需要显示的释放。
根据垃圾回收机制的不同,Java堆中可能有不同的结果。
最为常见的划分:
新生代:存放新生代对象或者年龄不大的对象
分为eden区,s0区,s1区。s0和s1也被称为from和to区域,它们是两块大小相等并且可以互换角色的空间
老年代:存放老年对象 (默认经过15次gc后进入老年代)
绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则进入s0和s1区,之后每经过一次新生代回收,如果对象还存活则年龄+1,当对象达到一定的年龄后,进入老年代。
Java栈
Java栈是一块线程私有的内部空间。
栈组成:局部变量表、操作数栈、帧数据区。
局部变量表:用于报错函数的参数及局部变量。
操作数栈:主要保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
帧数据区:处理局部变量表和操作数栈外,栈还需要一些数据来支撑常量池的解析。帧数据区保存着访问常量池的指针,方便程序访问常量池。另外当函数返回或出现异常时,虚拟机必须有一个异常处理表,方便发送异常的时候找到异常代码,因此,异常处理表也是帧数据区的一部分。
Java方法区
Java的方法区和堆区一样,方法区是一块所有线程共享的内存区域,它保存系统的类信息。比如类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类,如果系统定义太多类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。方法区可以理解为永久区。
虚拟机参数
在虚拟机运行过程中,如果跟踪系统的运行状态,那么对于问题的故障排查会有一定的帮助,为此,虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。我们进行虚拟机参数配置,其主要就是围绕着堆、栈、方法区进行配置。
1.堆分配参数
-XX:JVM级别的配置,若不是,表示的是应用级别
+:表示启用
-:表示禁用
-XX:+PrintGC 使用这个参数,虚拟机启动后,只要遇到GC就会打印日志(JVM级别)。
-XX:UseSerialGC 配置串行回收器(JVM级别)。
-XX:PrintGCDetails 可以查看详细信息,包括各个区的情况(JVM级别)。
-XX:PrintCommandLineFlags:可以将配置内容输出(JVM级别)。
-Xms:设置Java程序启动时初始堆的大小(应用级别)。
-Xmx:设置Java程序能获得的最大堆的大小(应用级别)。
例:-Xmx20m -Xms50m -XX:PrintCommandLineFlags:可以将配置内容输出
总结:在实际工作中,我们可以直接将初始堆大小和最大堆大小设置相等(80%也可以),可以减少垃圾回收次数,提高性能。
1.1 新生代配置
-Xmn:设置新生代的大小(绝对大小)。
设置一个比较大的新生代会减少老年代的大小。这个参数对系统性能以及GC行为有很大的影响。
新生代大小一般会设置整个堆空间的1/3到1/4左右。
-XX:NewRatio:新生代和老年代的比例 (老年代/新生代)
-XX:SurvivorRatio:设置新生代中eden空间和s0、s1空间的比例eden/from=eden/to
总结:
不同的堆分布情况,对系统执行会产生一定的影响。在实际工作中,应根据系统的特点作出合理的配置。
新生代空间太小时,新建对象会直接进入老年代,如果老年代GC次数过多,对应用的性能影响很大。
基本策略:尽可能将对象预留在新生代,减少老年代的GC次数。
堆溢出处理
在Java程序运行过程中,如果堆空间不足,则抛出内存溢出的错误,一旦发生在生产环境,可能引起严重的业务中断。
Java虚拟机提供了 -XX:+HeapDumpOnOutOfMemoryError,使用该参数可以在内存溢出时导出整个堆信息。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/test.dump:导出堆信息的存放路径
内存分析工具:Memory Analyzer
栈配置
-Xss:指定线程的最大栈空间,这个参数也直接决定了函数可调用的最大深度
局部变量表(就是定义的变量)内容越多,栈帧越大,栈深度越小
方法区配置
和堆一样,方法区是一块所有线程共享的内存区域。用于保存系统的类信息,方法区(永久区)可以保存多少信息可以进行配置。
在默认情况下,-XX:MaxPermSize为64M。
如果系统运行时产生大量的类,就需要设置一个合适的方法区,以免出现方法区内存溢出问题。
-XX:PermSize=64M
-XX:MaxPermSize=64M
直接内存配置(1.8以后已经优化,可以不用关注)
直接内存也是Java程序中非常重要的组成部分,特别是广泛应用于NIO中,直接内存跳过了Java堆,是Java程序可以直接访问原生堆空间,因此在一定程度上加快了内存空间的访问速度。
但是说直接内存一定就可以提高访问速度也不见得,具体情况具体分析。
-XX:MaxDirectMemorySize 如果不设置就默认为最大堆空间,即-Xmx
直接内存使用达到上限时,就会触发GC,如果不能有效回收,也会引起系统的OOM(outofmemory)
垃圾回收器
GC有很多种算法:引用计数法、标记压缩法、复制算法、分代分区思想
引用计数法:
这是个比较古老经典的垃圾收集算法,核心思想就是在对象被引用时+1,引用失败时-1,但是无法处理循环引用的情况。每次进行加减操作浪费系统资源。
标记清除法(此算法需要暂停整个应用,同时会产生内存碎片):
就是分为标记和清除两个阶段进行处理内存中的对象,弊端就是空间碎片的问题,垃圾回收后的空间是不连续的,不连续的内存空间的工作效率要低于连续的内存空间。
复制算法:
核心思想就是将内存分为两块,每次使用其中一块,在垃圾回收时,将正在使用的内存中的留存对象复制到未使用的内存中去,然后清楚之前使用的内存空间,反复去交换这两个内存的角色,完成垃圾收集(新生代使用的就是复制算法)。
标记压缩法(标记-整理算法):
就是在标记清除法的基础上做了优化,把存活的对象压缩到内存的一端,而后进行垃圾清除。(Java中老年代使用的就是标记压缩法)。
分代算法:
就是根据对象的特点把内存分为N块,然后根据每个内存的特点使用不同的算法。
对于新生代和老年代,新生代回收频率高,但是每次回收耗时很短,而老年代回收频率低,但是耗时会相对较长,所以应该尽量减少老年代的GC。
分区算法:
将整个内存分为N个小的独立空间,每个小空间都可以独立使用,这样细粒度的控制一次回收多少个小空间,而不用对整个空间GC,减少了GC停顿,从而提高性能(G1垃圾手收集器使用的就是分区算法jdk11)。
JVM中的垃圾收集器:
Scavenge GC(次收集器)和Full GC(全收集器)
Scavenge GC(次收集器):发生在新生代,eden空间不足时触发。收集非常频繁,回收速度快
Full GC(全收集器):发生在老年代。速度比新生代GC慢10倍以上,内存不足或者显示调用System.gc()时触发
分代回收器:
新生代收集器:
serial:
采用复制算法。
串行收集器(stop the world),client端默认收集器。
虽然是单线程收集,但是简单高效,在VM管理内存不大的情况下,停顿时间可以控制在100毫秒以内。
可以跟cms配合使用。
ParNew:
采用复制算法。
并行收集器(也会stop the world),就是serial的多线程版本,使用多条线程进行GC,停顿时间缩短了,其他内容完全与serial一样。
-XX:ParallelGCThread=n 控制GC线程数
除了serial外,只有它可以跟cms配合使用
parallel Scavenge:
也是使用复制算法,也是并行收集器,但是更关注吞吐量。
吞吐量=cpu运行用户代码时间/(cpu运行用户代码时间+垃圾收集时间)
可以高效的的利用cpu的时间,尽快完成运算任务
-XX:MaxGCPauseMillis 内存回收时间不超过设定值,但是太小会增加GC次数
-XX:GCTimeRatio GC时间占总时间的比率
老年代收集器:
cms(concurrent mark sweep):
使用标记-清除算法。
以获取最短停顿时间为目标。
可以和新生代的serial、ParNew配合使用。
serial old:
使用标记-整理算法。
可以和新生代的serial、ParNew以及Parallel Scavenge配合使用(兼容性好)。
parallel old:
也是使用标记-整理算法,也是并行收集器,但是更关注吞吐量
只能和新生代的Parallel Scavenge配合使用。
新老贯穿的收集器(最新的收集器,分区收集器)
G1(garbage first):
是一款面向服务端应用的收集器,主要目标是用于配备多个CPU的服务器治理大内存。
与其他基于分代的收集器不同,G1将整个堆内存划分为多个大小相等的独立区域。
虽然还保留新生代和老年代的概念,但新老年代不在是物理隔离,它们都是一部分区域(不需要连续)的集合。
不是默认的收集器。
TLAB
Thread Local Allocation Buffer 线程本地分配缓存
线程专用的内存分配区域,为了加速对象分配而生的。
每一个线程都会产生一个TLAB,该线程独享的工作区域,Java虚拟机使用这种TLAB区来避免多线程冲突问题,提高了对象分配效率。
TLAB空间不会太大,当大对象无法在TLAB上分配时,则直接分配到堆上。
-XX:+UseTLAB 使用TLAB
-XX:+TLABSize 设置TLAB大小
-XX:TLABRefillWasterFraction 设置维护进入TLAB空间的单个对象大小,是一个比例值,默认为64,即如果对象大于整个空间的1/64,则在堆创建对象
-XX:PrintTLAB 查看TLAB信息
-XX:ResizeTLAB 自调整TLABRefillWasterFraction阀值
常见JVM参数
JVM设置(内存空间):
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n 设置年轻代大小
-XX:NewRatio=n 老年代/年轻代
-XX:SurvivorRatio=n 如3,等价于eden=3,s0=1,s1=1
-XX:MaxPermSize=n 设置持久区的大小
GC设置:
-XX:+UseSerialGC 设置串行收集器
-XX:+UseParallelGC 设置并行收集器
-XX:+UseParallelOldGC 设置并行老年代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器
GC统计信息:
-XX:+PrintGC
-XX:+PrintDetails
-XX:+PrintGCTimeStamps
-XLoggc:filename
并行收集器设置:
...
例:吞吐量优先的并行收集器
-Xmx3800m
-Xms3800m
-Xmn2g
-Xss128k
-XX:+UseParallelGC
-XX:+UseParallelGCThreads=4(最好等于处理器的逻辑核心数)
调优总结
1.年轻代大小选择:
1.1响应时间优先的应用:
尽可能设大,直到接近系统的最低响应时间限制。在这种情况下,年轻代GC的频率是最小的,同时减少达到老年代的对象。
1.2吞吐量优先的应用:
尽可能的设大,可能达到Gbit的程度。因为响应时间没有要求,GC可以并行运行,一般适合8CPU以上的应用。
2.老年代大小选择:
2.1响应时间优先的应用:
老年代使用并发GC,所以其大小需要小心设置,一般要考虑并发会话率和会话持久时间等一些参数,如果堆设置小了,可能会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆设置大了,则需要较长的收集时间。
最优的方案,一般需要参考以下数据获得:
并发收集信息
持久代并发收集次数
传统GC信息
花在年轻代和老年代回收上的时间比例
减少老年代和年轻代花费的时间,一般会提高应用的效率
2.2吞吐量优先的应用:
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代,这样可以尽可能回收掉大部分的短期对象,减少中期对象,老年代存放长时间存活的对象。
较小堆引起空间碎片问题,因为老年代的并发GC使用标记清除算法,所以不会对堆进行压缩。当GC时,它会把相邻的空间合并,这样可以分配给较大的对象。但是当堆空间较小时,运行一段时间后就会出现碎片,如果并发收集器找不到足够的空间,那么并发收集器就会停止,然后转用传统的标记清除算法进行回收。
如果出现碎片,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection 使用并发收集器,开启对老年代的压缩
-XX:+CMSFullGCsBeforeCompaction=0 上面配置开启的情况下,设置多少次Full GC后对老年代进行压缩
JVM性能检测工具
jps:查看JVM中的线程
jstat:查看HotSpot VM运行时的信息(类加载、内存、GC分代、jit编译),有很多参数
jinfo:查看和修改虚拟机的各项配置
jvisualvm:jdk中最强大运行监视和故障处理工具