JAVA虚拟机原理初探

时间:2022-12-27 16:36:06

一、JAVA虚拟机的生命周期

运行中的JAVA虚拟机有着清晰而明确的任务,执行JAVA程序。JVM从程序开始时运行,程序结束时停止。每一个JAVA程序都有一个JVM为其服务。JVM总是开始于 public void main(String[] args)这个方法。在程序执行时,必须为JVM指明包含main()方法的类名。main()方法是程序的起点,它被执行的线程初始化为程序的初始线程,程序中的其他线程都由此启动。

JAVA中的线程分为两种:守护线程(daemon)和普通线程(non-daemon)。守护线程是JVM自己使用的线程,负责垃圾回收等功能,当然程序员也可以将自己的线程设为守护进程。而包含main()方法的初始线程不是守护线程。只要JVM还有普通线程在执行,JVM就不会停止,但也可以条用exit()方法中止程序。

二、JVM的体系结构

每一个JVM都由以下部件组成:

* 类加载器子系统(class loader subsystem),负责加载程序中的类型(类和接口),并赋予唯一的名字
* 执行引擎(execution engine)负责执行被加载类中包含的指令
* 数据区(data areas),保存字节码、被加载类的其他额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等等

三、JAVA数据区:

 JAVA数据区中有一部分数据共享,有一部分私有。
  • 共享区:
    • 方法区(method area):从类文件中解析出来的信息保存于方法区中。
    • 堆(heap):执行时创建的 对象都保存在堆中。
  • 私有区:
    • 程序计数器(pc register):当线程不掉用本地方法时,PC寄存器中保存线程执行的下一条指令
    • 栈(java stack):堆栈保存了一个线程调用方法时的状态,包括本地变量、调用方法的 参数、返回值、处理的中间变量。调用本地方法时的状态保存在本地方法堆栈中(native method stacks),可能在寄存器或者其他非平*立的内存中。
    • 堆栈块(stack frames (or frames)):包含Java方法调用的状态。当一个线程调用一个方法时,Java虚拟机会将一个新的块压到Java堆栈中,当这个方法运行结束时,Java虚拟机会将对应的块弹出并抛弃。

四、类加载器子系统:

JVM中的类加载器分为两种,原始类加载器(primordial class loader)和类加载器对象(class loader objects)。原始类加载器是Java虚拟机实现的一部分,类加载器对象是运行中的程序的一部分。不同类加载器加载的类被不同的命名空间所分割。

类加载器调用了许多Java虚拟机中其他的部分和java.lang包中的很多类。比如,类加载对象就是java.lang.ClassLoader子类 的实例,ClassLoader类中的方法可以访问虚拟机中的类加载机制;每一个被Java虚拟机加载的类都会被表示为一个 java.lang.Class类的实例。像其他对象一样,类加载器对象和Class对象都保存在堆中,被加载的信息被保存在方法区中。

类加载器子系统有着如下工作流程:

1. 加载 Loading:寻找并导入指定类型(类和接口)的二进制信息
2. 连接 Linking:进行验证、准备和解析
1. 验证:确保导入类型的正确性
2. 准备:为类型分配内存并初始化为默认值
3. 解析:将字符引用解析为直接引用
3. 初始化 Initialization:调用Java代码,初始化类变量为合适的值
  • 原始类加载器(primordial class loader):
    每个Java虚拟机都必须实现一个原始类加载器,他能够加载那些遵守类文件格式并且被信任的类。但是,Java虚拟机的规范并没有定义如何加载类,这由 Java虚拟机实现者自己决定。对于给定类型名的类型,原始莱加载器必须找到那个类型名加“.class”的文件并加载入虚拟机中。

  • 类加载器对象(class loader objects):虽然类加载器对象是Java程序的一部分,但是ClassLoader类中的三个方法可以访问Java虚拟机中的类加载子系统。

    • protected final Class defineClass(…):使用这个方法可以出入一个字节数组,定义一个新的类型。
    • protected Class findSystemClass(String name):加载指定的类,如果已经加载,就直接返回。
    • protected final void resolveClass(Class c):defineClass()方法只是加载一个类,这个方法负责后续的动态连接和初始化
  • 命名空间:当多个类加载器加载了同一个类时,为了保证他们名字的唯一性,需要在类名前加上加载该类的类加载器的标识。(namespace)

五、方法区

在Java虚拟机中,被加载类型的信息都保存在方法区中。

  • 信息在内存中的组织形式由虚拟机的实现者定义,
  • 方法区的大小可以在程序运行期间扩展,
  • 可以被JVM垃圾收集器收集垃圾。因为程序中的内由类加载器动态加载,所有类可能变成没有被引用。

程序中的所有线程共享一个方法区,所以访问方法区信息的方法必须是线程安全的。如果你有两个线程都去加载一个叫Lava的类,那只能由一个线程被容许去加载这个类,另一个必须等待。

  1. 类型信息(Type Information)
    每一个被加载的类型,在Java虚拟机中都会在方法区中保存如下信息:

    • 类型的全名(The fully qualified name of the type)
    • 类型的父类型的全名(除非没有父类型,或者父类形式为java.lang.Object)(The fully qualified name of the typeís direct superclass)
    • 类型是一个类还是接口(class or an interface)(Whether or not the type is a class )
    • 类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)
    • 所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)

    类型全名保存的数据结构由虚拟机实现者定义。除此之外,Java虚拟机还要为每个类型保存如下信息:

    • 类型的常量池(The constant pool for the type):

      • 常量池中保存中所有类型是用的有序的常量集合,包含直接常量(literals)如字符串、整数、浮点数的常量,和对类型、字段、方法的符号引用。常量池 中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象。
    • 类型字段的信息(Field information)

      • 字段名、字段类型、字段的修饰符(public,private,protected,static,final,volatile,transient等)、字段在类中定义的顺序。
    • 类型方法的信息(Method information)

      • 方法名、方法的返回值类型(或者是void)、方法参数的个数、类型和他们的顺序、字段的修饰符(public,private,protected,static,final,volatile,transient等)、方法在类中定义的顺序

      • 如果不是抽象和本地方法还需要保存

    • 所有的静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)

      • 类变量被所有类的实例共享,即使不通过类的实例也可以访问。这些变量绑定在类上(而不是类的实例上),所以他们是类的逻辑数据的一部分。
      • 在Java虚拟机使用这个类之前就需要为类变量(non-final)分配内存
    • 一个指向类加载器的引用(A reference to class ClassLoader)

      • 虚拟机必须保存这个类型是否由原始类加载器或者类加载器加载。那些被类加载器加载的类型必须保存一个指向类加载器的引 用。
    • 一个指向Class类的引用(A reference to class Class)

      • Java虚拟机为每一个加载的类型创建一个java.lang.Class类的实例。
  2. 方法列表(Method Tables)
    为了更有效的访问所有保存在方法区中的数据,这些数据的存储结构必须经过仔细的设计。所有方法区中,除了保存了上边的那些原始信息外,还有一个为了加快存 取速度而设计的数据结构,比如方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例 方法的引用,报错那些父类中调用的方法。

六、堆

当Java程序创建一个类的实例或者数组时,都在堆中为新的对象分配内存。虚拟机中只有一个堆,所有的线程都共享他。

  • 垃圾收集(Garbage Collection):
    垃圾收集是释放没有被引用的对象的主要方法。它也可能会为了减少堆的碎片,而移动对象。在Java虚拟机的规范中没有严格定义垃圾收集,只是定义一个Java虚拟机的实现必须通过某种方式管理自己的堆。

  • 对象存储结构(Object Representation):

    Java虚拟机的规范中没有定义对象怎样在堆中存储。每一个对象主要存储的是他的类和父类中定义的对象变量。对于给定的对象的引用,虚拟机必须很快的定位到这个对象的数据。另为,必须提供一种通过对象的引用获取对象数据的方法。

    • 设计一:引用池和对象池。一个对象的引用就是指向引用池的本地指针。每一个引用池中的条目都包含两个部分:指向对象池中对 象数据的指针和方法区中对象类数据的指针。这种设计能够方便Java虚拟机堆碎片的整理。当虚拟机在对象池中移动一个对象的时候,只需要修改对应引用池中的指针地址。但是每次访问对象的数据都需要处理两次指针。
    • 设计二:一个对象的引用就是一个指向一堆数据和指向相应对象的偏移指针。这种设计方便了对象的访问,可是对象的移动会变的异常复杂。
  • 数组的保存(Array Representation):在Java 中,数组是一种完全意义上的对象,他和对象一样保存在堆中、有一个指向Class类实例的引用。所有同一维度和类型的数组拥有同样的Class,数组的长 度不做考虑。对应Class的名字表示为维度和类型。

七、基本结构

JVM内存结构主要包括两个子系统和两个组件。两个子系统分别是Classloader子系统和Executionengine(执行引擎)子系统;两个组件分别是Runtimedataarea(运行时数据区域)组件和Nativeinterface(本地接口)组件。

JAVA虚拟机原理初探

  • Classloader子系统的作用:根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到Runtimedataarea中的methodarea(方法区域)。Java程序员可以extends:java.lang.ClassLoader类来写自己的Classloader。

  • Executionengine子系统的作用:执行classes中的指令。任何JVMspecification实现(JDK)的核心都是Executionengine,不同的JDK例如Sun的JDK和IBM的JDK好坏主要就取决于他们各自实现的Executionengine的好坏。

  • Nativeinterface组件:与nativelibraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的nativeheapOutOfMemory。

  • RuntimeDataArea组件: 这就是我们常说的JVM的内存了。它主要分为五个部分
    • Heap(堆):一个Java虚拟实例中只存在一个堆空间
    • MethodArea(方法区域):被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。
    • JavaStack(java的栈):虚拟机只会直接对Javastack执行两种操作:以帧为单位的压栈或出栈
    • ProgramCounter(程序计数器):每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
    • Nativemethodstack(本地方法栈):保存native方法进入区域的地址

八、Java源码编译机制

Java代码编译和执行的整个过程Java代码编译是由Java源码编译器来完成,流程图如下所示:JAVA虚拟机原理初探

Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
JAVA虚拟机原理初探
Java代码编译和执行的整个过程包含了以下三个重要的机制:

  • Java源码编译机制
  • 类加载机制
  • 类执行机制

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

  • 结构信息。包括class文件格式版本号及各部分的数量与大小的信息
  • 元数据。对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
  • 方法信息。对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息

九、类加载机制

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
JAVA虚拟机原理初探
类加载双亲委派机制介绍和分析
在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

十、JVM内存回收

JAVA虚拟机原理初探
Sun的JVMGenerationalCollecting(垃圾回收)原理是这样的:
把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)

  • 年青代(Young):年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
  • 年老代(Tenured):年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
  • 持久代(Perm):用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。

通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的MethodArea,不属于Heap。

关于JVM内存管理的一些建议
  1. 手动将生成的无用对象,中间对象置为null,加快内存回收。
  2. 对象池技术如果生成的对象是可重用的对象,只是其中的属性不同时,可以考虑采用对象池来较少对象的生成。如果有空闲的对象就从对象池中取出使用,没有再生成新的对象,大大提高了对象的复用率。
  3. JVM调优通过配置JVM的参数来提高垃圾回收的速度,如果在没有出现内存泄露且上面两种办法都不能保证JVM内存回收时,可以考虑采用JVM调优的方式来解决,不过一定要经过实体机的长期测试,因为不同的参数可能引起不同的效果。如-Xnoclassgc参数等。

参考自:
http://blog.csdn.net/witsmakemen/article/details/28600127
对该文章进行了精简,有些不全面,但也许更适合新手阅读