这是我在公司给团队小伙伴一次技术小分享。
新手司机可以收藏、学习,老司机可以批评指正。ps:
内容参考了众多优秀博文、书籍,部分图片来源于博文,如有侵权请联系删除。
1. 前言
为什么Java
可以实现所谓的“一次编写,到处运行”,主要是因为虚拟机的存在。Java
虚拟机负责Java
程序设计语言的安全特性和平台无关性。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java语言编译器只需要生成在Java
虚拟机上运行的字节码,就可以在多种平台上不加修改地运行。Java虚拟机使得Java摆脱了具体机器的束缚,使跨越不同平台编写程序成为了可能。
Java虚拟机基本上都是JDK
自带的虚拟机HotSpot
,这款虚拟机也是目前商用虚拟中市场份额最大的一款虚拟机,可以通过在命令行程序中输入java -version
来查看:
其实市面上还有很多别的优秀的虚拟机。Sun公司除了有大名鼎鼎的HotSpot
外,还有KVM
、Squawk VM
、Maxine VM
,BEA公司有JRockit VM
、IBM公司有J9 VM
等等。
2. 内存模型(JMM)
Java
虚拟机(JVM
)内部定义了程序在运行时需要使用到的内存区域。内存区域主要分为主内存和工作内存。主内存即主机物理内存,工作内存按作用域可划分为线程独享区和线程共享区。
宏观来看是这样子的,如下图:
Java内存模型规定了所有的变量都存储在主内存(Main Memory
)中,每条线程还有自己的工作内存(Working Memory
),线程的工作内存中保存了被该线程使用到的变量和主内存副本拷贝,线程对变量所有的操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,
Jvm运行时内存模型,不包含主内存。如下图:
下面将会逐一详细介绍上面的内存区域。
2.1 线程独享区
- 虚拟机栈,即
Java stack
。声明周期和线程相同,方法执行会创建栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。 - 本地方法栈,即
Native method stack
。作用和虚拟机栈一样,不过面向的是本地方法。不属于jvm
规范,hotspot
没有这块区域。 - 程序计数器,即
Program counter register
。区域小,是线程执行的字节码的行号指示器,相当于存的是一条条的指令。
2.2 线程共享区
- 堆
用于存放对象实例,是所有内存区域中最大的一块。实际上这块内存还被划分的更细:新生代和老年代,空间占用比例为
1 : 2
,新生代再细致一点有:Eden空间、From Survivor(S0)、To Survivor(S1),空间占用比例为8 : 1 : 1
。进一步划分的目的是更好地回收内存,或者更快地分配内存。 - 方法区
用于存放虚拟机加载的类信息、常量(常量池)、静态变量、即使编译器编译后的代码等数据,即“HotSpot”的永久代。在JDK 7之后,我们使用的HotSpot应该就没有永久代这个概念,采用的是Native Memory来实现方法区的规划。
2.3 直接内存
直接内存,即主内存,并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
JDK1.4中新加入的NIO(New Input/Output)类,可以直接使用Native函数库直接分配堆外内存,这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。
3. 垃圾回收(GC)
哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。那么如何确定要回收的对象,以及采用什么样的策略去回收,适合什么样的场景,这是我们要关注的几个点。
3.1 确定对象算法
了解一个对象满足什么样的条件就认为是可被回收的对象是重要的一环。
3.1.1 引用计数法
给对象添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1
;当引用失效时,计数器值-1
。当计数值为0的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。
public class ReferenceCountingGC{
public static void main(String[] args){
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}
3.1.2 可达性分析法
这个算法的基本思想是通过一系列称为GC Roots
的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。在Java语言中可以作为GC Roots
的对象包括:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即
Native
方法)引用的对象
3.2 回收算法
3.2.1 复制算法
采用内存空间比例为1 : 1
的2块内存上,只使用其中一块,当需要回收时,将存活的对象复制到另外一块,原有的那一块内存空间直接全部清除。这种算法比较简单粗暴,缺点也很明显,内存只能使用1/2
。
3.2.2 标记-清除算法
对标识为可清理的对象直接进行清理操作,不会发生复制或者移动,相对复制算法成本比较小。缺点:对标记的对象清除之后,由于未移动过对象,将产生大量不连续的内存碎片,当大对象出现时,由于没有足够的连续内存导致不得不对碎片进行整理,也就是Full GC。
3.2.3 标记-整理算法
标记-整理算法能够解决标记-清除算法带来的碎片化问题
3.3 垃圾收集器
根据上面提到的回收算法,jvm
内置了拥有众多的收集器来适应不同的场景。根据运行环境的物理配置信息,会自动的选择使用client模式、server模式的垃圾收集器,还可以继续根据运行时数据的情况来筛选适合当前场景的垃圾收集器。
上图展示了新生代和老年代的几种垃圾收集器,其中有连线的代表是可以组合使用的。
4. jvm参数
4.1 参数规则
- 标准参数:例如
javap -verbose
-
-X
参数:所有的这类参数都以-X
开始,例如常用的-Xmx
, - 布尔类型的参数:
+
或-
,然后才设置JVM
选项的实际名称。例如,-XX:+
类似true
,表启用,-XX:-
类似false
。 - 非布尔值的参数:如
string
或者integer
,我们先写参数的名称,后面加上=
,最后赋值。例如-XX:ParamName
=Value
4.2 常用参数清单
-
-Xms
即-XX:InitialHeapSize
的缩写,指定JVM的初始内存大小-Xms20M 设置JVM启动内存的最小值为20M,必须以M为单位
-
-Xmx
即-XX:MaxHeapSize
的缩写,指定JVM的最大堆内存大小-Xmx20M 表示设置JVM启动内存的最大值为20M,单位为M,将-Xmx和-Xms设置为一样可以避免JVM内存自动扩展。
-
-verbose:gc
输出虚拟机中GC的详细情况 -
-Xss128k
设置虚拟机栈的大小为128k -
-Xoss128k
设置本地方法栈的大小为128k。HotSpot不区分虚拟机栈和本地方法栈,因此对于HotSpot这个参数无效。 -
-XX:PermSize=10M
JVM初始分配的永久代的容量,必须以M为单位 -
-XX:MaxPermSize=10M
JVM允许分配的永久代的最大容量,必须以M为单位,大部分情况下这个参数默认为64M -
-Xnoclassgc
关闭JVM对类的垃圾回收 -
-XX:+TraceClassLoading
查看类的加载信息 -
-XX:+TraceClassUnLoading
查看类的卸载信息 -
-XX:NewRatio=4
设置年轻代:老年代的大小比值为1:4,这意味着年轻代占整个堆的1/5 -
-XX:SurvivorRatio=8
设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8 -
-Xmn20M
设置年轻代的大小为20M -
-XX:+HeapDumpOnOutOfMemoryError
可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照 -
-XX:+UseG1GC
让JVM使用G1垃圾收集器 -
-XX:+PrintGCDetails
在控制台上打印出GC具体细节 -
-XX:+PrintGC
在控制台上打印出GC信息 -
-XX:PretenureSizeThreshold=3145728
对象大于3145728(3M)时直接进入老年代分配,单位为byte -
-XX:MaxTenuringThreshold=1
对象年龄大于1,自动进入老年代 -
-XX:CompileThreshold=1000
一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译 -
-XX:+PrintHeapAtGC
可以看到每次GC前后堆内存布局 -
-XX:+PrintTLAB
可以看到TLAB的使用情况 -
-XX:+UseSpining
开启自旋锁 -
-XX:PreBlockSpin
更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁
4.3 使用参数
-
命令行
java -jar projectName.jar -verbose:gc -Xms20M -Xmx20M
- Eclipse
- IDEA
5. 常用工具
所谓工具,就是通过一些简便的脚本去执行程序去呈现结果数据。这里涉及到一些语法格式。
统一语法都类似这种形式:$ cmd [option id[ pid | vmid |hostid ]]
其中hostid
为可选项,默认为localgost
, vmid
/pid
依赖jps
获取
5.1 jps
jps
是Java Process Status
的缩写,查看当前java
进程的运行状态快照。理解为linux
命令ps
的java
版本
-
-m
运行时传入的参数 -
-v
虚拟机参数 -
-l
运行的主类全限定名或jar
包名称 -
示例
jps -mlv
5.2 jstat
jstat
是JVM Statistics Monitoring Tool
的缩写,查看虚拟机统计信息监控数据,如类信息、内存、垃圾收集、JIT
编译等
-
-gc
显示gc的信息,查看gc
次数以及时间 -
-class
监视类装载、卸载数量、总空间以及类装载所耗费的时间 -
-gc
监视Java堆状况,包括Eden
区、两个Survivor
区、老年代、永久带等的容量、已用空间、GC
时间合计等信息 -
-gccapacity
监视内容基本与-gc
相同,但输出主要关注Java堆各个区域使用到的最大、最小空间 -
-gcutil
监视内容基本与-gc
相同,但输出主要关注已使用的空间占总空间的百分比 -
-gccause
与-gcutil
功能一样,但是会额外输出导致上一次GC
产生的原因 -
-gcnew
监视新生代GC
状况 -
-gcnewcapacity
监视内容基本与-gcnew
相同,但输出主要关注使用到的最大、最小空间 -
-gcold
监视老年代GC
状况 -
-gcoldcapacity
监视内容基本与-gcold
相同,但输出主要关注使用到的最大、最小空间 -
-gcpermcapacity
输出永久代使用到的最大、最小空间 -
-compiler
输出JIT编译器编译过的方法、耗时等信息 -
-printcompilation
输出已经被JIT
编译的方法 -
jstat -gcutil pid
依赖jps
获得pid
查看类装载、内存、垃圾收集、jit
编译信息 -
示例
-
jstat -gcutil 3333 1000 10
对pid
为3333
的进程每隔1
秒打印1
次,总打印10
次
-
5.3 jinfo
jinfo
即Configuration Info for Java
,实时查看和调整jvm
参数
-
-flag <name>
打印jvm参数的值 -
-flag [+|-]<name>
启用/禁用jvm参数 -
-flag <name>=<value>
修改jvm参数值 -
-flags <pid>
打印所有jvm参数值 -
-sysprops <pid>
打印java系统属性 -
<no option> <pid>
打印上面所有信息 -
示例
- jinfo -flags 7298 打印pid为7298的虚拟机运行时的所有参数
- xxx
5.4 jmap
jmap
即Memory Map for Java
,内存映像工具用于生成堆转存快照
-
-dump
生成Java堆转储快照。格式为-dump:[live, ]format=b,file=<filename>
,其中live
自参数说明是否只dump
出存活的对象 -
-finalizerinfo
显示在F-Queue
中等待Finalizer
线程执行finalize
方法的对象。只在Linux
和Solaris
系统有效 -
-heap
显示Java
堆详细信息,如使用哪种收集器、参数配置、分代状况等。只在Linux
和Solaris
系统有效 -
-histo
显示堆中对象统计信息,包括类、实例数量、合计容量 -
-permstat
以ClassLoader
为统计口径显示永久代内存状态。只在Linux
和Solaris
系统下有效 -
-F
当虚拟机进行对-dump
选项没有响应时,可使用这个选项强制生成dump
快照。只在Linux
和Solaris
系统下有效 -
示例
-
jmap -dump:live,format=b,file=heap.bin 7298
将pid
为7298
的虚拟机内活对象导出为heap.bin
二进制文件
-
5.5 jhat
jhat
即JVM Heap Analysis Tool
,虚拟机堆分析工具
- xxx
-
示例
-
jhat /data/dump.bin
分析导出的堆快照
-
5.6 jstack
jstack
即Stack Trace for Java
, 堆栈跟踪工具,查看虚拟机线程快照。目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。
-
-F
即force
,强制打印线程快照信息 -
-m
即mixed mode
,同时打印java
框架信息和本地库信息 -
-l
即long listing
,打印更长(更多)的列信息 -
示例
jstack -F 7298
jstack -l 7298
jstack -m 7298
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。