JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.
1.创建JVM装载环境和配置
2.装载JVM.dll
3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
4.调用JNIEnv实例装载并处理class类。
在我们运行和调试Java程序的时候,经常会提到一个JVM的概念.JVM是Java程序运行的环境,但是他同时一个操作系统的一个 应用程序一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间.
首先来说一下JVM工作原理中的jdk这个东西,不管你是初学者还是高手,是j2ee程序员还是j2se程序员,jdk总是在帮我们做一些 事情.我们在了解Java之前首先大师们会给我们提供说jdk这个东西.它在Java整个体系中充当着什么角色呢?我很惊叹sun大师们设计天才,能把一 个如此完整的体系结构化的如此完美.jdk在这个体系中充当一个生产加工中心,产生所有的数据输出,是所有指令和战略的执行中心.本身它提供了Java的 完整方案,可以开发目前Java能支持的所有应用和系统程序.这里说一个问题,大家会问,那为什么还有j2me,j2ee这些东西,这两个东西目的很简 单,分别用来简化各自领域内的开发和构建过程.jdk除了JVM之外,还有一些核心的API,集成API,用户工具,开发技术,开发工具和API等组成
好了,废话说了那么多,来点于主题相关的东西吧.JVM在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,提供一个 完整的Java运行环境,因此也就虚拟计算机. 操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.
1.创建JVM装载环境和配置
2.装载JVM.dll
3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
4.调用JNIEnv实例装载并处理class类。
一.JVM装入环境,JVM提供的方式是操作系统的动态连接文件. 既然是文件那就一个装入路径的问 题,Java是怎么找这个路径的呢?当你在调用Java test的时候,操作系统会在path下在你的Java.exe程序,Java.exe就通过下面一个过程来确定JVM的路径和相关的参数配置了.下面基 于Windows的实现的分析.
首先查找jre路径,Java是通过GetApplicationHome api来获得当前的Java.exe绝对路径,c:/j2sdk1.4.2_09/bin/Java.exe,那么它会截取到绝对路径c: /j2sdk1.4.2_09/,判断c:/j2sdk1.4.2_09/bin/Java.dll文件是否存在,如果存在就把c: /j2sdk1.4.2_09/作为jre路径,如果不存在则判断c:/j2sdk1.4.2_09/jre/bin/Java.dll是否存在,如果存 在这c:/j2sdk1.4.2_09/jre作为jre路径.如果不存在调用GetPublicJREHome查HKEY_LOCAL_MACHINE /Software/JavaSoft/Java Runtime Environment/“当前JRE版本号”/JavaHome的路径为jre路径。
然后装载JVM.cfg文件JRE路径+/lib+/ARCH(CPU构架)+/JVM.cfgARCH(CPU构架)的判断是通过 Java_md.c中GetArch函数判断的,该函数中windows平台只有两种情况:WIN64的‘ia64’,其他情况都为‘i386’。以我的 为例:C:/j2sdk1.4.2_09/jre/lib/i386/JVM.cfg.主要的内容如下:
- -client KNOWN
- -server KNOWN
- -hotspot ALIASED_TO -client
- -classic WARN
- -native ERROR
- -green ERROR
在我们的jdk目录中jre/bin/server和jre/bin/client都有JVM.dll文件存在,而Java正是通过JVM.cfg 配置文件来管理这些不同版本的JVM.dll的.通过文件我们可以定义目前jdk中支持那些JVM,前面部分(client)是JVM名称,后面是参 数,KNOWN表示JVM存在,ALIASED_TO表示给别的JVM取一个别名,WARN表示不存在时找一个JVM替代,ERROR表示不存在抛出异 常.在运行Java XXX是,Java.exe会通过CheckJVMType来检查当前的JVM类型,Java可以通过两种参数的方式来指定具体的JVM类型,一种按照 JVM.cfg文件中的JVM名称指定,第二种方法是直接指定,它们执行的方法分别是“Java -J”、“Java -XXaltJVM=”或“Java -J-XXaltJVM=”。如果是第一种参数传递方式,CheckJVMType函数会取参数‘-J’后面的JVM名称,然后从已知的JVM配置参数中 查找如果找到同名的则去掉该JVM名称前的‘-’直接返回该值;而第二种方法,会直接返回“-XXaltJVM=”或“-J-XXaltJVM=”后面的 JVM类型名称;如果在运行Java时未指定上面两种方法中的任一一种参数,CheckJVMType会取配置文件中第一个配置中的JVM名称,去掉名称 前面的‘-’返回该值。CheckJVMType函数的这个返回值会在下面的函数中汇同jre路径组合成JVM.dll的绝对路径。如果没有指定这会使用 JVM.cfg中第一个定义的JVM.可以通过set _Java_LAUNCHER_DEBUG=1在控制台上测试.
最后获得JVM.dll的路径,JRE路径+/bin+/JVM类型字符串+/JVM.dll就是JVM的文件路径了,但是如果在调用 Java程序时用-XXaltJVM=参数指定的路径path,就直接用path+/JVM.dll文件做为JVM.dll的文件路径.
二:装载JVM.dll
通过第一步已经找到了JVM的路径,Java通过LoadJavaVM来装入JVM.dll文件.装入工作很简单就是调用Windows API函数:
LoadLibrary装载JVM.dll动态连接库.然后把JVM.dll中的导出函数JNI_CreateJavaVM和 JNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions变量的CreateJavaVM和 GetDefaultJavaVMInitArgs函数指针变量上。JVM.dll的装载工作宣告完成。
三:初始化JVM,获得本地调用接口, 这样就可以在Java中调用JVM的函数了.调用 InvocationFunctions->CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的 实例.
四:运行Java程序.
Java程序有两种方式一种是jar包,一种是class. 运行jar,Java -jar XXX.jar运行的时候,Java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用Java类 Java.util.jar.JarFileJNIEnv中方法getManifest()并从返回的Manifest对象中取 getAttributes("Main-Class")的值即jar包中文件:META-INF/MANIFEST.MF指定的Main-Class的 主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。main 函数直接调用Java.c中LoadClass方法装载该类。如果是执行class方法。main函数直接调用Java.c中LoadClass方法装载 该类。
然后main函数调用JNIEnv实例的GetStaticMethodID方法查找装载的class主类中
“public static void main(String[] args)”方法,并判断该方法是否为public方法,然后调用JNIEnv实例的
CallStaticVoidMethod方法调用该Java类的main方法。
JVM工作原理和特点介绍到这里。
http://developer.51cto.com/art/200907/135143.htm
在一个jvm实例的内部,类信息被存储在一个称为方法区的内存逻辑区中。类信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量 也存储在方法区中。
jvm实现的设计者决定了类 信息的内部表现形式 。如,多字节变量在类文件是以big-endian存储的,但在加载到方法区后,其存放形式由 jvm根据不同的平台来具体定义。
jvm在运行应用时要大量使用存储在方法区中的类信息。在类信息的表示上,设计者除了要尽可能提高应用的运行效率外,还要考虑空间问题。根据 不同的需求,jvm的实现者可以在时间和空间上追求一种平衡。
因为方法区是被所有线程共享的,所以必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类还没有被加载的情况下,只应该有 一个线程去加载,而另一个线程等待。
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。同样方法区也不必是连续的。方法区可以在堆(甚至是虚拟机自己的堆)中分配。 jvm可以允许用户和程序指定方法区的初始大小,最小和最大尺寸。
方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展java程序,一些类也会成为垃圾。jvm可以回收一个未被引用类所占的空间, 以使方法区的空间最小。
类信息
对每个加载的类,jvm必须在方法区中存储以下类信息:
一 这个类的完整有效名
二 这个类直接父类的完整有效名 (除非这个类是interface或是
java.lang.Object,两种情况下都没有父类)
三 这个类的修饰符 (public,abstract, final的某个子集)
四 这个类直接接口的一个有序列表
类名称在java类文件和jvm中都以完整有效名出现。在java源代码中,完整有效名由类的所属包名称加一个".",再加上类名
组成。例如,类Object的所属包为java.lang,那它的完整名称为java.lang.Object,但在类文件里,所有的"."都被
斜杠“/”代替,就成为java/lang/Object。完整有效名在方法区中的表示根据不同的实现而不同。
除了以上的基本信息外,jvm还要为每个类保存以下信息:
类的常量池 ( constant pool)
域(Field)信息
方法(Method)信息
除了常量外的所有静态(static)变量
常量池
jvm为每个已加载的类都维护一个常量池。常量池就是这个类用到的常量的一个有序集合,包括实际的常量(string,
integer, 和floating point常量)和对类,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。
因为常量池存储了一个类所使用到的所有类,域和方法的符号引用,所以它在java程序的动态链接中起了核心的作用。
域信息
jvm必须在方法区中保存类的所有域的相关信息以及域的声明顺序,
域的相关信息包括:
域名
域类
域修饰符(public, private, protected,static,final volatile, transient的某个子集)
方法信息
jvm必须保存所有方法的以下信息,同样域信息一样包括声明顺序
方法名
方法的返回类(或 void)
方法参数的数量和类(有序的)
方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)除了abstract和native方法外,其他方法还有保存方法的字节码(bytecodes)操作数栈和方法栈帧的局部 变量区的大小
异常表
类变量 (
Class Variables
译者:就是类的静态变量,它只与类相关,所以称为类变量
)
类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在jvm使 用一个类之前,它必须在方法区中为每个non-final类变量分配空间。
常量 (被声明为final的类变量)的处理方法则不同,每个常量都会在常量池中有一个拷贝。non-final类变量被存储在声明它的
类信息内,而final类被存储在所有使用它的类信息内。
对类加载器 的引用
jvm必须知道一个类是由启动加载器加载的还是由用户类加载器加载的。如果一个类是由用户类加载器加载的,那么jvm会将这个类加载器的一个 引用作为类信息的一部分保存在方法区中。
jvm在动态链接的时候需要这个信息。当解析一个类到另一个类的引用的时候,jvm需要保证这两个类的类加载器是相同的。这对jvm区分名 字空间的方式是至关重要的。
对Class类的引用
jvm为每个加载的类(译者:包括类和接口)都创建一个java.lang.Class的实例 。而jvm必须以某种方式把Class的这个实例 和存储在方法区中的类数据联系起来。
你可以通过Class类的一个静态方法得到这个实例的引用// A method declared in class java.lang.Class:
public static Class forName(String className);
假如你调用forName("java.lang.Object"),你会得到与java.lang.Object对应的类对象。你甚至可以通过 这个函数
得到任何包中的任何已加载的类引用,只要这个类能够被加载到当前的名字空间。如果jvm不能把类加载到当前名字空间,
forName就会抛出ClassNotFoundException。
(译者:熟悉COM的朋友一定会想到,在COM中也有一个称为 类对象(Class Object)的东东,这个类对象主要 是实现一种工厂模式,而java由于有了jvm这个中间 层,类对象可以很方便的提供更多的信息。这两种类对象 都是Singleton的)
也可以通过任一对象的getClass()函数得到类对象的引用,getClass被声明在Object类中:
// A method declared in class java.lang.Object:
public final Class getClass();
例如,假如你有一个java.lang.Integer的对象引用,可以激活getClass()得到对应的类引用。
通过类对象的引用,你可以在运行中获得相应类存储在方法区中的类信息,下面是一些Class类提供的方法:
// Some of the methods declared in class java.lang.Class:
public String getName();
public Class getSuperClass();
public boolean isInterface();
public Class[] getInterfaces();
public ClassLoader getClassLoader();
这些方法仅能返回已加载类的信息。getName()返回类的完整名,getSuperClass()返回父类的类对 象,isInterface()判断是否是接口。getInterfaces()返回一组类对象,每个类对象对应一个直接父接口。如果没有,则返回一个长 度为零的数组。
getClassLoader()返回类加载器的引用,如果是由启动类加载器加载的则返回null。所有的这些信息都直接从方法区中获得。
方法表
为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。 jvm对每个加载的非虚拟类的类信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)。jvm可以通过方法表快速激 活实例方法。(译者:这里的方法表与C++中的虚拟函数表一样,但java方法全都 是virtual的,自然也不用虚拟二字了。正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,个人认为java的设计者 始终是把安全放在效率之上的,所有java才更适合于网络开发)
一个例子
为了显示jvm如何使用方法区中的信息,我们据一个例子,我们
看下面这个类:
class Lava {
private int speed = 5; // 5 kilometers per hour
void flow() {
}
}
class Volcano {
public static void main(String[] args) {
Lava lava = new Lava();
lava.flow();
}
}
下面我们描述一下main()方法的第一条指令的字节码是如何被执行的。不同的jvm实现的差别很大,这里只是其中之一。
为了运行这个程序,你以某种方式把“Volcano"传给了jvm。有了这个名字,jvm找到了这个类文件(Volcano.class)并读 入,它从
类文件提取了类信息并放在了方法区中,通过解析存在方法区中的字节码,jvm激活了main()方法,在执行时,jvm保持了一个指向当前类 (Volcano)常量池的指针。
注意jvm在还没有加载Lava类的时候就已经开始执行了。正像大多数的jvm一样,不会等所有类都加载了以后才开始执行,它只会在需要的时候才 加载。
main()的第一条指令告知jvm为列在常量池第一项的类分配足够的内存。jvm使用指向Volcano常量池的指针找到第一项,发现是一个对 Lava类的符号引用,然后它就检查方法区看lava是否已经被加载了。
这个符号引用仅仅是类lava的完整有效名”lava“。这里我们看到为了jvm能尽快从一个名称找到一个类,一个良好的数据结构是多么重要。这 里jvm的实现者可以采用各种方法,如hash表,查找树等等。同样的算法可以用于Class类的forName()的实现。
当jvm发现还没有加载过一个称为"Lava"的类,它就开始查找并加载类文件"Lava.class"。它从类文件中抽取类信息并放在了方法 区中。
jvm于是以一个直接指向方法区lava类的指针替换了常量池第一项的符号引用。以后就可以用这个指针快速的找到lava类了。而这个替换过程称 为常量池解析(constant pool resolution)。在这里我们替换的是一个native指针。
jvm终于开始为新的lava对象分配空间了。这次,jvm仍然需要方法区中的信息。它使用指向lava数据的指针(刚才指向volcano常量 池第一项的指针)找到一个lava对象究竟需要多少空间。
jvm总能够从存储在方法区中的类信息知道某类对象需要的空间。但一个对象在不同的jvm中可能需要不同的空间,而且它的空间分布也是不同 的。(译者:这与在C++中,不同的编译器也有不同的对象模型是一个道理)
一旦jvm知道了一个Lava对象所要的空间,它就在堆上分配这个空间并把这个实例的变量speed初始化为缺省值0。假如lava的父对象也有 实例变量,则也会初始化。
当把新生成的lava对象的引用压到栈中,第一条指令也结束了。下面的指令利用这个引用激活java代码把speed变量设为初始值,5。另外一 条指令会用这个引用激活Lava对象的flow()方法。
http://hetaohappy1.javaeye.com/blog/780603