`
solidsnake2007
  • 浏览: 28859 次
  • 性别: Icon_minigender_1
  • 来自: 西安
社区版块
存档分类
最新评论

JVM原理结构详解

阅读更多

 

图1 Java虚拟机运行时数据区

 

 

        做Java就是和内存打交道,如果在这条道路上有更加深入的发展,就必须了解JVM的结构和生命周期。如图所示,图中是一个完整的JVM结构。下面,会列出JVM各个区块的分工。

 

    1. Java栈

        Java栈(JVM Stack),通常我们把内存分为堆和栈,这是一种比较粗糙的划分方法,但确实,我们最关心的也就是堆和栈两个主要的内存区块,实际上JVM中的内存分布远比它复杂。

        栈,再Java中用来保存8个基本类型数据,也就是局部变量表,以及对象的引用(reference类型,它并不直接代表对象本身。根据不同的JVM虚拟机实现,它可能是一个指向对象起始的内存地址、也可能是代表一个对象的句柄、或者指向一条字节码指令地址)。

        其中只有double 和 long 类型(它们是64位的数据长度)数据会占据两个局部变量位置,其他的均占一个局部变量位置,并且局部变量所需的内存空间在编译期就已经确定了。直到方法结束局部变量表的大小都不会改变,所以虚拟机会在进入方法的一开始就直到需要在栈帧中分配多大的内存空间给局部变量表。

        上面提到了栈帧,这里说明一下,栈的生命周期和线程的生命周期相同。虚拟机栈所表述的是Java方法的内存模型。每个方法被执行的时候都会在栈中创建一个栈帧来保存方法中的局部变量表、操作栈、动态链接、方法出口等信息。所以每一个方法被执行到方法的结束, 都对应着一个栈帧在Java栈中压栈出栈的过程。

 

    2. 本地方法栈

        本地方法栈(Native Method Stack), 本地方法栈, 顾名思义,它是用来调用本地方法的。 它的机制与Java栈(JVM Stack)非常类似。所以有些虚拟机(sun HotSpot)甚至直接将它们两个合起来用。

 

    3. 程序计数器

        这是一块很小的内存区域,主要作用是记录当前线程所执行的字节码的行号。字节码解释器工作时就是通过改变当前线程的程序计数器选取下一条字节码指令来工作的。任何分支,循环,方法调用,判断,异常处理,线程等待以及恢复线程,递归等等都是通过这个计数器来完成的。

        由于Java多线程是通过交替线程轮流切换并分配处理器时间的方式来实现的,在任何一个确定的时间里,在处理器的一个内核(现在都是多核处理器啊!)只会执行一条线程中的指令。因此为了线程等待结束需要恢复到正确的位置执行,每条线程都会有一个独立的程序计数器来记录当前指令的行号。计数器之间相互独立互不影响,我们称这块内存为“线程私有”的内存。

        还有一点要提一下,如果在执行一个方法的时候调用另一个Java方法,那么这个计数器指向的是Java方法字节码中的指令行号,如果是一个native方法,那么这个计数器的值为Undefined。

 

    4. Java堆

        Java的堆是JVM内存中最大的一块,它的主要的作用就是用来存放对象,并且这块区域是所有线程共享的。所有的对象,数组都必须在堆中创建(除了现在个别特例JIT实现 【JIT就是用来把java字节码变成可以直接给CPU处理器执行的指令的即时编译器】 可以支持栈上分配)。 同时GC也发生在堆中。由于现在的GC回收策略基本上都是使用代分回收算法, 所以堆中也同时存在多个区域, 例如Eden、Form Survivor、To Survivor、Old Space 、premanent Space等这些都是下一部分的内容,现在可以粗略的了解一下。

        值得一提的是,Java堆中的数据在物理上市不连续的,只要逻辑连续即可。但这造成了一个问题, 比如内存中本来数据是相对连续的,有些对象已经不再使用,刚好被GC清理。这时,内存中就会出现如断牙的梳子一般,如果这时你又创建了一个较大对象时(例如2维,3维长数组),虽然剩余的总堆空间确实足够分配如此大的一个对象,却因为不连续性(类似磁盘碎片)而无法在堆中分配出足够大的内存空间因此报出 OutOfMemoryError。幸运的是,GC还有一种清理方式会把当前堆中的内存进行排序。两种方式交替作用下,可以有效的避免这类问题。(关于GC的两种清理方式,参阅Thinking in Java)

 

    5. 方法区

        方法区(Method Area)与JAVA堆类似,但是其中存放的是常量,静态方法,类信息,静态常量以及上述提到的JIT(即时编译器)编译后的代码等数据信息的。一般情况下,方法区的内存空间是相对固定的, 除非在初始化的时候由于内存过小而导致无法满足内存分配需求的时候会报出 OutOfMemoryError。

        其中需要主要说明的一点是,运行时常量也保存在方法区中,如内设的int值,String常量池等等内容。

 

    JVM生命周期概述

        由于我还是个鸟,所以堆JVM生命周期的理解并不是那么详细和到位,所以,下文中的内容如有异议及补充请留言回复。

        JVM运行时首先会执行ClassLoader的最上层的BootStrap ClassLoader加载系统类所必须的类(如String,int 等等),由于BootStrap是由C语言编写的所以我们无法再Java中得到它。加载完毕后执行程序main方法,在这个个过程中,会首先将系统所必须的类信息加载入方法区中,并定义String缓冲池int内设值以及涉及到的常量和静态方法。

        当遇到需要加载的类时,JVM会再次执行ClassLoader,由于ClassLoader是一个Parent结构,ClassLoader会从最下层的

System ClassLoader(通常我们使用的Class.forName(...)等)向上递归搜索至Extension ClassLoader, 同理Extension ClassLoader向上搜索到 BootStrap ClassLoader , BootStrap会检索自己的加载列表中有没有这个类,如果有则返回这个类的信息,如果没有则会在自己的加载范围内搜索该类并把它加载成Java字节码放入对应的位置,这时,如果是一个用户自定义的类时,由于BootStrap的加载范围不包括用户自定义类,所以结束上层递归由Extension ClassLoader检索自己加载链表,如果没有则同理向下结束中层递归由System ClassLoader来加载, 加载过程同理为先检索后在当前项目ClassPath中查找要加载的自定义类(ClassLoader的机制可以自行去了解 如:http://dlevin.iteye.com/blog/772604)。 当类加载完毕后,JVM会将这个类信息保存到方法区内,并将对应的常量、静态常量方法等信息保存进方法区中。

        当程序执行到需要new出一个对象的时候, JVM首先会在栈中创建一个该对象类型的引用, 而后在堆中创建出类的一个对象, 最后将这个对象和栈中对象的引用关联起来, 这里可能是一个内存地址,也可以使一个句柄。 而后,对象执行方法的过程中,栈顶压入一个栈帧,同时程序计数器会记录当前线程中执行的指令行数,并做对应的跳转(如果是内联方法可能不需要跳转)。最后,方法结束,栈帧出栈,并清理栈中的局部变量表以及对象的引用等信息。但对于堆并不急于清理, 因为在编译时编译器并不知道堆中的对象的生命周期。

        程序运行了一段时间,堆中的数据将要满时(当需要创建一个对象却发现剩下的内存空间并不能分配足够大的空间时)Java垃圾清理系统(GC)一定会执行,清理掉没有被任何地方引用的对象,释放堆中的内存空间。 这里会出现类似于磁盘碎片的现象(所以我们在写代码的时候尽量不要创建庞大的对象,而是将他作为静态方法放在基本不变的方法区中),一种坏的情况是,随着程序的运行, 不可被回收或者内存不连续序列越来越紧密,最终会导致OutOfMemoryError。不过如果代码优化的够好,直到程序结束,这个现象都不会发生。

        当程序结束后,JVM会一次性释放全部当前Java进程中全部内存空间。整个JVM生命周期在此结束了。

 

-------------------------------

        作为补充内容,下次的内容为上述的代分垃圾回收算法介绍。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics