Java虚拟机学习入门

时间:2022-01-15 10:01:17

1      前言

想深入了解Java,虚拟机是必须掌握的技能,任何一个Java程序都离不开虚拟机,对于初学者了解JVM也可以更好的理解Java的初始化、内存使用等知识点。总结了一下自己在学习虚拟机过程中的一些知识点,整理了一下笔记,有不妥和不当之处欢迎Java爱好者批评指正,也欢迎各位来自五湖四海的朋友交流任何问题。

Java虚拟机(Java Virtual Machine) 简称JVM,Java虚拟机是一个想象的虚拟机器,通过在实际计算机上的软件模拟来实现,一个Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。Java程序通过这个虚拟的机器,加载代码,管理代码存放位置,执行代码。换句话说,JVM=类加载器 (classloader)+ 执行引擎(execution engine )+ 运行时数据区域 (runtime data areaclassloader)。我个人的理解,JVM的核心部分就是代码存储在哪里以及代码是如何运行的,为了更好的理解JVM的运行机制,首先看看JVM的内存管理

 

2      Java虚拟机内存管理

大多数的Java虚拟机把虚拟机的内存分为程序计数器(Program CounterRegister)、堆区(Heap)、虚拟机栈(VMStack)、本地方法栈(Native Method Stack)、方法区(Method Area)、运行时常量池(Runtime Constant Pool)等,如下表。

表 1JVM内存分配

Java虚拟机学习入门

2.1      程序计数器

程序计数器作用可以看作当前线程所执行的字节码行号的指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器来完成。

程序计数器是一种寄存器,在计算机世界里寄存器作为cpu的重要组成部分,用来暂存指令、数据和地址等信息。是内存层次结构中的最顶端,也是系统操作数据的最快途径,基本单元为触发器。

除了程序计数器,JVM还设置了另外3个常用的寄存器。它们是:optop操作数栈顶指针 ,frame当前执行环境指针, vars指向当前执行环境中第一个局部变量的指针, 所有寄存器均为32位。pc用于记录程序的执行。optop,frame和vars用于记录指向Java栈区的指针。

该区域也是虚拟机内存中唯一一块没有规定任何异常的区域。

2.2     Java虚拟机栈

栈内存属于单个线程,每个线程都会有一个栈内存,即栈内存可以理解为线程的私有内存。

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接和方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。具体来讲就是当JVM得到一个Java字节码应用程序后,便为该代码中一个类的每一个方法创建一个栈框架,以保存该方法的状态信息。每个栈框架包括以下三类信息:局部变量执行环境操作数栈,局部变量用于存储一个类的方法中所用到的局部变量。vars寄存器指向该变量表中的第一个局部变量。执行环境用于保存解释器对Java字节码进行解释过程中所需的信息。它们是:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。执行环境是一个执行一个方法的控制中心。例如:如果解释器要执行iadd(整数加法),首先要从frame寄存器中找到当前执行环境,而后便从执行环境中找到操作数栈,从栈顶弹出两个整数进行加法运算,最后将结果压入栈顶。操作数栈用于存储运算所需操作数及运算的结果。

栈内存没有可用的空间存储方法调用和局部变量,JVM抛出Java.lang.*Error

-Xss设置栈内存大小,栈内存远远小于堆内存,如果使用递归的话,不及时跳出很容易发生*Error问题。

栈内存存放基本类型的变量数据和对象的引用、局部变量,存取方式仅次于寄存器,String a=”abc”,是个例外,存放在栈中如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。 

2.3     本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似,虚拟机栈为虚拟机执行的Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。有的虚拟机(如sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

本地方法栈与虚拟机栈一样,也会抛出*Error和OutOfMemoryError

2.4     Java

Java堆即JVM碎片回收堆,线程共享的,在虚拟机启动时创建。此区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是立即收集器管理的主要区域,因此也就“GC堆”

Java类的实例所需的存储空间是在堆上分配的。解释器具体承担为类实例分配空间的工作。解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用的内存区域的使用。一旦对象使用完毕,便将其回收到堆中。在Java语言中,除了new语句外没有其他方法为一对象申请和释放内存。对内存进行释放和回收的工作是由Java运行系统承担的。这允许Java运行系统的设计者自己决定碎片回收的方法。在SUN公司开发的Java解释器和Hot Java环境中,碎片回收用后台线程的方式来执行。这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险。

堆内存没有可用的空间存储生成对象,JVM会抛出Java.lang.OutOfMemoryError

—Xms设置堆的初始大小 —设置堆的最大值

如果再进一步细分堆分为新生代和老年代,更多详细内容这里不再赘述。

2.5     方法区

 

方法区与Java堆一样是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做Non-Heap(非堆)。同样,根据Java虚拟机规范,当此区域无法满足内存分配时,抛出OutOfMemoryError

2.6     运行时常量缓冲池(runtime constant pool 

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生产的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池(Runtime ConstantPool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)

2.7     直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这一部分也被频繁使用,也可能导致OutOfMemoryError异常出现。例如,JDK1.4中新加入的NIO类,引入了一种基于通道与缓冲区的I/O方式,使用的就是堆外内存,但既然是内存,肯定会受到本机总内存的大小及处理器寻址空间的限制。

3      类和对象的生命周期

3.1     类的生命周期

Java虚拟机学习入门 

图1 类的生命周期

3.1.1     加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个Java.lang.Class对象,用来封装类在方法区内的数据结构 。这里的class对象其实就像一面镜子一样,外面是类的源程序,里面是class对象,它实时的反应了类的数据结构和信息。把硬盘上的class 文件加载到JVM中的运行时数据区域, 但是它不负责这个类文件能否执行,而这个是 执行引擎负责的

 

不同的JVM对于类的装载时机并不相同,有些在遇到这个类时就装载这个类(虽然并不知道这个类是否会被用到),另一些则在真正用到一个类的时候才对它进行装载。

表 2类的加载

Java虚拟机学习入门

3.1.2     连接

(1)验证

验证是连接阶段的第一步,其目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全,如果验证失败,会抛出Java.lang.VerifyError异常。

表 3验证主要工作

Java虚拟机学习入门

 

(2)准备

准备阶段:为类的静态变量分配内存并设为JVM默认的初值,对于非静态变量则不会分配内存。基本类型默认值为0,引用类型默认值为null,常量类型默认值为程序中设定值,这些内存都将在方法区中进行分配。

对于普通非final的类变量,如public static int value = 123;在准备阶段过后的初始值是0(数据类型的零值),而不是123,而把123赋值给value是在初始化阶段才进行的动作。

对于final的类变量,即常量,如public staticfinal int value =123;在准备阶段过程的初始值直接就是123了,不需要准备为零值。

(3)解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用(SymbolicReference):以一组符号来描述所引用的目标,与虚拟机内存布局无关,引用的目标不一定已经被加载到虚拟机内存中。

直接引用(DirectReference):可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译处理的直接引用不一定相同,如果有了直接引用,则引用的目标对象必须已经被加载到虚拟机内存中。

解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行解析。

 

3.1.3     初始化

初始化是类使用前的最后一个阶段,在初始化阶段Java虚拟机真正开始执行类中定义的Java程序代码。初始化:只会初始化与类相关的静态赋值语句和静态语句,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。如果一个类被直接引用,就会触发类的初始化。

表 4主动引用和被动引用

Java虚拟机学习入门

 

如果一个类被直接引用,而对象没有初始化时,就会触发类的初始化

 

 

初始化的过程其实就是一个执行类构造器<clint>方法的过程,类构造器执行的特点和注意事项:

(1)类构造器<clint>方法是由编译器自动收集类中所有类变量(静态非final变量)赋值动作和静态初始化块(static{……})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定。静态初始化块中只能访问到定义在它之前的类变量,定义在它之后的类变量,在前面的静态初始化中可以赋值,但是不能访问。

(2)类构造器<clint>方法与实例构造器<init>方法不同,它不需要显式地调用父类构造器方法,虚拟机会保证在调用子类构造器方法之前,父类的构造器<clinit>方法已经执行完毕。

(3)由于父类构造器<clint>方法先与子类构造器执行,因此父类中定义的静态初始化块要先于子类的类变量赋值操作。

(4) 类构造器<clint>方法对于类和接口并不是必须的,如果一个类中没有静态初始化块,也没有类变量赋值操作,则编译器可以不为该类生成类构造器<clint>方法。

(5)接口中不能使用静态初始化块,但可以有类变量赋值操作,因此接口与类一样都可以生成类构造器<clint>方法。接口与类不同的是:

首先,执行接口的类构造器<clint>方法时不需要先执行父接口的类构造器<clint>方法,只有当父接口中定义的静态变量被使用时,父接口才会被初始化。

其次,接口的实现类在初始化时同样不会执行接口的类构造器<clint>方法。

(6)Java虚拟机会保证一个类的<clint>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,只会有一个线程去执行这个类的<clint>方法,其他线程都需要阻塞等待,直到活动线程执行<clint>方法完毕。

初始化阶段,当执行完类构造器<clint>方法之后,才会执行实例构造器的<init>方法,实例构造方法同样是按照先父类,后子类,先成员变量,后实例构造方法的顺序执行。

JVM在类初始化完成后,根据类的信息在堆区实例化类对象,初始化非静态变量和默认构造方法。

说到这里,上一节的问题应该可以解决了吧,父类的非静态成员变量在对象实例化的时候进行赋值。

3.1.4     使用

当初始化完成之后,Java虚拟机就可以执行Class的业务逻辑指令,通过堆Java.lang.Class对象的入口地址,调用方法区的方法逻辑,最后将方法的运算结果通过方法返回地址存放到方法区或堆中。使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。

 

3.1.5     卸载

当对象不再被使用时,Java虚拟机的垃圾收集器将会回收堆中的对象,方法区中不再被使用的Class也要被卸载,否则方法区(Sun HotSpot永久代)会内存溢出。

Java虚拟机规定只有当加载该类型的类加载器实例为unreachable状态时,当前被加载的类型才被卸载.启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载,类型卸载仅仅是作为一种减少内存使用的性能优化措施存在的,具体和虚拟机实现有关,对开发者来说是透明的.
如果下面的所有情况都成立,类将会被卸载:
(1)类所有的实例都已经被回收。(即堆中不存在该类的任何实例)
(2)加载该类的ClassLoader被回收。
(3)该类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM在方法区垃圾回收的时候对类进行卸载,在方法区中清空类信息。
至此,一个Java类的生命周期结束。

 

 

 

3.2     对象的生命周期

对象的生命周期只是类的生命周期中使用阶段主动引用的一种情况(实例化对象),Java对象是在JVM的堆区创建的,在创建对象之前,可能会触发类的加载、连接和初始化。
由于Java在堆上创建对象,因此编译器对对象的生命周期一无所知。Java提供了垃圾回收器机制,JVM会在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。 

表 5对象的生命周期

Java虚拟机学习入门

 

3.3     执行引擎

执行引擎是Java虚拟机最核心的组成部分之一,“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面的,而虚拟机的执行引擎是自己实现的。在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能有解释执行(通过解释器执行)和编译执行(通过即时编译器产生的本地代码执行)两种选择,但从外观看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

JVM是为Java字节码定义的一种独立于具体平台的规格描述,是Java平*立性的基础。目前的JVM还存在一些限制和不足,有待于进一步的完善,但无论如何,JVM的思想是成功的。对比分析:如果把Java原程序想象成我们的C++原程序,Java原程序编译后生成的字节码就相当于C++原程序编译后的80x86的机器码(二进制程序文件),JVM虚拟机相当于80x86计算机系统,Java解释器相当于80x86CPU。在80x86CPU上运行的是机器码,在Java解释器上运行的是Java字节码。  Java解释器相当于运行Java字节码的“CPU”,但该“CPU”不是通过硬件实现的,而是用软件实现的。Java解释器实际上就是特定的平台下的一个应用程序。只要实现了特定平台下的解释器程序,Java字节码就能通过解释器程序在该平台下运行,这是Java跨平台的根本。当前,并不是在所有的平台下都有相应Java解释器程序,这也是Java并不能在所有的平台下都能运行的原因,它只能在已实现了Java解释器程序的平台下运行。

 

小结:

1.类的成员变量在不同对象中,都有自己的存储空间,在堆内存中的类的实例中;类的方法却是该类的所有对象共享的,存在与方法区的字节码,不使用则不占内存;方法的调用是通过堆内存中的方法入口访问方法区的方法字节码。

2.线程私有的内存区域随着线性的终结内存释放,没有自动垃圾回收,而线程共享的区域则是垃圾回收的区域。

3.常量在编译期放入方法区的类的常量池中,静态成员变量在初始化的时候赋值,非静态成员变量在对象实例化的时候赋值。

 

 

 

 

 

http://blog.csdn.net/u010233323/article/details/51354331

https://segmentfault.com/a/1190000004206269

http://blog.csdn.net/yfqnihao/article/details/8289363

http://www.cnblogs.com/smyhvae/p/4810168.html

http://blog.csdn.net/zhengzhb/article/details/7517213(类生命周期)

http://blog.csdn.net/sodino/article/details/38387049(对象生命周期)

http://chengjianxiaoxue.iteye.com/blog/2153147