您的位置:首页 > 编程语言 > Java开发

JVM 学习笔记

2012-12-18 19:11 141 查看
一、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++ 中的指针,参考不向指针那样允许参与计算,避免了开发人员直接操作内存,还有个垃圾回收机制也避免了开发者手动释放内存,还有就是 C++ 中的数组是不进行边界检查的而Java中每次使用数组的时候都要进行边界检查,岂不安全。
可见Java相比C++ 提高了开发效率和安全性。Java和C++ 比运行速度是个大问题,因此任何语言都不万能的,在开发是我们应该适当权衡。

注:此处“参考”这个词,其实就是我们常说的“引用”。

二、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 类的相关方法。

三、JVM 内部机理

1. JVM的生命周期

任何一个类的 main 函数运行都会创建一个JVM实例,当main 函数结束JVM实例也就结束了,如果三个类分别运行各自的 main 函数则会创建3个不同的JVM实例。JVM实例启动时默认启动几个守护线程,而 main 方法的执行是在一个单独的非守护线程中执行的。只要母线程结束,子线程就自动销毁,只要非守护main 线程结束JVM实例就销毁了。

2. JVM的框架





JVM主要由 ClassLoader 子系统和执行引擎子系统组成,运行数据区分为五个部分,他们是方法区,堆,栈,指令寄存器,本地方法栈。方法区和堆是所有线程共享的,一般我们习惯将临时变量放在寄存器中,但JVM中不用寄存器而是用栈,每个线程都有自己独立的栈空间和指令寄存器,其中栈里的元素叫帧,帧有三部分组成分别是局部变量,操作数栈和帧数据。正式因为栈是每个线程独有的,所以对于
local 变量,是没有多线程冲突问题的。栈和帧的数据结构和大小规范里没有硬性规定,由实现者具体实做。

3. 数据类型





虚拟机的数据类型分为:原始类型和参考类型,有人一直说Java只有90%是OO的,其中原始类型就算不上,不过还好有对应的封装类型如 int->Integer,JDK1.5可以用这些封装类型直接做算术运算,不过Java内部要做拆箱和装箱的工作。似乎 OO了些,不过现代的程序员才不管你这些,能用就行。

字长是数据值得基本单元,一般像 int, byte, short 等类型的值用一个字长存储,而浮点类型用两个字长存储,字长的大小一般和机器的指针大小相同,最小是32 位。

在Java中像byte, short, or char 这样的类型在方法区和堆中是原来类型,但在程序运行时放在栈帧中,统一用整型(int)来表示。

4. 方法区

ClassLoader负责把class加载进来并解析类信息放在方法区,方法区是被所有线程共享的,所以方法区必须保证线程的安全。比如两个线程同时需要一个类,必须保证只有一个线程去加载而另一个线程等待。

方法区的大小不是固定的,一般会在运行时JVM会根据运行情况作出调整,但JVM实现者都留有接口可供调节,比如最大和最小值。

我们都知道堆是被垃圾回收器回收的,在方法区的类也是会被回收的,类也有他的生命周期,当某个类不再被参考之后也就没有这个类的引用和对象了那么他的存在就没有意义了,只是占着内存,回收器就会按照一定的算法将它卸载了。对于一个类信息的表述都是用全名的,在方法区中也有其他信息,比如常量池,属性和方法信息,还有就是指向堆中的
ClassLoader和Class 的参考。大家都熟悉的就是类的静态变量,也放在方法区的由所有的对象实例共享。

可以想象在方法区内放置了大量的二进制的Class信息,为了加快访问速度,JVM实现者都会维护一个方法表,另外注意的就是方法表只针对可实例化的类,对抽象类和接口没有意义。

每个JVM实例都只有一个堆,所有的线程共享其中的对象,这才出现了多线程安全问题。JVM有new 对象的指令但没有释放对象的指令,当让这些指令都是虚拟指令,这些对象的释放是有 GC 来做的,GC在JVM规范中并没有硬性的规定,有实现者设计他的实现形式和算法。

想必很多同人都想知道,对象是怎么样在堆里表示的,其实很简单。其实JVM规范也没有细致的规定对象怎么在堆里表示的,





如图是一个参考的堆模型,具体实现可能不是这样的,这个是 HeapOfFish applet 的一个演示模型,具体内容可以看看JVM规范。当然也有很多其他的模型,这个模型的好处就是在堆压缩的时候很方便,而在 reference 直接 point到一个对象的模型来说在堆压缩方面是很麻烦的,因为你要考虑到方法区,堆,栈里可能的参考,你都要修改。堆还有一个很重要的数据结构就是方法表,方法表可以加快访问速度,但并不是说所有的JVM实现都有。

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 都没有直接运算,而是在运算前把他们转换成整型。

原文链接:http://my.oschina.net/xianggao/blog/86962
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  JAVA JVM