关于JVM的那些事儿(A)

什么是 JVM:

            JVM是Java Virtual Machine的缩写。它是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

JDK、JRE、JVM关系:

      JRE:JavaRuntimeEnvironment,所有编译过后的Java 程序都要在JRE下才能运行。普通用户只需要按照JRE,运行class文件即可。
      JDK:Java Development Kit是developers用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。
     JVM:JavaVirtualMachine,为JRE的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。使用JVM就是为了支持与操作系统无关,实现跨平台。

 

JVM class 加载机制:

         类加载机制,大致可分为:加载、验证、准备、解析、初始化、使用、卸载等七个过程,而类的加载由Bootstrap ClassLoader 及其子类完成,以下将详细介绍其具体过程。

         编译:此过程将把.java的文件编译成.class 文件,前者为源文件,后者字节码文件,此过程主要检验编译错误,例如赋值错误、缺少字符、包引用错误、对象、函数引用错误等。

         类加载:类的加载主要由根加载器及其子类完成,分为Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader、以及自定义的加载器。在类的加载阶段,将在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。需要注意的是,类的加载并非只能从class文件或者,也能从jar包或war包等包中获取,当然也能通过动态代理以及其它文件(如jsp)生成。当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

        验证:这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

       准备:该阶段是正式为类变量分配内存以及为基础类型设置初始值,即在方法区中分配这些变量所使用的内存空间。例如public static int a=1000,此时a将被初始化为0而非1000。

      解析:该阶段,将常量池中的符号引用,改为直接引用。其中,符号引用对于目标地址是否存于内存中无硬性要求,直接引用,则需要保证其存在。

      初始化:该阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。类的初始化,是自上由下的进行,即先进行父类初始化,再进行子类初始化。在类内部,其初始化步骤为:静态变量、静态初始化块、变量、初始化块、构造器,当存在继承时,其初始化步骤为:父类–静态变量、父类–静态初始化块、子类–静态变量、子类–静态初始化块、父类–变量、父类–初始化块、父类–构造器、子类–变量、子类–初始化块、子类–构造器。

图1.类加载流程

JVM执行引擎:

        1.代码解释器。 
        2.JIT代码生成器。该方式第一次执行的字节码会编译成本地的机器代码,被缓存在系统中,以后可以随时调用。
        3.自适应的优化器。这种方法里,虚拟机开始的时候解释字节码,但是会监控运行中程序的活动,并记录下使用最频繁的代码段,虚拟机会把这些活动最频繁的代码段编译成本地代码。
其实以上提的都是软件实现的虚拟机,还有一种虚拟机是由硬件芯片构成,它用本地方法执行java字节码。

JVM内存划分:

           JVM内存主要划分为六大块,即方法区、java堆、java栈、程序计数器、本地方法栈。

          方法区:存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息(其中静态变量放于常量池中)。当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

         JVM堆:JVM中的堆内存,为所有线程共享,用于存放对象实例,几乎所有的对象都在此处分配内存。堆内存 占据了JVM大多数内存空间,其存于一片不连续存储块上,而我们熟知的GC即垃圾回收,发生在该空间,由于垃圾回收机制采用分代回收算法,因此可将堆细分为新生代、老年代。新生代又有Eden空间、From Survivor空间、To Survivor空间三部分。而永久代(非堆内存,JDK1.8之前)的数据不会被更新,例如intern()产生的对象,并不会被回收,因此程序操作不当,可能导致永久代数据量过大,可能会导致OOM问题。在jdk1.7以后,引入了元空间,废除了永久代。关于各引用的GC,读者可自行阅读之前的文章。

      JVM栈:JVM栈分为本地方法栈与java栈,栈为线程私有,其生命周期与线程相同,每个方法被执行时,都会创建一个栈帧,用于保护现场,其中存储局部变量表、操作栈、动态链接、方法出口等信息。当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常。当java需要调用c、c++等非java程序时,会创建本地方法栈,对于大多数程序来说,基本上不会用到,当然其原理与前者大致类似,在此不做赘述。

      程序计数器:线程私有,它的生命周期与线程相同。可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、跳转、异常处理、线程恢复(多线程切换)等基础功能。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(undefined)。
程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以此区域不会出现OutOfMemoryError的情况。

发表评论

电子邮件地址不会被公开。