目录
1 JVM架构引言
2 JVM安全框架
3 JVM内部机理
3.1 JVM的生命周期
3.2 JVM的框架
3.3 数据类型
3.3.1 Java数据类型
3.3.2 浮点运算
3.4 方法区
3.5 操作数栈
3.6 本地方法栈
3.7 执行引擎
4 类文件结构
5 线程同步
6 垃圾回收机制
7 总结
8 参考资料
文中的图片没有上传,不过想看图片的可以下载原文。
本文下载地址 JVM学习笔记.doc
1 JVM架构引言
Java 的平*立,安全和网络可移植性是的 Java 最适合网络计算环境,java 的四大核心技术是,Java 语言,字节码格式,Java API,JVM。每次你编写Java程序时你就用到了这四项技术,那么你是否对他们有足够的了解呢?
很多人把 JVM 看成是 Java 的解释器,这是错误的理解,并不是所有的 JVM 实现都 Java 的解释器,有的也用到了 JIT (Just-In-Time) 技术。Java 是不允许直接操作本地机器指令的,对于Java方法来说有两种:Java和native,对于native我更习惯用C++ 写DLL,但Java并不提倡你这么做,因为有损Java平*立性。JVM 中除了执行引擎就是类加载器了,ClassLoader也分为两种:原始加载器和加载器Object,原始加载器使用和写JVM 一样的语言写的,比如用C写的类加载器,而加载器Object就是用 Java 实现的类加载器,方便我们扩展,比如你自己可以 New 一个 URLClassLoader 从网络上下载字节码到本地运行。一个类的加载和他参考的类的加载应该用同一个 ClassLoader。这一点在发生异常的时候很难找出,比如 OSGI 中每个 bundle 都有自己独立的 ClassLoader,对于新手很容易犯错误而无从下手,我们熟悉的WEB 服务器 Tomcat 的类加载器是分层(有继承关系)的,所以在应用整合的时候也很容易发生 ClassLoader 相关的异常,而这样的异常往往很难定位。平台互异的字节序问题,在Java中,字节码是大字节序的。Java 为支持开发者开发应用软件提供了大量的 API,可以说,在计算机领域的大部分计算中Java都有对应的解决方案。
C++中可能比较受关注和困扰的就是指针了,而在Java中用“参考”这样一个类似的东西代替了,参考不向指针那样允许参与计算,避免了开发人员直接操作内存,还有个垃圾回收机制也避免了开发者手动释放内存,还有就是 C++ 中的数组是不进行边界检查的而Java中每次使用数组的时候都要进行边界检查,岂不安全。 可见Java相比C++ 提高了开发效率和安全性。Java和C++ 比运行速度是个大问题,因此任何语言都不万能的,在开发是我们应该适当权衡,Java运行速度低的原因主要有:
Interpreting bytecodes is 10 to 30 times slower than native execution.
Just-in-time compiling bytecodes can be 7 to 10 times faster than interpreting, but still not quite as fast as native execution.
Java programs are dynamically linked.
The Java Virtual Machine may have to wait for class files to download across a network.
Array bounds are checked on each array access.
All objects are created on the heap (no objects are created on the stack).
All uses of object references are checked at run-time for null.
All reference casts are checked at run-time for type safety.
The garbage collector is likely less efficient (though often more effective) at managing the heap than you could be if you managed it directly as in C++.
Primitive types in Java are the same on every platform, rather than adjusting to the most efficient size on each platform as in C++.
Strings in Java are always UNICODE. When you really need to manipulate just an ASCII string, a Java program will be slightly less efficient than an equivalent C++ program.
2 JVM安全框架
Java 允许基于网络的代码运行和传播,为其带了安全问题。但Java也提供了内嵌的安全模型,开发者可高枕无忧。Java的沙箱安全模型使即时你不信任的代码也可让他在本机执行,如果是恶意的代码他的恶意行为也会被沙箱拦截,所以在运行任何你有点怀疑的代码前请确保你的沙箱没有缺陷。
对于沙箱的四大基础组件是:类加载器,类文件验证,JVM安全特性,安全管理的API,其中最重要的是类加载器和安全管理API,因为他们可以客制化。对于加载器,每个 JVM 都可以有多个,同一个类可以加载多次到不同的 ClassLoader 中,类跨ClassLoader是不可见的,而在同一ClassLoader中是可直接访问的,这样可以隔离一些不安全的类。
类型检查是很必要的,分为两个阶段,第一是在类加载进来的时候要进行类的合法性和完整性检查,第二是运行时确认该类所参考的类,方法和属性是否存在。类文件头都是以一个四个字节的幻数开头(0xCAFEBABE)来标识是个类文件,当然也有文件大小域,第一阶段确保加载进来的类是正确格式,内部一直,Java语法语义限制一直,包括安全的可执行代码,在这个过程中如果有错误,JVM会抛出异常,该类就不会被使用。第二阶段其实由于动态连接的原因,需要在运行时检查参考,因为 ClassLoader 在需要某些类时才去加载,延迟加载,在 ORM 产品中,比如 Hibernate, jdo 等都有所谓的延迟加载
SecurityManager 有一系列的 checkXXX 的方法,用来检测相关操作是否合法,一般我们的程序是不用 SecurityManager 的,除非你安装一个SecurityManager,如果没有写自己的策略文件,一般是用jre 下面的默认策略文件的设置,当然也可在 VM 运行参数设置策略文件的位置。SecurityManager 类的相关方法。
publicstatic SecurityManager getSecurityManager() {
return security;
}
publicstatic
void setSecurityManager(final SecurityManager s) {
try {
s.checkPackageAccess("java.lang");
} catch (Exception e) {
// no-op
}
setSecurityManager0(s);
}
privatestaticsynchronized
void setSecurityManager0(final SecurityManager s) {
SecurityManager sm = getSecurityManager();
if (sm != null) {
// ask the currently installed security manager if we
// can replace it.
sm.checkPermission(new RuntimePermission
("setSecurityManager"));
}
if ((s != null) && (s.getClass().getClassLoader() != null)) {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
s.getClass().getProtectionDomain().implies
(SecurityConstants.ALL_PERMISSION);
returnnull;
}
});
}
security = s;
InetAddressCachePolicy.setIfNotSet(InetAddressCachePolicy.FOREVER);}
设置自己的 SercurityManager:
System.setSecurityManager(new JackSecurityManager());
SecurityManager sm = System.getSecurityManager();
3 JVM内部机理
3.1 JVM的生命周期
任何一个类的 main 函数运行都会创建一个JVM实例,当main 函数结束JVM实例也就结束了,如果三个类分别运行各自的 main 函数则会创建3个不同的JVM实例。JVM实例启动时默认启动几个守护线程,而 main 方法的执行是在一个单独的非守护线程中执行的。只要母线程结束,子线程就自动销毁,只要非守护main 线程结束JVM实例就销毁了。
3.2 JVM的框架
JVM主要由 ClassLoader 子系统和执行引擎子系统组成,运行数据区分为五个部分,他们是方法区,堆,栈,指令寄存器,本地方法栈。方法区和堆是所有线程共享的,一般我们习惯对临时变量放在寄存器中,但JVM中不用寄存器而是用栈,每个线程都有自己独立的栈空间和指令计数器,其中栈里的元素叫帧,帧有三部分组成分别是局部变量,操作数栈和帧数据。正式因为栈是每个线程独有的,所以对于 local 变量,是没有多线程冲突问题的。栈和帧的数据结构和大小规范里没有硬性规定,由实现者具体实做。
3.3 数据类型
3.3.1 Java数据类型
虚拟机的数据类型分为:原始类型和参考类型,有人一直说Java只有90%是OO的,其中原始类型就算的上,不过还好有对应的封装类型如 int->Integer,JDK1.5可以用这些封装类型直接做算术运算,不过Java内部要做拆箱和装箱的工作。似乎 OO了些,不过现代的程序员才不过你这些,能用就行。
字长是数据值得基本单元,一般像 int, byte, short 等类型的值用一个字长存储,而浮现类型用两个字长存储,字长的大小一般和机器的指针大小相同,最小是32 位。
像byte, short, or char 这样的类型在方法区和堆中Java是原来类型,但在程序运行时放在栈帧中,统一用整型(int)来表示
对于类型之间的转换JVM有对应的指令如:
上图是小范围向大范围转变,叫做向上转型,当然也有向下转型的指令,不过可能会造成数据被破坏,如图:
不管你怎么变化,栈帧里都是以字为单位的,32位机子一个字是4个字节,正好是一个整型大小,如前所述原始类型在栈中都被表示为 int 型,当然如果是long 和 double 那就用两个字来表示咯。
3.3.2 浮点运算
浮点运算算法因该是倍受关注的了,Java中浮点数的表示遵照IEEE 754 1985的规范,这个规范定义了32位和64位浮点格式和运算,我都知道Java中的float 是32位而,double是64位。对于浮点数大家都很清楚他分为四个部分:符号,尾数,基数和幂,要得到浮点数值必须将这四个部分相乘才能得到值。所有的浮点数都要经过标准化,我们都知道计算机里只有0,1这样的表示,尾数和幂都要二进制表示,对于尾数float 用它的23位表示,double 用它的52位来表示,对于幂数 float 用它的 8位表示,double用它的11为表示,剩下以为表示符号。
JVM也有对应的浮点运算指令如图(加法指令)
3.4 方法区
ClassLoader负责把class加载进来并解析类信息放在方法区,方法区是被所有线程共享的,所以方法区必须保证线程的安全。比如两个线程同时需要一个类,必须保证只有一个线程去加载而另一个线程等待。
方法区的大小不是固定的,一般会在运行时JVM会根据运行情况作出调整,但JVM实现者都留有接口可供调节,比如最大和最小值。
我们都知道堆是被垃圾回收器回收的,在方法区的类也是会被回收的,类也有他的生命周期,当某个类不再被参考之后也就没有这个类的引用和对象了那么他的存在就没有意义了,只是站着内存,回收器就会按照一定的算法将它卸载了。对于一个类信息的表述都是用全名的,在方法区中也有其他信息,比如常量池,属性和方法信息,还有就是指向堆中的 ClassLoader和Class 的参考。我大家都熟悉的就是类的静态变量,也放在方法区的由所有的对象实例共享。
可以想象在方法区内放置了大量的二进制的Class信息,为了加快访问速度,JVM实现者都会维护一个方法表,记得读研一的时候中间件老师讲过这些东西是结合指针讲的C++ 的内存模型。另外注意的就是方法表只针对可实例化的类,对抽象类和接口没有意义。
每个JVM实例都只有一个堆,所有的线程共享其中的对象,这才出现了多线程安全问题。JVM有new 对象的指令但没有释放对象的指令,当让这些指令都是虚拟指令,这些对象的释放是有 GC 来做的,GC在JVM规范中并没有硬性的规定,有实现者设计他的实现形式和算法。
想必很多同人都想知道,对象是怎么样在堆里表示的,其实很简单。其实面JVM规范也没有细致的规定对象怎么在堆里表示的,
如图是一个参考的堆模型,具体实现可能不是这样的,这个是HeapOfFish applet 的一个演示模型,具体内容可以看看JVM规范。当然也有很多其他的模型,这个模型的好处就是在堆压缩的时候很方便,而在 reference 直接 point到一个对象的模型来说在堆压缩方面是很麻烦的,因为你要考虑到方法区,堆,栈里可能的参考,你都要修改。对象还有一个很重要的数据结构就是方法表,方法表可以加快访问速度,但并不是说所有的JVM实现都有。
堆中的每个对象都有指向方法区的指针,而自己主要保留对象属性信息,如图:
看一个方法区链接的例子,看看一个类是怎么加载进来,怎么链接初始化的:
有一 Salutation 类
class Salutation {
private static final String hello = "Hello, world!";
private static final String greeting = "Greetings, planet!";
private static final String salutation = "Salutations, orb!";
private static int choice = (int) (Math.random() * 2.99);
public static void main(String[] args) {
String s = hello;
if (choice == 1) {
s = greeting;
}
else if (choice == 2) {
s = salutation;
}
System.out.println(s);
}
}
Assume that you have asked a Java Virtual Machine to run Salutation. When the virtual machine starts, it attempts to invoke the main() method of Salutation. It quickly realizes, however, that it canít invoke main(). The invocation of a method declared in a class is an active use of that class, which is not allowed until the class is initialized. Thus, before the virtual machine can invoke main(), it must initialize Salutation. And before it can initialize Salutation, it must load and link Salutation. So, the virtual machine hands the fully qualified name of Salutation to the primordial class loader, which retrieves the binary form of the class, parses the binary data into internal data structures, and creates an instance of java.lang.Class.
常量池里的内容:
Index |
Type |
Value |
|
1 |
CONSTANT_String_info |
30 |
|
2 |
CONSTANT_String_info |
31 |
|
3 |
CONSTANT_String_info |
39 |
|
4 |
CONSTANT_Class_info |
37 |
|
5 |
CONSTANT_Class_info |
44 |
|
6 |
CONSTANT_Class_info |
45 |
|
7 |
CONSTANT_Class_info |
46 |
|
8 |
CONSTANT_Class_info |
47 |
|
9 |
CONSTANT_Methodref_info |
7, 16 |
|
10 |
CONSTANT_Fieldref_info |
4, 17 |
|
11 |
CONSTANT_Fieldref_info |
8, 18 |
|
12 |
CONSTANT_Methodref_info |
5, 19 |
|
13 |
CONSTANT_Methodref_info |
6, 20 |
|
14 |
CONSTANT_Double_info |
2.99 |
|
16 |
CONSTANT_NameAndType_info |
26, 22 |
|
17 |
CONSTANT_NameAndType_info |
41, 32 |
|
18 |
CONSTANT_NameAndType_info |
49, 34 |
|
19 |
CONSTANT_NameAndType_info |
50, 23 |
|
20 |
CONSTANT_NameAndType_info |
51, 21 |
|
21 |
CONSTANT_Utf8_info |
"()D" |
|
22 |
CONSTANT_Utf8_info |
"()V" |
|
23 |
CONSTANT_Utf8_info |
(Ljava/lang/String;)V" |
|
24 |
CONSTANT_Utf8_info |
([Ljava/lang/String;)V" |
|
25 |
CONSTANT_Utf8_info |
<clinit" |
|
26 |
CONSTANT_Utf8_info |
<init" |
|
27 |
CONSTANT_Utf8_info |
"Code" |
|
28 |
CONSTANT_Utf8_info |
"ConstantValue" |
|
29 |
CONSTANT_Utf8_info |
"Exceptions" |
|
30 |
CONSTANT_Utf8_info |
"Greetings,planet!" |
|
31 |
CONSTANT_Utf8_info |
"Hello, world!" |
|
32 |
CONSTANT_Utf8_info |
"I" |
|
33 |
CONSTANT_Utf8_info |
"LineNumberTable" |
|
34 |
CONSTANT_Utf8_info |
"Ljava/io/PrintStream;" |
|
35 |
CONSTANT_Utf8_info |
"Ljava/lang/String;" |
|
36 |
CONSTANT_Utf8_info |
"LocalVariables" |
|
37 |
CONSTANT_Utf8_info |
"Salutation" |
|
38 |
CONSTANT_Utf8_info |
"Salutation.java" |
|
39 |
CONSTANT_Utf8_info |
"Salutations, orb!" |
|
40 |
CONSTANT_Utf8_info |
"SourceFile" |
|
41 |
CONSTANT_Utf8_info |
"choice" |
|
42 |
CONSTANT_Utf8_info |
"greeting" |
|
43 |
CONSTANT_Utf8_info |
"hello" |
|
44 |
CONSTANT_Utf8_info |
"java/io/PrintStream" |
|
45 |
CONSTANT_Utf8_info |
"java/lang/Math" |
|
46 |
CONSTANT_Utf8_info |
"java/lang/Object" |
|
47 |
CONSTANT_Utf8_info |
"java/lang/System" |
|
48 |
CONSTANT_Utf8_info |
"main" |
|
49 |
CONSTANT_Utf8_info |
"out" |
|
50 |
CONSTANT_Utf8_info |
"println" |
|
51 |
CONSTANT_Utf8_info |
"random" |
|
52 |
CONSTANT_Utf8_info |
"salutation" |
As part of the loading process for Salutation, the Java Virtual Machine must make sure all of Salutationís superclasses have been loaded. To start this process, the virtual machine looks into Salutationís type data at the super_class item, which is a seven. The virtual machine looks up entry seven in the constant pool, and finds a CONSTANT_Class_info entry that serves as a symbolic reference to class java.lang.Object. See Figure 8-5 for a graphical depiction of this symbolic reference. The virtual machine resolves this symbolic reference, which causes it to load class Object. Because Object is the top of Salutationís inheritance hierarchy, the virtual machine and links and initializes Object as well.
3.5 操作数栈
操作数栈是Java运行时的核心栈,看看 i+j 的一个简单运算,
iload_0
iload_1
iadd
istore_2
以上是四个JVM指令,完成 i+j并把结果保存到k中,如图示:
在堆中不可能分配一个原始类型的空间放值,而是先用对象封装才能存在堆空间中,带Java栈中也不可能放对象,而只有原始类型和参考类型。上次有人争议数组放在何处?在Java中数组和对象是同等地位的,都放在堆中而他的参考是放在栈里,JVM有对应的指令比如 newarray, anewarray等。
3.6 本地方法栈
想必很多人用过 JNI 结束,Java是不提倡这么做的,而且在这放的设计和实现上,个人觉得不是那么好,至少他比不那么方便,所以很少见应用开发者去写些 Native 方法,每次你去看Java原代码是你经常看到native 方法,也看到JDK下的 DLL 文件,大部分JVM都是用C或C++写的。前面也提过,这样就破坏了Java的平*立性,在本地方法运行的时候也有专门的栈去处理。Java在执行本地方法的时候暂时放弃Java stack 的操作,转向本地方法,本地方法有自己的栈或堆的处理方式,Java在执行本地方法时会在本地栈和Java栈之间切换,如图:
a thread first invoked two Java methods, the second of which invoked a native method. This act caused the virtual machine to use a native method stack. In this figure, the native method stack is shown as a finite amount of contiguous memory space. Assume it is a C stack. The stack area used by each C-linkage function is shown in gray and bounded by a dashed line. The first C-linkage function, which was invoked as a native method, invoked another C-linkage function. The second C-linkage function invoked a Java method through the native method interface. This Java method invoked another Java method, which is the current method shown in the figure,做过JNI开发的朋友应该了解Java和C,C++ 是如何交互的,这些都是执行引擎的事。
3.7 执行引擎
执行引擎,应该是JVM的核心了,一般我把它看作指令集合。JVM规范详细的描述了每个指令的作用,但没有进一步描述如何实现,还由实现厂商自己设计,可以用解释的方式,JIT方式,编译本地代码的方式,或者几者混合的方式,当然也可用一些新的不为我们知道的技术。
每一个线程都有一个自己的执行引擎,据我了解JVM指令用一个字节表示,也就是JVM最多有256个指令,目前JVM已有160(目前可能多于这些)个指令。就有这百十个指令组成了我的系统,JVM指令一般只有操作码没有操作数,一般操作数放在常量池和Java栈中,设计指令集的最重要的目标应该是平*立性,同时在验证bytecode也比较方便。有些指令的操作都是基于具体类型的,有的就没有比如 goto,如图
指令前缀表示操作类型, 可见Java编译器和JVM对类型操作要求很严格,为何使指令尽量用一个字节表示,很多类型如 byte, char 都没有直接运算,而是在运算前把他们转换成整型。
执行过程的在某一时刻内容如图:
在帧里的局部变量有四个分别都是 reference,都指向不同的对象,有的时候我们编程当操作完成,最好把 o, greeter ,c, gcl 这四个参考付为 null,这样他们指向的对象不可达,对象不可达,他们在方法区的类信息也不可达,类信息不可达,堆中的 Class 对象不可达,你可以看到图中的所有对象,类信息都是可回收状态,这样GC某个时刻就可以释放了这些内存了。
写方法时我们希望 try cache 起来,当然也有需要在后面 加上 finally,我们都知道在finally 里的代码被执行玩前所有的 return 操作都会压入栈里,等finally块执行完了再弹出返回,如:
static boolean test(boolean bVal) {
while (bVal) {
try {
return true;
}
finally {
break;
}
}
return false;
}
可以看出这个方法的执行以返回 false 告终。
4 类文件结构
读者可以用 UltrEdit 之类的编辑工具打开一个 class 文件查看,每个类文件都包含如下内容如图:其中 u2 表示两个字节大小,u4表示四个字节大小。
如第一行所示,每个类文件都以幻数开头固定为(0xCAFEBABE),用UltraEdit 等编辑工具可以直接看到,当然你也可以看到很多如图所示的属性和方法的描述:
常量池是以类型来划分不同的表格的,如CONSTANT_Double_info 表就只是double 常量值,CONSTANT_Class_info表存储参考信息,比如类,接口,属性和方法的符号参考,类文件的核心数据结构就是常量池了,而所有的类,属性和方法的描述以及值都是通过 index 到常量池获得的,所以常量池是个很重要的概念,有兴趣的朋友可进一步参考JVM规范。
5 线程同步
对多线程的支持是java 语言的一大亮点,java 实现线程同步的方式是monitor,具体有两种方式:互斥和协同,每个对象都有一把锁,通过 lock 和 unlock 实现互斥的操作,Object类有 wait notify 等这样的方法可以实现线程为实现一个共同的目标而协同工作。monitor就像一个建筑有一个特殊的房间每次只允许一个线程访问。都知道线程同步有一个重要的概念就是临界区,任何一个线程要想进入临界区就必须获得monitor,如果线程发现monitor被其他线程获得,此线程必须排队,等到他前面的线程也退出了 monitor 的占有,则此线程才能操作 monitor 的临界区,当然也有优先级 的问题。此图说明了多个线程协作工作的过程。
前面介绍过,堆和方法区是被线程共享的,这两大块要做同步,而对于局部变量是由线程独有的所以不存在同步问题,Java API 中有一个很重要也很简单的类就是 ThreadLocal,他是用 Theard key 来标识变量,是非局部变量局部化的一个很好的例子。
对象锁的概念大家都很熟悉了,Java中通过这个锁有两种方式,一种是同步块,一种是同步方法,具体的内容请看看java 多线程编程的书。
6 垃圾回收机制
堆里聚集了所有由应用程序创建的对象,JVM也有对应的指令比如 new, newarray, anewarray和multianewarray,然并没有向 C++ 的 delete,free 等释放空间的指令,Java的所有释放都由 GC 来做,GC除了做回收内存之外,另外一个重要的工作就是内存的压缩,这个在其他的语言中也有类似的实现,相比 C++ 不仅好用,而且增加了安全性,当然她也有弊端,比如性能这个大问题。
垃圾回收的算法有很多,不过怎么样,任何一个算法都要做两件事情,一是识别垃圾对象,二是清理内存以备运行程序使用。区分活着的对象和垃圾对象的主要方法就是参考计数和追踪,参考计数就是每个对象都维护一个引用 count,而每次追踪都去标记对象,当一次跟踪完成没有被标记的对象就是不可到达的那就是垃圾了。参考计数的方式现在已经很少JVM实现用了,因为对于 child, parent 的内部参考,count 永远不会是0,也就是不会是垃圾对象,而且每次修改引用都要加减 count。而跟踪是从根节点开始扫描所有对象,并作标记。知道那些对象是垃圾了之后,就要释放了,有两种方式一个是压缩,一个是拷贝,不管哪种方式改变了对象的物理位置,就要修改他的参考值,每个对像都有一个 finalize 方法,一般我们不去 override 他,他是在回收时期执行,但并不是所有的对象的 finalize 方法都会被执行,具体内容请看 《inside in java machine》.
7 总结
本文结合作者的一些观点和几年的开发实践,谈谈对JVM规范的看法,由于完稿仓促,也没多少时间整理,如果有什么问题可以进一步讨论,本人编译器这方面也是比较薄弱,真正按照JVM规范实现虚拟机还有很长的路要走,可能在将来的某天有机会进一步研究一下。多谢各位的指导和帮助,我会陆续在Blog里发些文章。
本文转自BlogJava 新浪blog的博客,原文链接:JVM 学习笔记,如需转载请自行联系原博主。