JVM原理和排查思路

我们知道Java语言是跨平台的语言,那他是怎么实现的呢?Java虚拟机的原理是什么呢?

虚拟化技术

虚拟化就是由位于下层的软件模块,根据上层的软件模块的期待,抽象(虚拟)出一个虚拟的软件或硬件模块,使上一层软件直接运行在这个与自己期待完全一致的虚拟环境上。从这个意义上来看,虚拟化既可以是软件层的抽象,又可以是硬件层的抽象。

image

Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

从进程的角度解释JVM

让我们尝试从操作系统的层面来理解JVM虚拟机。我们知道,虚拟机是运行在操作系统之中的,那么什么东西才能在操作系统中运行呢?当然是进程,因为进程是操作系统中的执行单位。可以这样理解,当它在运行的时候,它就是一个操作系统中的进程实例,当它没有在运行时(作为可执行文件存放于文件系统中),可以把它叫做程序。

我们以一个HelloWorld为例:

public class HelloWorld {
 
    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }
}

我们需要先编译在执行:

$ javac HelloWorld.java 
$ ls
HelloWorld.class  HelloWorld.java
$ java -classpath . HelloWorld 

从上面的过程可以看到, 我们在运行Java版的HelloWorld程序的时候, 敲入的命令并不是 ./HelloWorld.class 。 因为class文件并不是可以直接被操作系统识别的二进制可执行文件 。 我们敲入的是java这个命令。 这个命令说明, 我们首先启动的是一个叫做java的程序, 这个java程序在运行起来之后就是一个JVM进程实例。

image

JVM的内存管理

我们知道,Java虚拟机会进行自动内存管理。具体说来就是自动释放没有用的对象,而不需要程序员编写代码来释放分配的内存。

JVM虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同是数据区域,这些区域有各自各自的用途。Java1.8主要包含以下几个部分组成:

image

方法区和堆是所有线程共享的内存区域,而Java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。

  1. 程序计数器占用的内存空间我们可以忽略不计,它是每个线程所执行的字节码的行号指示器。
  2. 虚拟机栈是线程私有的,生命周期和线程相同。它描述的是方法执行的内存模型,同时用于存储局部变量、操作数栈、动态链接、方法出口等。
  3. 本地方法栈,类似虚拟机栈,它调用的是是native方法。
  4. 堆是Jvm中管理内存中最大一块。它是被共享,存放对象实例,也被称为“gc堆”。垃圾回收的主要管理区域。 由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor(S0)空间、To Survivor(S1)空间,默认情况下年轻代按照8:1:1的比例来分配。
  5. 方法区也是共享的内存区域。它主要存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(jit)编译后的代码数据。
image

回收方法

引用计数算法:

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1,当引用失效,计数器-1.任何时刻计数器为0的对象就是不可能再被使用的。

  • 优点:实现简单,判定效率高效,被actionscript3和python中广泛应用。
  • 缺点:无法解决对象之间的相互引用问题。java没有采纳
可达性分析算法:

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的时候,则证明此对象是不可用的。

比如如下,右侧的对象是到GCRoot时不可达的,可以判定为可回收对象。

image

回收依据

在java中,可以作为GCRoot的对象包括以下几种:

  • 虚拟机栈中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法中JNI引用的对象。

那么是不是这些对象就非死不可,也不一定,此时只能宣判它们存在于一种“缓刑”的阶段,要真正的宣告一个对象死亡。至少要经历两次标记:第一次:对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选。第二次:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时会被认定为没必要执行。

如何回收

标记清除法(Mark-Sweep)

标记清除法是垃圾回收算法的思想基础。标记清除算法将垃圾分为两个阶段:标记阶段和清除阶段。

标记阶段,通过根节点,标记所有从根节点开始的可达对象,未标记过的对象就是未被引用的垃圾对象。

清除阶段,清除所有未被标记的对象。

image
复制算法(Copying)

复制算法是,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在适用的内存中存活对象复制到未使用的内存块,然后清除使用的内存块中所有的对象。

image
标记压缩算法(Mark-Compact)

标记压缩算法是一种老年代的回收算法。

标记阶段和标记清除算法一致,对可达对象做一次标记。

清理阶段,为了避免内存碎片产生,将所有的存活对象压缩到内存的一端。

image
分代收集算法

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

JVM工具

jps
jps [ options ] [ hostid ]

使用命令如下:
使用jps:jps -l
使用ps:ps aux | grep tomat

image
jinfo

jinfo -flag PID

 $ jinfo -flag MaxMetaspaceSize 18348
 -XX:MaxMetaspaceSize=536870912    #即MaxMetaspaceSize为512M
 $ jinfo -flag ThreadStackSize 18348
 -XX:ThreadStackSize=256           #即Xss为256K
image
jstat
jstat [ option vmid [interval[s|ms] [count]] ]

使用命令如下:

jstat -gcutil [pid] 1000

意思是每1000毫秒查询一次,一直查。gcutil的意思是已使用空间站总空间的百分比。

image
jmap

jmap [ option ] vmid
使用命令如下:
jmap -histo:live 20954
查看存活的对象情况,如下图所示:

image

导出文件进行分析:

jmap -dump:live,format=b,file=heap.hprof  23817
image

内存溢出和内存泄露

java.lang.OutOfMemoryError,是指程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError。

产生原因

产生该错误的原因主要包括:

  1. JVM内存过小。
    程序不严密,产生了过多的垃圾。

  2. 程序体现
    一般情况下,在程序上的体现为:

    • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据。

    • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收。

    • 代码中存在死循环或循环产生过多重复的对象实体。

    • 使用的第三方软件中的BUG。

    • 启动参数内存值设定的过小。

错误提示

此错误常见的错误提示:
tomcat:java.lang.OutOfMemoryError: PermGen space #永久代 1.7之前
tomcat:java.lang.OutOfMemoryError: Java heap space #堆大小
weblogic:Root cause of ServletException java.lang.OutOfMemoryError
resin:java.lang.OutOfMemoryError
java:java.lang.OutOfMemoryError

解决方法
  1. 增加JVM的内存大小
    对于Tomcat找到JAVA_OPTS,也可以在操作系统的环境变量中对JAVA_OPTS进行设置,因为tomcat在启动的时候,也会读取操作系统中的环境变量的值,进行加载。
  2. 优化程序,释放垃圾
    主要思路就是避免程序体现上出现的情况。避免死循环,防止一次载入太多的数据,提高程序健壮型及时释放。因此,从根本上解决Java内存溢出的唯一方法就是修改程序,及时地释放没用的对象,释放内存空间。

CPU使用率高

jps 获取Java进程的PID
jps -l
jstack pid >> java.txt 导出CPU占用高进程的线程栈
查找线程占用情况
top -H -p PID 查看对应进程的哪个线程占用CPU过高。
线程转换成16进制
printf "%x\n" 线程号 
查看线程号对应的线程栈
jstack pid | grep XXXX
image

你可能感兴趣的:(JVM原理和排查思路)