JVM 学习笔记
Jack.Wang(本文未完,待续…..)
摘要:JVM 作为 Java 的核心技术,很多朋友想必也有研究。一直都在关注 JVM 方面的技术,以前看过一些书籍和网上的资料,自己也发了些 Blog 文章,不过还是没有彻底的了解 JVM 机制,最近有时间研究了研究,特此写下一篇文章并结合笔者多年实践以揭露 JVM 实现机理,本文后面提供资料下载,读者可进一步研究。
关键字: JVM,Java 技术,虚拟机,Java 架构
目录
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!"
|
|