JVM工作原理

一、JVM生命周期

Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。 Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。 Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可以把自己的程序设置为守护线程。包含Main()方法的初始线程不是守护线程。 只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。

二、Java代码编译和执行的整个过程

    开发人员编写Java代码(.java文件),然后将之编译成字节码文件(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

   (1)Java代码编译是由Java源码编译器来完成,也就是Java代码到JVM字节码的过程。

    源代码、词法分析器、语法分析器、语义分析器、字节码生成器、JVM字节码

   (2)Java字节码的执行是由JVM执行引擎来完成的。

     JVM字节码、字节码解释器

     JVM字节码、机器无关优化、机器相关优化、寄存器分配器、目标代码生成器、目标代码

 

    Java代码编译和执行的整个过程包含三个重要的机制为:java源码编译机制、类加载机制、类执行机制

 

     1.Java源码编译机制(分析和输出到符号表、注解处理、语义分析和生成class文件)

      最后生成的class文件由以下部分组成:

      结构信息:包括class文件格式版本号及各部分的数量与大小的信息

      元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池

      方法信息:对应于Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区的大小、求值栈的类型记录、调试符号信息

2.类加载机制

       每一个Java虚拟机都有一个类加载器子系统,负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎负责执行被加载类中包含的指令。

       JVM的类加载是通过ClassLoader及其子类来完成的

       启动类加载器(Bootstrap ClassLoader):负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

       扩展类加载器(Extension ClassLoader):负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

       应用程序加载器(App ClassLoader):负责加载启动参数中指定的classpath的jar包及目录中class

       用户自定义加载器(Custom ClassLoader):属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader 

       加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝         试加载此类。

       

        加载:

1.       通过“类全名”来获取定义此类的二进制字节流

2.       将字节流所代表的静态存储结构转换为方法区的运行时数据结构

3.       在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

 

   三、类执行机制

       JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

       栈帧由三部分组成:局部变量区,操作数栈和帧数据区。

       JVM执行class字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。

二、java垃圾回收机制

   典型的垃圾收集算法

(1)   Mark-Sweep(标记-清除)算法:这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

 

 

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

(2)Copying(复制)算法:为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

 

 

    这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

(3)Mark-Compact(标记-整理)算法: 为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

 

 

(4) Generational Collection(分代收集)算法:

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

  目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

  注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

 

两个重要方法:

System.gc()方法:使用System.gc()可以不管JVM使用的哪一种垃圾回收的算法,都可以请求Java的垃圾回收。

Finalize()方法:在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象释放资源,这个方法就是finalize()。它的原型就是protect void finalize() throws Throws

Throwable  在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throws

Throwable表示它可以抛出任何类型的异常。

   之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况:如打开的文件资源不属于垃圾回收器的回收范围。

此外:

(1)每个对象只能调用finalize( )方法一次。如果在finalize( )方法执行时产生异常(exception),则该对象仍可以被垃圾收集器收集。

  

对象在JVM堆区的状态:

(1) 可触及状态:程序中还有变量引用,那么此状态为可触及状态。

(2) 可复活状态:当程序中已经没有变量引用这个对象,那么此对象由可触及状态转为可复活状态。GC线程将在一定的时间准备调用此对象的finalize()方法,finalize方法内的代码有可能将对象转为可触及状态,否则对象转化为不可触及状态。

(3) 不可触及状态:只有当对象处于不可触及状态时,GC线程才能回收此对象的内存。

 

另:http://www.open-open.com/lib/view/open1408453806147.html

posted @ 2017-01-11 20:34  samcheng  阅读(869)  评论(2编辑  收藏  举报