我们知道Java语言是跨平台的语言,那他是怎么实现的呢?Java虚拟机的原理是什么呢?
虚拟化技术
虚拟化就是由位于下层的软件模块,根据上层的软件模块的期待,抽象(虚拟)出一个虚拟的软件或硬件模块,使上一层软件直接运行在这个与自己期待完全一致的虚拟环境上。从这个意义上来看,虚拟化既可以是软件层的抽象,又可以是硬件层的抽象。
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进程实例。
JVM的内存管理
我们知道,Java虚拟机会进行自动内存管理。具体说来就是自动释放没有用的对象,而不需要程序员编写代码来释放分配的内存。
JVM虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同是数据区域,这些区域有各自各自的用途。Java1.8主要包含以下几个部分组成:
方法区和堆是所有线程共享的内存区域,而Java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
- 程序计数器占用的内存空间我们可以忽略不计,它是每个线程所执行的字节码的行号指示器。
- 虚拟机栈是线程私有的,生命周期和线程相同。它描述的是方法执行的内存模型,同时用于存储局部变量、操作数栈、动态链接、方法出口等。
- 本地方法栈,类似虚拟机栈,它调用的是是native方法。
- 堆是Jvm中管理内存中最大一块。它是被共享,存放对象实例,也被称为“gc堆”。垃圾回收的主要管理区域。 由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor(S0)空间、To Survivor(S1)空间,默认情况下年轻代按照8:1:1的比例来分配。
- 方法区也是共享的内存区域。它主要存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(jit)编译后的代码数据。
回收方法
引用计数算法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1,当引用失效,计数器-1.任何时刻计数器为0的对象就是不可能再被使用的。
- 优点:实现简单,判定效率高效,被actionscript3和python中广泛应用。
- 缺点:无法解决对象之间的相互引用问题。java没有采纳
可达性分析算法:
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的时候,则证明此对象是不可用的。
比如如下,右侧的对象是到GCRoot时不可达的,可以判定为可回收对象。
回收依据
在java中,可以作为GCRoot的对象包括以下几种:
- 虚拟机栈中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法中JNI引用的对象。
那么是不是这些对象就非死不可,也不一定,此时只能宣判它们存在于一种“缓刑”的阶段,要真正的宣告一个对象死亡。至少要经历两次标记:第一次:对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选。第二次:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时会被认定为没必要执行。
如何回收
标记清除法(Mark-Sweep)
标记清除法是垃圾回收算法的思想基础。标记清除算法将垃圾分为两个阶段:标记阶段和清除阶段。
标记阶段,通过根节点,标记所有从根节点开始的可达对象,未标记过的对象就是未被引用的垃圾对象。
清除阶段,清除所有未被标记的对象。
复制算法(Copying)
复制算法是,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在适用的内存中存活对象复制到未使用的内存块,然后清除使用的内存块中所有的对象。
标记压缩算法(Mark-Compact)
标记压缩算法是一种老年代的回收算法。
标记阶段和标记清除算法一致,对可达对象做一次标记。
清理阶段,为了避免内存碎片产生,将所有的存活对象压缩到内存的一端。
分代收集算法
GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
JVM工具
jps
jps [ options ] [ hostid ]
使用命令如下:
使用jps:jps -l
使用ps:ps aux | grep tomat
jinfo
jinfo -flag
$ jinfo -flag MaxMetaspaceSize 18348
-XX:MaxMetaspaceSize=536870912 #即MaxMetaspaceSize为512M
$ jinfo -flag ThreadStackSize 18348
-XX:ThreadStackSize=256 #即Xss为256K
jstat
jstat [ option vmid [interval[s|ms] [count]] ]
使用命令如下:
jstat -gcutil [pid] 1000
意思是每1000毫秒查询一次,一直查。gcutil的意思是已使用空间站总空间的百分比。
jmap
jmap [ option ] vmid
使用命令如下:
jmap -histo:live 20954
查看存活的对象情况,如下图所示:
导出文件进行分析:
jmap -dump:live,format=b,file=heap.hprof 23817
内存溢出和内存泄露
java.lang.OutOfMemoryError,是指程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError。
产生原因
产生该错误的原因主要包括:
JVM内存过小。
程序不严密,产生了过多的垃圾。-
程序体现
一般情况下,在程序上的体现为:内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
集合类中有对对象的引用,使用完后未清空,使得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
解决方法
- 增加JVM的内存大小
对于Tomcat找到JAVA_OPTS,也可以在操作系统的环境变量中对JAVA_OPTS进行设置,因为tomcat在启动的时候,也会读取操作系统中的环境变量的值,进行加载。 - 优化程序,释放垃圾
主要思路就是避免程序体现上出现的情况。避免死循环,防止一次载入太多的数据,提高程序健壮型及时释放。因此,从根本上解决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