java JVM运行机制和原理
本文参考自:https://www.cnblogs.com/lishun1005/p/6019678.html 和 https://blog.csdn.net/albenxie/article/details/70145603 http://www.cnblogs.com/CongLollipop/p/6665606.html
一、java
1.java是一种技术
说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:
运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),JVM才能识别并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。
JVM运行字节码:字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。
利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)屏蔽了与具体操作系统平台相关的信息,实现了程序与操作系统的分离,从而实现了Java 的平台无关性,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
2.java平台:
Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:
在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。
二、JVM
1.JVM概念:
JVM即 Java Virtual Machine ,Java虚拟机。它是由软件技术模拟出计算机运行的一个虚拟的计算机。
Java虚拟机主要由字节码指令集、寄存器、栈、垃圾回收堆和存储方法域等构成。
2.JVM周期:
JVM在Java程序开始执行的时候,它才运行,程序结束的时它就停止。
一个Java程序会开启一个JVM进程,如果一台机器上运行三个程序,那么就会有三个运行中的JVM进程。
JVM中的线程分为两种:守护线程和普通线程
守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。
普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。
权限足够的话,可以调用exit()方法终止程序。
3.JVM的结构体系:
·每个JVM都有两种机制:
①类装载子系统:装载具有适合名称的类或接口
类装载子系统也可以称之为类加载器,JVM默认提供三个类加载器:
1、BootStrap ClassLoader :称之为启动类加载器,是最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jar、resources.jar、charsets.jar等。
2、Extension ClassLoader:称之为扩展类加载器,负责加载Java的扩展类库,默认加载$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
3、App ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。
除了Java默认提供的三个ClassLoader(加载器)之外,我们还可以根据自身需要自定义ClassLoader,自定义ClassLoader必须继承java.lang.ClassLoader 类。除了BootStrap ClassLoader 之外的另外两个默认加载器都是继承自java.lang.ClassLoader 。BootStrap ClassLoader 不是一个普通的Java类,它底层由C++编写,已嵌入到了JVM的内核当中,当JVM启动后,BootStrap ClassLoader 也随之启动,负责加载完核心类库后,并构造Extension ClassLoader 和App ClassLoader 类加载器。
类加载器子系统不仅仅负责定位并加载类文件,它还严格按照以下步骤做了很多事情:
1、加载:寻找并导入Class文件的二进制信息
2、连接:进行验证、准备和解析
1)验证:确保导入类型的正确性
2)准备:为类型分配内存并初始化为默认值
3)解析:将字符引用解析为直接引用
3、初始化:调用Java代码,初始化类变量为指定初始值
②执行引擎:负责执行包含在已装载的类或接口中的指令 。
·每个JVM都包含:
方法区、Java堆、Java栈、本地方法栈、指令计数器及其他隐含寄存器
(1)Class文件
Class文件由Java编译器生成,我们创建的.Java文件在经过编译器后,会变成.Class的文件,这样才能被JVM所识别并运行。
(2)方法区:(Method Area)-----永久区
在JVM中,类型信息和类静态变量都保存在方法区中,类型信息是由类加载器在类加载的过程中从类文件中提取出来的信息。
需要注意的一点是,常量池也存放于方法区中。
程序中所有的线程共享一个方法区,所以访问方法区的信息必须确保线程是安全的。如果有两个线程同时去加载一个类,那么只能有一个线程被允许去加载这个类,另一个必须等待。
在程序运行时,方法区的大小是可以改变的,程序在运行时可以扩展。
方法区也可以被垃圾回收,但条件非常严苛,必须在该类没有任何引用的情况下,详情可以参考另一篇文章:Java性能优化之JVM GC(垃圾回收机制) - 知乎专栏
类型信息包括什么?
1、类型的全名(The fully qualified name of the type)
2、类型的父类型全名(除非没有父类型,或者父类型是java.lang.Object)(The fully qualified name of the typeís direct superclass)
3、该类型是一个类还是接口(class or an interface)(Whether or not the type is a class )
4、类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)
5、所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)
6、类型的字段信息(Field information)
7、类型的方法信息(Method information)
8、所有静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)
9、一个指向类加载器的引用(A reference to class ClassLoader)
10、一个指向Class类的引用(A reference to class Class)
11、基本类型的常量池(The constant pool for the type)
方法列表(Method Tables)
为了更高效的访问所有保存在方法区中的数据,在方法区中,除了保存上边的这些类型信息之外,还有一个为了加快存取速度而设计的数据结构:方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例方法的引用,保存那些父类中调用的方法。
JVM 常量池
JVM常量池也称之为运行时常量池,它是方法区(Method Area)的一部分。用于存放编译期间生成的各种字面量和符号引用。运行时常量池不要求一定只有在编译器产生的才能进入,运行期间也可以将新的常量放入池中,这种特性被开发人员利用比较多的就是String.intern()方法。
由“用于存放编译期间生成的各种字面量和符号引用”这句话可见,常量池中存储的是对象的引用而不是对象的本身。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,它也实现了对象的共享。
例如字符串常量池:在编译阶段就把所有字符串文字放到一个常量池中。
1、节省内存空间:常量池中如果有对应的字符串,那么则返回该对象的引用,从而不必再次创建一个新对象。
2、节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,==判断引用是否相等,也就可以判断实际值是否相等。
双等号(==)的含义
基本数据类型之间使用双等号,比较的是数值。
复合数据类型(类)之间使用双等号,比较的是对象的引用地址是否相等。
八种基本类型的包装类和常量池
Byte、Short、Integer、Long、Character、Boolean、String这7种包装类都各自实现了自己的常量池。
//例子:
Integer i1 = 20;
Integer i2 = 20;
System.out.println(i1=i2);//输出TRUE
Byte、Short、Integer、Long、Character这5种包装类都默认创建了数值[-128 , 127]的缓存数据。当对这5个类型的数据不在这个区间内的时候,将会去创建新的对象,并且不会将这些新的对象放入常量池中。
//IntegerCache.low = -128
//IntegerCache.high = 127
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//例子
Integer i1 = 200;
Integer i2 = 200;
System.out.println(i1==i2);//返回FALSE
Float 和Double 没有实现常量池。
String包装类与常量池
String str1 = "aaa";
当以上代码运行时,JVM会到字符串常量池查找 "aaa" 这个字面量对象是否存在?
存在:则返回该对象的引用给变量 str1 。
不存在:则在堆中创建一个相应的对象,将创建的对象的引用存放到常量池中,同时将引用返回给变量 str1 。
String str1 = "aaa";
String str2 = "aaa";
System.out.println(str1 == str2);//返回TRUE
因为变量str1 和str2 都指向同一个对象,所以返回true。
String str3 = new String("aaa");
System.out.println(str1 == str3);//返回FALSE
当我们使用了new来构造字符串对象的时候,不管字符串常量池中是否有相同内容的对象的引用,新的字符串对象都会创建。因为两个指向的是不同的对象,所以返回FALSE 。
String.intern()方法
对于使用了new 创建的字符串对象,如果想要将这个对象引用到字符串常量池,可以使用intern() 方法。
调用intern() 方法后,检查字符串常量池中是否有这个对象的引用,并做如下操作:
存在:直接返回对象引用给变量。
不存在:将这个对象引用加入到常量池,再返回对象引用给变量。
String interns = str3.intern();
System.out.println(interns == str1);//返回TRUE
假定常量池中都没有以上字面量的对象,以下创建了多少个对象呢?
String str4 = "abc"+"efg";
String str5 = "abcefg";
System.out.println(str4 == str5);//返回TRUE
答案是三个。第一个:"abc" ,第一个:"efg",第三个:"abc"+"efg"("abcefg")
String str5 = "abcefg"; 这句代码并没有创建对象,它从常量池中找到了"abcefg" 的引用,所有str4 == str5 返回TRUE,因为它们都指向一个相同的对象。
什么情况下会将字符串对象引用自动加入字符串常量池?
//只有在这两种情况下会将对象引用自动加入到常量池
String str1 = "aaa";
String str2 = "aa"+"a";
//其他方式下都不会将对象引用自动加入到常量池,如下:
String str3 = new String("aaa");
String str4 = New StringBuilder("aa").append("a").toString();
StringBuilder sb = New StringBuilder();
sb.append("aa");
sb.append("a");
String str5 = sb.toString();
JVM常量池也称之为运行时常量池,它是方法区(Method Area)的一部分。用于存放编译期间生成的各种字面量和符号引用。运行时常量池不要求一定只有在编译器产生的才能进入,运行期间也可以将新的常量放入池中,这种特性被开发人员利用比较多的就是String.intern()方法。
由“用于存放编译期间生成的各种字面量和符号引用”这句话可见,常量池中存储的是对象的引用而不是对象的本身。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,它也实现了对象的共享。
例如字符串常量池:在编译阶段就把所有字符串文字放到一个常量池中。
1、节省内存空间:常量池中如果有对应的字符串,那么则返回该对象的引用,从而不必再次创建一个新对象。
2、节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,==判断引用是否相等,也就可以判断实际值是否相等。
双等号(==)的含义
基本数据类型之间使用双等号,比较的是数值。
复合数据类型(类)之间使用双等号,比较的是对象的引用地址是否相等。
八种基本类型的包装类和常量池
Byte、Short、Integer、Long、Character、Boolean、String这7种包装类都各自实现了自己的常量池。
//例子:
Integer i1 = 20;
Integer i2 = 20;
System.out.println(i1=i2);//输出TRUE
Byte、Short、Integer、Long、Character这5种包装类都默认创建了数值[-128 , 127]的缓存数据。当对这5个类型的数据不在这个区间内的时候,将会去创建新的对象,并且不会将这些新的对象放入常量池中。
//IntegerCache.low = -128
//IntegerCache.high = 127
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//例子
Integer i1 = 200;
Integer i2 = 200;
System.out.println(i1==i2);//返回FALSE
Float 和Double 没有实现常量池。
String包装类与常量池
String str1 = "aaa";
当以上代码运行时,JVM会到字符串常量池查找 "aaa" 这个字面量对象是否存在?
存在:则返回该对象的引用给变量 str1 。
不存在:则在堆中创建一个相应的对象,将创建的对象的引用存放到常量池中,同时将引用返回给变量 str1 。
String str1 = "aaa";
String str2 = "aaa";
System.out.println(str1 == str2);//返回TRUE
因为变量str1 和str2 都指向同一个对象,所以返回true。
String str3 = new String("aaa");
System.out.println(str1 == str3);//返回FALSE
当我们使用了new来构造字符串对象的时候,不管字符串常量池中是否有相同内容的对象的引用,新的字符串对象都会创建。因为两个指向的是不同的对象,所以返回FALSE 。
String.intern()方法
对于使用了new 创建的字符串对象,如果想要将这个对象引用到字符串常量池,可以使用intern() 方法。
调用intern() 方法后,检查字符串常量池中是否有这个对象的引用,并做如下操作:
存在:直接返回对象引用给变量。
不存在:将这个对象引用加入到常量池,再返回对象引用给变量。
String interns = str3.intern();
System.out.println(interns == str1);//返回TRUE
假定常量池中都没有以上字面量的对象,以下创建了多少个对象呢?
String str4 = "abc"+"efg";
String str5 = "abcefg";
System.out.println(str4 == str5);//返回TRUE
答案是三个。第一个:"abc" ,第一个:"efg",第三个:"abc"+"efg"("abcefg")
String str5 = "abcefg"; 这句代码并没有创建对象,它从常量池中找到了"abcefg" 的引用,所有str4 == str5 返回TRUE,因为它们都指向一个相同的对象。
什么情况下会将字符串对象引用自动加入字符串常量池?
//只有在这两种情况下会将对象引用自动加入到常量池
String str1 = "aaa";
String str2 = "aa"+"a";
//其他方式下都不会将对象引用自动加入到常量池,如下:
String str3 = new String("aaa");
String str4 = New StringBuilder("aa").append("a").toString();
StringBuilder sb = New StringBuilder();
sb.append("aa");
sb.append("a");
String str5 = sb.toString();
(3)Java堆(JVM堆、Heap)---年轻代,老年代
当Java创建一个类的实例对象或者数组时,都在堆中为新的对象分配内存。
虚拟机中只有一个堆,程序中所有的线程都共享它。
堆占用的内存空间是最多的。
堆的存取类型为管道类型,先进先出。
在程序运行中,可以动态的分配堆的内存大小。
堆的内存资源回收是交给JVM GC进行管理的,详情请参考:Java性能优化之JVM GC(垃圾回收机制) - 知乎专栏
(4)Java栈(JVM Stack)-用来加载方法的内存模型
java虚拟机栈:用来执行逻辑指令;执行方法运行时期的内存模型;线程内部独有
虚拟机栈的内存空间就是:队列;每一个单位:帧--》线程加载的每一个方法内存数据FILO(First in last out)
虚拟机栈-优化参数:定义队列里有多少个帧->即定义虚拟机栈的大小
(递归会导致虚拟机栈爆掉吗?如下图:调用递归函数,执行后console:压栈压到6232次后,虚拟机栈爆掉了,所以不想浪费时间,不想压那么多栈,要进行性能调优,如果不断进行压栈,这个线程会莫名占用一大撮的栈空间,其他线程占用的虚拟机栈的空间会变小,导致线程的数量会变少,因为每个线程必须要分配这么大的一个位置,其中一个线程不断进行压栈压栈,需要的空间比其他线程多很多倍,所以导致整个操作系统的线程会变少,所以会有一个控制参数Xss--》来优化栈空间,)系统分配线程数量上限就会提升,并发量就会有提升。如图:设置-Xss为128k(限制线程栈的空间,线程的高并发数量就会增多),再次执行main方法,调优后,从6000多次,到1036次就出现异常了,这样就可以把线程申请的无线空间进行控制,顶多只能使用这么多。
(扩展:为什么JVM当中的虚拟机栈要设计为FILO?:例如java要执行MethodOne()方法,然后MethodOne()中调用方法MethodTwo(),如果先进先出的话,我们要拿到MethodTwo()的返回结果,这个时候MethodOne()已经退出了,MethodOne()在虚拟机栈已经不存在数据了,MethodTwo()的执行将没有意义;所以必须要先进后出,后进来的执行完,先进来的才能出去)
(扩展:对于操作系统来说,为什么线程资源是有限的?(阿里面试):实际上我们的内存是根据我们的操作位,例如32位操作系统,我们能得到的最大物理内存是3G,操作系统本身数据会占有内存空间,实际给我们java的运行内存是2G,这其中还有java堆,方法区,都在2G内存中瓜分,剩下由虚拟机栈和本地方法栈去瓜分;虚拟机栈就是预留给我们成千上万的线程去请求,比如虚拟机栈只有100MB,线程初始化会申请-Xss,初始化时有一个参数会控制复合栈的申请,通常是默认5MB ,所以线程的复合栈申请初始化越大,线程数量越少)
在Java栈中只保存基础数据类型(参考:Java 基本数据类型 - 四类八种 - 知乎专栏)和自定义对象的引用,注意只是对象的引用而不是对象本身哦,对象是保存在堆区中的。
拓展知识:像String、Integer、Byte、Short、Long、Character、Boolean这六个属于包装类型,它们是存放于堆中的。
栈的存取类型为类似于水杯,先进后出。
栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。
每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。
每个线程都会建立一个操作栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,每个栈帧包含了三部分:
局部变量区(方法内基本类型变量、变量对象指针)
操作数栈区(存放方法执行过程中产生的中间结果)
运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)
(5)本地方法栈(Native Method Stack)
本地方法栈的功能和JVM栈非常类似,用于存储本地方法的局部变量表,本地方法的操作数栈等信息。
栈的存取类型为类似于水杯,先进后出。
栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。
每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。
本地方法栈是在程序调用或JVM调用本地方法接口(Native)时候启用。
本地方法都不是使用Java语言编写的,比如使用C语言编写的本地方法,本地方法也不由JVM去运行,所以本地方法的运行不受JVM管理。
HotSpot VM将本地方法栈和JVM栈合并了。
(6)程序计数器
执行引擎肯定是线程,线程执行类加载器,类加载器执行这段代码,线程执行这段代码之后,CUP设计属于抢占式的,所以CPU对线程分配时间片--》执行这段代码--》没有执行完这段代码,时间片没有了,时间片分配给其他线程;重新拿到时间片时,线程为什么知道他上次执行到哪里,从哪里再次执行?--》因为有程序计数器:线程内部独占的,每个线程抓住一个程序计数器,用来记录当前字节码的句柄,下次再抢到时间片时,就知道从哪里开始执行。
在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器。
程序计数器仅占很小的一块内存空间。
当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空(Underfined)。
程序计数器这个内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError(内存不足错误)的区域。
(7)JVM执行引擎
Java虚拟机相当于一台虚拟的“物理机”,这两种机器都有代码执行能力,其区别主要是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的。而JVM的执行引擎是自己实现的,因此程序员可以自行制定指令集和执行引擎的结构体系,因此能够执行那些不被硬件直接支持的指令集格式。
在JVM规范中制定了虚拟机字节码执行引擎的概念模型,这个模型称之为JVM执行引擎的统一外观。JVM实现中,可能会有两种的执行方式:解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码)。有些虚拟机只采用一种执行方式,有些则可能同时采用两种,甚至有可能包含几个不同级别的编译器执行引擎。
输入的是字节码文件、处理过程是等效字节码解析过程、输出的是执行结果。在这三点上每个JVM执行引擎都是一致的。
(8)本地方法接口(JNI)
JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C和C++)。
JNI的适用场景
当我们有一些旧的库,已经使用C语言编写好了,如果要移植到Java上来,非常浪费时间,而JNI可以支持Java程序与C语言编写的库进行交互,这样就不必要进行移植了。或者是与硬件、操作系统进行交互、提高程序的性能等,都可以使用JNI。需要注意的一点是需要保证本地代码能工作在任何Java虚拟机环境。
JNI的副作用
一旦使用JNI,Java程序将丢失了Java平台的两个优点:
1、程序不再跨平台,要想跨平台,必须在不同的系统环境下程序编译配置本地语言部分。
2、程序不再是绝对安全的,本地代码的使用不当可能会导致整个程序崩溃。一个通用规则是,调用本地方法应该集中在少数的几个类当中,这样就降低了Java和其他语言之间的耦合。
4.JVM启动过程
1)JVM的装入环境和配置
在学习这个之前,我们需要了解一件事情,就是JDK和JRE的区别。
JDK是面向开发人员使用的SDK,它提供了Java的开发环境和运行环境,JDK中包含了JRE。
JRE是Java的运行环境,是面向所有Java程序的使用者,包括开发者。
JRE = 运行环境 = JVM。
如果安装了JDK,会发现电脑中有两套JRE,一套位于/Java/jre.../下,一套位于/Java/jdk.../jre下。那么问题来了,一台机器上有两套以上JRE,谁来决定运行那一套呢?这个任务就落到java.exe身上,java.exe的任务就是找到合适的JRE来运行java程序。
java.exe按照以下的顺序来选择JRE:
1、自己目录下有没有JRE
2、父目录下有没有JRE
3、查询注册表: HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\"当前JRE版本号"\JavaHome
这几步的主要核心是为了找到JVM的绝对路径。
jvm.cfg的路径为:JRE路径\lib\"CPU架构"\jvm.fig
jvm.cfg的内容大致如下:
-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
KNOWN 表示存在 、IGNORE 表示不存在 、ALIASED_TO 表示给别的JVM去一个别名
WARN 表示不存在时找一个替代 、ERROR 表示不存在抛出异常
2)装载JVM
通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。
LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。
3)初始化JVM,获得本地调用接口
调用InvocationFunction -> CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。
4)运行Java程序
JVM运行Java程序的方式有两种:jar包 与 Class
运行jar 的时候,Java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。
运行Class的时候,main函数直接调用Java.c中的LoadClass方法装载该类。
5.Java代码编译和执行的整个过程
也正如前面所说,Java代码的编译和执行的整个过程大概是:开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。
(1)Java代码编译是由Java源码编译器来完成,也就是Java代码到JVM字节码(.class文件)的过程。 流程图如下所示:
(2)Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
Java代码编译和执行的整个过程包含了以下三个重要的机制:
·Java源码编译机制
·类加载机制
·类执行机制
(1)Java源码编译机制
Java 源码编译由以下三个过程组成:
①分析和输入到符号表
②注解处理
③语义分析和生成class文件
流程图如下所示:
最后生成的class文件由以下部分组成:
①结构信息:包括class文件格式版本号及各部分的数量与大小的信息
②元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
③方法信息:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息
(2)类加载机制
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加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
(3)类执行机制
JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
JVM执行class字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。栈的结构如下图所示:
6.JVM内存管理及垃圾回收机制
JVM内存结构分为:方法区(method),栈内存(stack),堆内存(heap),本地方法栈(java中的jni调用),结构图如下所示:
(1)堆内存(heap)
所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。但由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。这时由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,它不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然这种方法用起来最不方便,但是速度快,也是最灵活的。堆内存是向高地址扩展的数据结构,是不连续的内存区域。由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
(2)栈内存(stack)
在Windows下, 栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 由系统自动分配,速度较快。但程序员是无法控制的。
堆内存与栈内存需要说明:
基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量 。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。方法调用时传入的literal参数,先在栈空间分配,在方法调用完成后从栈空间收回。字符串常量、static在DATA区域分配,this在堆空间分配。数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小。
如:
(3)本地方法栈(java中的jni调用)
用于支持native方法的执行,存储了每个native方法调用的状态。对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。
(4)方法区(method)
它保存方法代码(编译后的java代码)和符号表。存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(Permanet Generation)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。
垃圾回收机制
堆里聚集了所有由应用程序创建的对象,JVM也有对应的指令比如 new, newarray, anewarray和multianewarray,然并没有向 C++ 的 delete,free 等释放空间的指令,Java的所有释放都由 GC 来做,GC除了做回收内存之外,另外一个重要的工作就是内存的压缩,这个在其他的语言中也有类似的实现,相比 C++ 不仅好用,而且增加了安全性,当然她也有弊端,比如性能这个大问题。
7.Java虚拟机的运行过程示例
上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。
虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:
编译后在命令行模式下键入: java HelloApp run virtual machine
将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行HelloApp时可能采取的步骤。
开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下: