目录
5.内存管理
5.1内存结构
5.1.1逻辑分区
JVM内存从应用逻辑上可分为如下区域。
程序计数器:字节码行号指示器,每个线程需要一个程序计数器
虚拟机栈:方法执行时创建栈帧(存储局部变量,操作栈,动态链接,方法出口)编译时期就能确定占用空间大小,线程请求的栈深度超过jvm运行深度时抛StackOverflowError,当jvm栈无法申请到空闲内存时抛OutOfMemoryError,通过-Xss,-Xsx来配置初始内存
本地方法栈:执行本地方法,如操作系统native接口
堆:存放对象的空间,通过-Xmx,-Xms配置堆大小,当堆无法申请到内存时抛OutOfMemoryError
方法区:存储类数据,常量,常量池,静态变量,通过MaxPermSize参数配置
对象访问:初始化一个对象,其引用存放于栈帧,对象存放于堆内存,对象包含属性信息和该对象父类、接口等类型数据(该类型数据存储在方法区空间,对象拥有类型数据的地址)
而实际上JVM 内存分类实际上的物理分区还有更为详细,整体上分为堆内存和非堆内存,具体介绍如下。
5.1.2内存模型
堆内存
堆内存是运行时的数据区,从中分配所有java 类实例和数组的内存,可以理解为目标应用依赖的对象。堆在JVM启动时创建,并且在应用程序运行时可能会增大或减小。可以使用-Xms 选项指定堆的大小。堆可以是固定大小或可变大小,具体取决于垃圾收集策略。可以使用-Xmx选项设置最大堆大小。默认情况下,最大堆大小设置为64 MB。
JVM堆内存在物理上分为两部分:新生代和老年代。新生代是为分配新对象而保留堆空间。当新生代占用完时,Minor GC垃圾收集器会对新生代区域执行垃圾回收动作,其中在新生代中生活了足够长的所有对象被迁移到老年代,从而释放新生代空间以进行更多的对象分配。此垃圾收集称为 Minor GC。新生代分为三个子区域:伊甸园Eden区和两个幸存区S0和S1。
关于新生代内存空间:
大多数新创建的对象都位于Eden区内存空间
当Eden区填满对象时,执行Minor GC并将所有幸存对象移动到其中一个幸存区空间
Minor GC还会检查幸存区对象并将其移动到其他幸存者空间,也即是幸存区总有一个是空的
在多次GC后还存活的对象被移动到老年代内存空间。至于经过多少次GC晋升老年代则由参数配置,通常为15
当老年区填满时,老年区同样会执行垃圾回收,老年区还包含那些经过多次Minor GC后还存活的长寿对象。垃圾收集器在老年代内存中执行的回收称为Major GC,通常需要更长的时间。
非堆内存
JVM的堆以外内存称为非堆内存。也即是JVM自身预留的内存区域,包含JVM缓存空间,类结构如常量池、字段和方法数据,方法,构造方法。类非堆内存的默认最大大小为64 MB。可以使用-XX:MaxPermSize VM选项更改此选项,非堆内存通常包含如下性质的区域空间:
-
元空间(Metaspace)
在Java 8以上版本已经没有Perm Gen这块区域了,这也意味着不会再由关于“java.lang.OutOfMemoryError:PermGen”内存问题存在了。与驻留在Java堆中的Perm Gen不同,Metaspace不是堆的一部分。类元数据多数情况下都是从本地内存中分配的。默认情况下,元空间会自动增加其大小(直接由底层操作系统提供),而Perm Gen始终具有固定的上限。可以使用两个新标志来设置Metaspace的大小,它们是:“ - XX:MetaspaceSize ”和“ -XX:MaxMetaspaceSize ”。
Metaspace背后的含义是类的生命周期及其元数据与类加载器的生命周期相匹配。也就是说,只要类加载器处于活动状态,元数据就会在元数据空间中保持活动状态,并且无法释放。
-
代码缓存
运行Java程序时,它以分层方式执行代码。在第一层,它使用客户端编译器(C1编译器)来编译代码。分析数据用于服务器编译的第二层(C2编译器),以优化的方式编译该代码。默认情况下,Java 7中未启用分层编译,但在Java 8中启用了分层编译。实时(JIT)编译器将编译的代码存储在称为代码缓存的区域中。它是一个保存已编译代码的特殊堆。如果该区域的大小超过阈值,则该区域将被刷新,并且GC不会重新定位这些对象。Java 8中已经解决了一些性能问题和编译器未重新启用的问题,并且在Java 7中避免这些问题的解决方案之一是将代码缓存的大小增加到一个永远不会达到的程度。
-
方法区
方法区域是Perm Gen中空间的一部分,用于存储类结构(运行时常量和静态变量)以及方法和构造函数的代码。
-
内存池
内存池由JVM内存管理器创建,用于创建不可变对象池。内存池可以属于Heap或Perm Gen,具体取决于JVM内存管理器实现。
-
常量池
常量包含类运行时常量和静态方法,常量池是方法区域的一部分。
-
Java堆栈内存
Java堆栈内存用于执行线程。它们包含特定于方法的特定值,以及对从该方法引用的堆中其他对象的引用。
-
Java堆内存配置项
Java提供了许多内存配置项,我们可以使用它们来设置内存大小及其比例,常用的如下:
VM Switch | 描述 |
---|---|
- Xms | 用于在JVM启动时设置初始堆大小 |
-Xmx | 用于设置最大堆大小 |
-Xmn | 设置新生区的大小,剩下的空间用于老年区 |
-XX:PermGen | 用于设置永久区存初始大小 |
-XX:MaxPermGen | 用于设置Perm Gen的最大尺寸 |
-XX:SurvivorRatio | 提供Eden区域的比例 |
-XX:NewRatio | 用于提供老年代/新生代大小的比例,默认值为2 |
5.2垃圾回收
5.2.1垃圾回收策略
流程
垃圾收集是释放堆中的空间以分配新对象的过程。垃圾收集器是JVM管理的进程,它可以查看内存中的所有对象,并找出程序任何部分未引用的对象,删除并回收空间以分配给其他对象。通常会经过如下步骤:
标记:标记哪些对象被使用,哪些已经是无法触达的无用对象
删除:删除无用对象并回收要分配给其他对象
压缩:性能考虑,在删除无用的对象后,会将所有幸存对象集中移动到一起,腾出整段空间
策略
虚拟机栈、本地栈和程序计数器在编译完毕后已经可以确定所需内存空间,程序执行完毕后也会自动释放所有内存空间,所以不需要进行动态回收优化。JVM内存调优主要针对堆和方法区两大区域的内存。通常对象分为Strong、soft、weak和phantom四种类型,强引用不会被回收,软引用在内存达到溢出边界时回收,弱引用在每次回收周期时回收,虚引用专门被标记为回收对象,具体回收策略如下:
对象优先在Eden区分配:
新生对象回收策略Minor GC(频繁)
老年代对象回收策略Full GC/Major GC(慢)
大对象直接进入老年代:超过3m的对象直接进入老年区 -XX:PretenureSizeThreshold=3145728(3M)
长期存活对象进入老年区:
Survivor区中的对象经历一次Minor GC年龄增加一岁,超过15岁进入老年区
-XX:MaxTenuringThreshold=15动态对象年龄判定:设置Survivor区对象占用一半空间以上的对象进入老年区
算法
垃圾收集有如下常用的算法:
标记-清除
复制
标记-整理
分代收集(新生用复制,老年用标记-整理)
5.2.2垃圾回收器
分类
serial收集器:单线程,主要用于client模式
ParNew收集器:多线程版的serial,主要用于server模式
Parallel Scavenge收集器:线程可控吞吐量(用户代码时间/用户代码时间+垃圾收集时间),自动调节吞吐量,用户新生代内存区
Serial Old收集器:老年版本serial
Parallel Old收集器:老年版本Parallel Scavenge
CMS(Concurrent Mark Sweep)收集器:停顿时间短,并发收集
G1收集器:分块标记整理,不产生碎片
配置
串行GC(-XX:+ UseSerialGC):串行GC使用简单的标记-扫描-整理方法,用于新生代和老年代的垃圾收集,即Minor和Major GC
并行GC(-XX:+ UseParallelGC):并行GC与串行GC相同,不同之处在于它为新生代垃圾收集生成N个线程,其中N是系统中的CPU核心数。我们可以使用-XX:ParallelGCThreads = n JVM选项来控制线程数
并行旧GC(-XX:+ UseParallelOldGC):这与Parallel GC相同,只是它为新生代和老年代垃圾收集使用多个线程
并发标记扫描(CMS)收集器(-XX:+ UseConcMarkSweepGC):CMS也称为并发低暂停收集器。它为老年代做垃圾收集。CMS收集器尝试通过在应用程序线程内同时执行大多数垃圾收集工作来最小化由于垃圾收集而导致的暂停。年轻一代的CMS收集器使用与并行收集器相同的算法。我们可以使用-XX限制CMS收集器中的线程数 :ParallelCMSThreads = n
G1垃圾收集器(-XX:+ UseG1GC):G1从长远看要是替换CMS收集器。G1收集器是并行,并发和递增紧凑的低暂停垃圾收集器。G1收集器不像其他收集器那样工作,并且没有年轻和老一代空间的概念。它将堆空间划分为多个大小相等的堆区域。当调用垃圾收集器时,它首先收集具有较少实时数据的区域,因此称为“Garbage First”也即是G1
6.执行引擎
6.1执行流程
类加载器加载的类文件字节码数据流由基于JVM指令集架构的执行引擎来执行。执行引擎以指令为单位读取Java字节码。我们知道汇编执行的流程是CPU执行每一行的汇编指令,同样JVM执行引擎就像CPU一个接一个地执行机器命令。字节码的每个命令都包含一个1字节的OpCode和附加的操作数。执行引擎获取一个OpCode并使用操作数执行任务,然后执行下一个OpCode。但Java是用人们可以理解的语言编写的,而不是用机器直接执行的语言编写的。因此执行引擎必须将字节码更改为JVM中的机器可以执行的语言。字节码可以通过以下两种方式之一转化为合适的语言。
解释器:逐个读取,解释和执行字节码指令。当它逐个解释和执行指令时,它可以快速解释一个字节码,但是同时也只能相对缓慢的地执行解释结果,这是解释语言的缺点。
JIT(实时)编译器:引入了JIT编译器来弥补解释器的缺点。执行引擎首先作为解释器运行,并在适当的时候,JIT编译器编译整个字节码以将其更改为本机代码。之后,执行引擎不再解释该方法,而是直接使用本机代码执行。本地代码中的执行比逐个解释指令要快得多。由于本机代码存储在高速缓存中,因此可以快速执行编译的代码。
但是,JIT编译器编译代码需要花费更多的时间,而不是解释器逐个解释代码。因此,如果代码只执行一次,最好是选择解释而不是编译。因此,使用JIT编译器的JVM在内部检查方法执行的频率,并仅在频率高于某个级别时编译方法。
JVM规范中未定义执行引擎的运行方式。因此,JVM厂商使用各种技术改进其执行引擎,并引入各种类型的JIT编译器。 大多数JIT编译器运行如下图所示:
JIT编译器将字节码转换为中间级表达式IR,以执行优化,然后将表达式转换为本机代码。Oracle Hotspot VM使用名为Hotspot Compiler的JIT编译器。它被称为Hotspot,因为Hotspot Compiler通过分析搜索需要以最高优先级进行编译的“Hotspot”,然后将热点编译为本机代码。如果不再频繁调用编译了字节码的方法,换句话说,如果该方法不再是热点,则Hotspot VM将从缓存中删除本机代码并以解释器模式运行。Hotspot VM分为服务器VM和客户端VM,两个VM使用不同的JIT编译器。
大多数Java性能改进都是通过改进执行引擎来实现的。除了JIT编译器之外,还引入了各种优化技术,因此可以不断改进JVM性能。初始JVM和最新JVM之间的最大区别是执行引擎。
下面我们通过下图可以看出JAVA执行的流程。
6.2栈帧结构
每个方法调用开始到执行完成的过程,对应这一个栈帧在虚拟机栈里面从入栈到出栈的过程。
栈帧包含:局部变量表,操作数栈,动态连接,方法返回
方法调用:方法调用不等于方法执行,而且确定调用方法的版本。
方法调用字节码指令:invokestatic,invokespecial,invokevirtual,invokeinterface
静态分派:静态类型,实际类型,编译器重载时通过参数的静态类型来确定方法的版本。(选方法)
动态分派:invokevirtual指令把类方法符号引用解析到不同直接引用上,来确定栈顶的实际对象(选对象)
单分派:静态多分派,相同指令有多个方法版本。
多分派:动态单分派,方法接受者只能确定唯一一个。
下图是JVM实例执行方法时的内存布局。
6.3早期编译
javac编译器:解析与符号表填充,注解处理,生成字节码
java语法糖:语法糖有助于代码开发,但是编译后就会解开糖衣,还原到基础语法的class二进制文件
重载要求方法具备不同的特征签名(不包括返回值),但是class文件中,只要描述不是完全一致的方法就可以共存。
6.4晚期编译
HotSpot虚拟机内的即时编译
解析模式 -Xint
编译模式 -Xcomp
混合模式 Mixed mode
分层编译:解释执行 -> C1(Client Compiler)编译 -> C2编译(Server Compiler)
触发条件:基于采样的热点探测,基于计数器的热点探测
7.性能调优
7.1调优原则
我们知道调优的前提是,程序没有达到我们的预期要求,那么第一步要做的是衡量我们的预期。程序不可能十全十美,我们要做的是通过各种指标来衡量系统的性能,最终整体达到我们的要求。
7.1.1 环境
首先我们要了解系统的运行环境,包括操作系统层面的差异,JVM版本,位数,乃至于硬件的时钟周期,总线设计甚至机房温度,都可能是我们需要考虑的前置条件。
7.1.2 度量
首先我们要先给出系统的预期指标,在特定的硬件/软件的配置,然后给出目标指标,比如系统整体输出接口的QPS,RT,或者更进一层,IO读写,cpu的load指标,内存的使用率,GC情况都是我们需要预先考察的对象。
7.1.3 监测
确定了环境前置条件,分析了度量指标,第三步是通过工具来监测指标,下一节提供了常用JVM调优工具,可以通过不同工具的组合来发现定位问题,结合JVM的工作机制已经操作系统层面的调度流程,按图索骥来发现问题,找出问题后才能进行优化。
7.1.4 原则
总体的调优原则如下图
图片来源《Java Performance》
7.2 调优参数
上节给出了JVM性能调优的原则,我们理清思路后应用不同的JVM工具来发现系统存在的问题,下面列举的是常用的JVM参数,通过这些参数指标可以更快的帮助我们定位出问题所在。
7.2.1内存查询
最常见的与性能相关的做法之一是根据应用程序要求初始化堆内存。这就是我们应该指定最小和最大堆大小的原因。以下参数可用于实现它:
-Xms<heap size>[unit] -Xmx<heap size>[unit]
unit表示要初始化内存(由堆大小表示)的单元。单位可以标记为GB的“g”,MB的“m”和KB的“k”。例如JVM分配最小2 GB和最大5 GB:
-Xms2G -Xmx5G
从Java 8开始Metaspace的大小未被定义,一旦达到限制JVM会自动增加它,为了避免不必要的不稳定性,我们可以设置Metaspace大小:
-XX:MaxMetaspaceSize=<metaspace size>[unit]
默认情况下YG的最小大小为1310 MB,最大大小不受限制,我们可以明确地指定它们:
-XX:NewSize=<young size>[unit]
-XX:MaxNewSize=<young size>[unit]
7.2.2垃圾回收
JVM有四种类型的GC实现:
串行垃圾收集器
并行垃圾收集器
CMS垃圾收集器
G1垃圾收集器
可以使用以下参数声明这些实现:
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+USeParNewGC
-XX:+UseG1GC
7.2.3GC记录
要严格监视应用程序运行状况,我们应始终检查JVM的垃圾收集性能,使用以下参数,我们可以记录GC活动:
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=< number of log files >
-XX:GCLogFileSize=< file size >[ unit ]
-Xloggc:/path/to/gc.log
UseGCLogFileRotation指定日志文件滚动的政策,就像log4j的,s4lj等 NumberOfGCLogFiles表示单个应用程序记录生命周期日志文件的最大数量。GCLogFileSize指定文件的最大大小。 loggc表示其位置。这里要注意的是,还有两个可用的JVM参数(-XX:+ PrintGCTimeStamps和-XX:+ PrintGCDateStamps),可用于在GC日志中打印日期时间戳。
7.2.4内存溢出
大型应用程序面临内存不足的错误是很常见的,这是一个非常关键的场景,很难复制以解决问题。
这就是JVM带有一些参数的原因,这些参数将堆内存转储到一个物理文件中,以后可以用它来查找泄漏:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit
这里有几点需要注意:
在OutOfMemoryError的情况下, HeapDumpOnOutOfMemoryError指示JVM将堆转储到物理文件中
HeapDumpPath表示要写入文件的路径; 任何文件名都可以给出; 但是如果JVM在名称中找到 标记,则导致内存不足错误的进程ID将以 .hprof格式附加到文件名
OnOutOfMemoryError用于发出紧急命令,以便在出现内存不足错误时执行; 应该在cmd args的空间中使用正确的命令。例如,如果我们想在内存不足时重新启动服务器,我们可以设置参数:
-XX:OnOutOfMemoryError="shutdown -r"
UseGCOverheadLimit是一种策略,用于限制在抛出 OutOfMemory错误之前在GC中花费的VM时间的比例
7.2.5其他配置
-server:启用“Server Hotspot VM”; 默认情况下,此参数在64位JVM中使用
-XX:+ UseStringDeduplication: Java 8引入了这个JVM参数,通过创建相同 String的太多实例来减少不必要的内存使用 ; 这通过将重复的 String值减少到单个全局char []数组来优化堆内存
-XX:+ UseLWPSynchronization:设置基于 LWP(轻量级进程)的同步策略而不是基于线程的同步
-XX:LargePageSizeInBytes:设置用于Java堆的大页面大小; 它采用GB / MB / KB的参数; 通过更大的页面大小,我们可以更好地利用虚拟内存硬件资源; 但是这可能会导致 PermGen的空间大小增加,从而可以强制减小Java堆空间的大小
-XX:MaxHeapFreeRatio:设置 GC后堆的最大自由百分比,以避免收缩
-XX:MinHeapFreeRatio:设置 GC后堆的最小自由百分比以避免扩展,监视堆使用情况
-XX:SurvivorRatio:Eden区 /幸存者空间大小的比例
-XX:+ UseLargePages:如果系统支持,则使用大页面内存; 如果使用此JVM参数,OpenJDK 7往往会崩溃
-XX:+ UseStringCache:启用字符串池中可用的常用分配字符串的缓存
-XX:+ UseCompressedStrings:对 String对象使用 byte []类型,可以用纯ASCII格式表示
-XX:+ OptimizeStringConcat:它尽可能优化字符串连接操作
7.3 调优工具
7.3.1命令行工具
-
虚拟机进程状况工具:jps -lvm
-
诊断命令工具:jcmd
用来发送诊断命令请求到JVM,这些请求是控制Java的运行记录,它必须在运行JVM的同一台机器上使用,并且具有用于启动JVM的相同有效用户和分组,可以使用以下命令创建堆转储(hprof转储):
jcmd GC.heap_dump filename =
-
虚拟机统计信息监视工具:jstat
提供有关运行的应用程序的性能和资源消耗的信息。在诊断性能问题时,可以使用该工具,特别是与堆大小调整和垃圾回收相关的问题。jstat不需要虚拟机启动任何特殊配置。
jstat -gc pid interval count
-
java配置信息工具:jinfo
jinfo -flag pid
-
java内存映像工具:jmap
用于生成堆转储文件
jmap -dump:format=b,file=java.bin pid
-
虚拟机堆转储快照分析工具:jhat
jhat file 分析堆转储文件,通过浏览器访问分析文件
-
java堆栈跟踪工具:jstack
用于生成虚拟机当前时刻的线程快照threaddump或者Javacore
jstack [ option ] vmid
-
堆和CPU分析工具:HPROF
HPROF是每个JDK版本附带的堆和CPU分析工具。它是一个动态链接库(DLL),它使用Java虚拟机工具接口(JVMTI)与JVM连接。该工具将分析信息以ASCII或二进制格式写入文件或套接字。HPROF工具能够显示CPU使用情况,堆分配统计信息和监视争用配置文件。此外,它还可以报告JVM中所有监视器和线程的完整堆转储和状态。在诊断问题方面,HPROF在分析性能,锁争用,内存泄漏和其他问题时非常有用。
java -agentlib:hprof = heap = sites target.class
7.3.2可视化工具
-
jconsole
-
jvisualvm
参考
公众号:编程原理林振华 编程原理