Jack.Wang(本文未完,待续…..)
摘要:JVM 作为 Java 的核心技术,很多朋友想必也有研究。一直都在关注 JVM 方面的技术,以前看过一些书籍和网上的资料,自己也发了些 Blog 文章,不过还是没有彻底的了解 JVM 机制,最近有时间研究了研究,特此写下一篇文章并结合笔者多年实践以揭露 JVM 实现机理,本文后面提供资料下载,读者可进一步研究。
关键字: JVM,Java 技术,虚拟机,Java 架构
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 数据类型
虚拟机的数据类型分为:原始类型和参考类型,有人一直说Java只有90%是OO的,其中原始类型就算的上,不过还好有对应的封装类型如 int->Integer,JDK1.5可以用这些封装类型直接做算术运算,不过Java内部要做拆箱和装箱的工作。似乎 OO了些,不过现代的程序员才不过你这些,能用就行。
字长是数据值得基本单元,一般像 int, byte, short 等类型的值用一个字长存储,而浮现类型用两个字长存储,字长的大小一般和机器的指针大小相同,最小是32 位。
像byte, short, or char 这样的类型在方法区和堆中Java是原来类型,但在程序运行时放在栈帧中,统一用整型(int)来表示
3.4 方法区
ClassLoader负责把class加载进来并解析类信息放在方法区,方法区是被所有线程共享的,所以方法区必须保证线程的安全。比如两个线程同时需要一个类,必须保证只有一个线程去加载而另一个线程等待。
方法区的大小不是固定的,一般会在运行时JVM会根据运行情况作出调整,但JVM实现者都留有接口可供调节,比如最大和最小值。
我们都知道堆是被垃圾回收器回收的,在方法区的类也是会被回收的,类也有他的生命周期,当某个类不再被参考之后也就没有这个类的引用和对象了那么他的存在就没有意义了,只是站着内存,回收器就会按照一定的算法将它卸载了。对于一个类信息的表述都是用全名的,在方法区中也有其他信息,比如常量池,属性和方法信息,还有就是指向堆中的 ClassLoader和Class 的参考。我大家都熟悉的就是类的静态变量,也放在方法区的由所有的对象实例共享。
可以想象在方法区内放置了大量的二进制的Class信息,为了加快访问速度,JVM实现者都会维护一个方法表,记得读研一的时候中间件老师讲过这些东西是结合指针讲的C++ 的内存模型。另外注意的就是方法表只针对可实例化的类,对抽象类和接口没有意义。
每个JVM实例都只有一个堆,所有的线程共享其中的对象,这才出现了多线程安全问题。JVM有new 对象的指令但没有释放对象的指令,当让这些指令都是虚拟指令,这些对象的释放是有 GC 来做的,GC在JVM规范中并没有硬性的规定,有实现者设计他的实现形式和算法。
想必很多同人都想知道,对象是怎么样在堆里表示的,其实很简单。其实面JVM规范也没有细致的规定对象怎么在堆里表示的,
如图是一个参考的堆模型,具体实现可能不是这样的,这个是HeapOfFish applet 的一个演示模型,具体内容可以看看JVM规范。当然也有很多其他的模型,这个模型的好处就是在堆压缩的时候很方便,而在 reference 直接 point到一个对象的模型来说在堆压缩方面是很麻烦的,因为你要考虑到方法区,堆,栈里可能的参考,你都要修改。对象还有一个很重要的数据结构就是方法表,方法表可以加快访问速度,但并不是说所有的JVM实现都有。
3.5 操作数栈
操作数栈是Java运行时的核心栈,看看 i+j 的一个简单运算,
iload_0
iload_1
iadd
istore_2
以上是四个JVM指令,完成 i+j并把结果保存到k中,如图示:
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 都没有直接运算,而是在运算前把他们转换成整型。
4 总结
本文结合作者的一些观点和几年的开发实践,谈谈对JVM规范的看法,由于完稿仓促,也没多少时间整理,如果有什么问题可以进一步讨论,本人编译器这方面也是比较薄弱,真正按照JVM规范实现虚拟机还有很长的路要走,可能在将来的某天有机会进一步研究一下。多谢各位的指导和帮助,我会陆续在Blog里发些文章。
资料下载:jvm学习笔记.doc
jvm调优.pdf
5 参考资料
1)inside in java machine (想要的请点击下载)
2)http://www.blogjava.net/Jack2007/archive/2008/05/23/202485.html《Java虚拟机深入研究》
3)http://www.blogjava.net/Jack2007/archive/2008/05/21/202018.html
4)http://www.tudou.com/programs/view/hN_4sQJMoFQ/
5)http://www.blogjava.net/Jack2007/archive/2008/04/11/192288.html
6)http://developer.违规广告.com/art/200805/73338.htm
MSN: wbjeasygo@163.com
Email: wbjeasygo@163.com
QQ 群:47763528
原文链接:http://www.blogjava.net/Jack2007/archive/2008/08/27/225052.html
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/14734416/viewspace-437318/ ,如需转载,请注明出处,否则将追究法律责任。