学过java知识和技术人,都应该听说过jvm,jvm一直是java知识里面晋级阶段的重要部分,如果想要在java技术领域更深入一步,jvm是必须需要明白的知识点。
本篇来讲解jvm的基础原理,先来熟悉一下大致的流程:
JVM运行流程:
我们都知道java一直宣传的口号:一次编译,到处运行。也是它的跨平台性。这点的具体实现如下:
java程序在经过一次编译之后,会将java代码编译成java字节码,也就是class文件。然后在不同的机器中依靠不同的java虚拟机来解析。
最后再转换为不同平台的机器码,最终得到执行。
这样,我们就可以大胆的推测说,如果我们要在mac系统中执行,是不是就只需要安装一个mac的java虚拟机就可以执行java程序了。
了解了这个基本原理后,那一个普通的java程序它的执行流程到底是怎样的?例如以下的代码。
package helloWorld;
public class HelloWorld {
public static void main(String[] args) {
System.out.print("hello world");
}
}
这段程序从编译到运行,最终打印出“Hello world”中间经过了哪些步骤,如下图;
java代码通过编译之后生成字节码文件(class文件),通过 java hello world执行,此时java会根据系统的版本找到jvm.cfg,它会根据系统版本放在不同的位置,我的在E:\SVN_ROOT\UMP_PROJECT\UMP1.0.0.0\14tools\jdk1.6.0_02\jre\lib\i386
打开可以看到:
其中-server KNOWN就表示名称为server的jvm可用,此时在电脑中搜索下jvm.dll文件,会发现是在某个server目录下。
E:\SVN_ROOT\UMP_PROJECT\UMP1.0.0.0\14tools\jdk1.6.0_02\jre\bin\server
简而言之就是通过jvm.cfg文件来找到对应的jvm.dll。这个jvm.dll文件就是java虚拟机的主要实现。
接下来会初始化jvm,并且获取JNI接口。
什么是JNI接口,就是java的本地接口(java不太好实现的与硬件或者操作系统相关的方法,一般是其他语言编写的。)。
也就是说java被编译成了class文件,jvm要通过这个JNI接口从硬盘上找到这个文件并装载到jvm里面。然后找到main方法执行。
JVM 基础结构:
在上面的例子上,我们已经知道了java程序大致的流程,但是jvm是怎么去执行class文件的,看看以下图:
从这个结构不难看出,class文件被jvm装载以后,经过jvm的内存空间调配,最后由执行引擎完成class文件的执行。这个过程还需要其他角色模块的配合才能完成。。
jvm的内存空间
jvm的内存空间包括:方法区,java堆,java栈,本地方法栈。
方法区是各个线程共享的区域,存放类信息,常亮,静态变量。
java堆也是线程共享的区域,存放类的实例,如果一个系统创建了很多类实例,如果java堆空间不足,程序就会抛出OutOfMemoryError异常。因此java堆空间是最大的。
堆被所有的线程共享,在虚拟机启动时,我们指定的"Xmx"之类的参数就是用来指定最大堆空间的指标。
理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老生代的划分。
java栈是每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”。
而这个栈帧中包括了方法中的局部变量,用于存放中间状态值的操作栈。如果java栈空间不足了,程序会抛出*Error异常。
本地方法栈和java栈类似,只是它用来表示执行本地方法的,本地方法栈存放的方法是调用本地方法接口,最终调用本地方法库,实现与操作系统,硬件交互的目的。
PC寄存器,其实就是控制这些类对象,方法,静态变量,他们的执行顺序。它可以看做是当前线程说执行的字节码的行号指示器。由于jvm是多线程是通过轮流浅黄并分配处理器执行时间的来方式来实现的,在任何一个确定的时刻,一个处理器(对应多核处理器来说是一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器(PC寄存器),各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有的内存”。
执行引擎就是根据PC寄存器调配的指令顺序,一次执行程序指令。
对于程序中出现OutOfMemoyError,简单的总结如下:
1、堆内存不足是最常见的OOM原因之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪。
比如:可能存在内存泄露问题;也很有可能就是堆得大小不合理,比如我们要处理可观的数据量,但是没有显式指定JVM堆大小或者指定数据偏小;或者出现JVM处理引用不及时,导致堆积起来,内存无法释放等。
2、Java虚拟机栈和本地方法栈,如果我们写一段程序不断的进行地柜调用,而且没有退出条件,就会导致不断的进行压栈。类似这种情况,JVM实际会抛出*Error;当然,如果JVM试图去扩展栈空间的时候失败,就会抛出OutOfMemoyError。
3、对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收非常的不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoyError也非常的多见,尤其是在运行时,存在大量动态类型生成的场合;类似Intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoyError:PermGen space”。
4、随着元数据区的引入,方法区内存以及不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了:"java.lang.OutOfMemoyError: Metaspace"。
结语:
本文主要介绍了java虚拟机运行的基本流程,以及java虚拟机内部结构。下一篇我们将学习java内存模型以及探索java变量的可见性、有序性、指令重排等问题以及总结了可能会发现OutOfMemoyError异常的原因和所在。