5
这是我在公司给团队小伙伴一次技术小分享。
新手司机可以收藏、学习,老司机可以批评指正。
ps:内容参考了众多优秀博文、书籍,部分图片来源于博文,如有侵权请联系删除。

1. 前言

为什么Java可以实现所谓的“一次编写,到处运行”,主要是因为虚拟机的存在。Java虚拟机负责Java程序设计语言的安全特性和平台无关性。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java语言编译器只需要生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改地运行。Java虚拟机使得Java摆脱了具体机器的束缚,使跨越不同平台编写程序成为了可能。

Java虚拟机基本上都是JDK自带的虚拟机HotSpot,这款虚拟机也是目前商用虚拟中市场份额最大的一款虚拟机,可以通过在命令行程序中输入java -version来查看:

java-version

其实市面上还有很多别的优秀的虚拟机。Sun公司除了有大名鼎鼎的HotSpot外,还有KVMSquawk VMMaxine VM,BEA公司有JRockit VM、IBM公司有J9 VM等等。

2. 内存模型(JMM)

Java虚拟机(JVM)内部定义了程序在运行时需要使用到的内存区域。内存区域主要分为主内存和工作内存。主内存即主机物理内存,工作内存按作用域可划分为线程独享区和线程共享区。

宏观来看是这样子的,如下图:

asss

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

    Eclipse

  • IDEA

    idea

5. 常用工具

所谓工具,就是通过一些简便的脚本去执行程序去呈现结果数据。这里涉及到一些语法格式。

统一语法都类似这种形式:$ cmd [option id[ pid | vmid |hostid ]]

其中hostid为可选项,默认为localgost, vmid/pid依赖jps获取

5.1 jps

jpsJava Process Status的缩写,查看当前java进程的运行状态快照。理解为linux命令psjava版本

  • -m 运行时传入的参数
  • -v 虚拟机参数
  • -l 运行的主类全限定名或jar包名称
  • 示例

    • jps -mlv

5.2 jstat

jstatJVM 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 10pid3333的进程每隔1秒打印1次,总打印10

5.3 jinfo

jinfoConfiguration 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

jmapMemory Map for Java,内存映像工具用于生成堆转存快照

  • -dump 生成Java堆转储快照。格式为-dump:[live, ]format=b,file=<filename>,其中live自参数说明是否只dump出存活的对象
  • -finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在LinuxSolaris系统有效
  • -heap 显示Java堆详细信息,如使用哪种收集器、参数配置、分代状况等。只在LinuxSolaris系统有效
  • -histo 显示堆中对象统计信息,包括类、实例数量、合计容量
  • -permstatClassLoader为统计口径显示永久代内存状态。只在LinuxSolaris系统下有效
  • -F 当虚拟机进行对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在LinuxSolaris系统下有效
  • 示例

    • jmap -dump:live,format=b,file=heap.bin 7298pid7298的虚拟机内活对象导出为heap.bin二进制文件

5.5 jhat

jhatJVM Heap Analysis Tool,虚拟机堆分析工具

  • xxx
  • 示例

    • jhat /data/dump.bin 分析导出的堆快照

5.6 jstack

jstackStack Trace for Java, 堆栈跟踪工具,查看虚拟机线程快照。目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。

  • -Fforce,强制打印线程快照信息
  • -mmixed mode,同时打印java框架信息和本地库信息
  • -llong listing,打印更长(更多)的列信息
  • 示例

    • jstack -F 7298
    • jstack -l 7298
    • jstack -m 7298

Aron
858 声望102 粉丝

搞过c/c++, 玩过c#,正在撸java。