java虚拟机之内存管理

     俗话说,不想当将军的士兵不是好士兵,作为一个有追求的java程序猿,终极目标必然是系统架构师(捂脸)~而要想成为一名合格的系统架构师,一定要对JVM有深入的研究与理解。建议对JVM感兴趣的朋友可以读一读周志明编写的《深入理解java虚拟机》一书,本文大多内容即出自对该书的摘抄与整理。

     1995年5月23日,SunWorld大会上正式发布了java1.0的版本,java语言第一次提出“Write Once,Run Anywhere”的口号。如今,距离java语言第一次发布已经将近二十年的时间,java也不断地在发展,每次升级都伴随着新特性的发布。一般来说,我们把java程序设计语言、java虚拟机、java API类库这三部分统称为JDK(Java Development Kit)。JDK是支持java程序开发的最小环境,另外,可以把Java API类库中的Java SE API子集和Java虚拟机两部分统称为JRE(Java Runtime Environment),JRE是支持Java程序运行的标准环境。而JDK除了SUN公司发布的标准JDK之外,还有一些开源的JDK,比如Sun在2006年把Java源码开放而形成的OpenJDK。

     我们都知道,使用Java语言不用每次new一个对象之后去写配对的delete/free代码,不容易造成内存泄露和内存溢出问题,因为内存都由Java虚拟机来管理,JVM自带的垃圾回收机制会定期按照一定的算法(后面会谈到)回收不再使用的对象并释放对应的内存。Java虚拟机在执行Java程序的过程中会把管理的内存区域划分为若干个不同的数据区域,1.程序计数器 2.java虚拟机栈 3.本地方法栈 4.java堆 5.方法区。如下图所示:

                                                                      

                                            

     顺便提一下java的类加载机制,当我们在命令行执行java  xxx.class命令的时候,java.exe首先找到JRE,然后找到位于JRE之中的JVM.dll(真正的JAVA虚拟机),最后加载这个动态联结函式库,启动java虚拟机。虚拟机启动后首先进行初始化,初始化完成后生成第一个类加载器BootstrapLoader。BootstrapLoader是由C++编写而成的,所以以java的观点来看逻辑上并不存在BootstrapLoader的类别实体,因此在试图通过getClass().getClassLoader()来打印时会打印出null。BootstrapLoader生成后会载入定义在sun.misc命名空间下的Launcher.java之中的ExtClassLoader。ExtClassLoader是一个内部类,编译之后会变成Launcher$ExtClassLoader.class,它的父加载器为BootstrapLoader。然后BootstrapLoader再加载Launcher.java之中的AppClassLoader(和ExtClassLoader一样也是一个内部类),它的parent为ExtClassLoader实体。这里ExtClassLoader与AppClassLoader都是由BootstrapLoader加载的,因此parent是谁和由哪个类别加载器加载并没有关系。他们的关系如下图所示:

                                     

最后由AppclassLoader加载程序员编译之后的.class文件。

    下面回来继续看虚拟机所管理的几个运行时数据区域:

1.程序计数器

    程序计数器(Program Counter Register)是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,因此为了线程切换后能回到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响独立存储,因此程序计数器这部分内存区域为线程私有的内存。该内存区域也是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

扫描二维码关注公众号,回复: 3196684 查看本文章

2. Java虚拟机栈

    Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型,它所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    Java虚拟机规范中,对Java虚拟机栈规定了两种异常情况:当线程请求的栈深度大于虚拟机所允许的深度时将抛出StackOverFlowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3. 本地方法栈

    本地方法栈(Native Method Stack)与虚拟机栈的作用非常相似,不同的是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。

4. Java堆

    对大多数应用程序来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域主要用来存放对象实例,几乎所有的对象实例都在这里分配内存。

    Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆。由于现在的垃圾收集器基本都采用分代收集算法,所以Java堆还可以细分为新生代和老年代,而新生代又可以分为Eden空间、From Survivor空间和To Survivor空间。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,可以通过-Xmx和-Xms控制Java堆的大小。如果在堆中没有足够的内存完成实例分配,并且堆也无法扩展时就会抛出OutOfMemoryError异常。

5. 方法区

    方法区(Method Area)和Java堆一样,是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的的代码等数据。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,对于HotSpot虚拟机来说,GC分代收集也可以在方法区上进行,因此也把方法区成为“永久代”(Permanent Generation)。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

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

    运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说除了预置入Class文件常量池中的内容外,运行期间也可能将新的常量放入方法区运行时常量池。如String的inter()方法。

6. 直接内存

    这里还要说明一下直接内存(Direct Memory)的概念,它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也经常被频繁使用,并且也可能会导致OutofMemoryError异常。

    JDK1.4之后引入了NIO(New Input/Output)类,相对于以前同步阻塞的OIO来说,可以执行异步非阻塞的IO操作,NIO引入了一种基于通道Channel和缓冲区Buffer的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。当各个内存区域综合大于物理内存限制时,就会使动态扩展时出现OutOfMemoryError异常。

    以上几个部分就是虚拟机所管理的运行时内存区域了,那么当一个对象被创建时,虚拟机都做了哪些工作呢?虚拟机遇到一条new指令时,会首先检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有则执行类加载的过程。在类加载检查通过后,虚拟机将为新生对象分配内存。由于对象所需的内存大小在类加载完成后已经确定了,因此为对象分配内存空间只需要把一块确定大小的内存从Java堆中划分出来。如果Java堆内存空间是绝对规整的,已用内存和空闲内存分放两边,那么可以使用“指针碰撞”的分配方式。如果内存不规整,已使用的内存和未使用的内存相互交错,则需要使用“空闲列表”的分配方式,由虚拟机维护一个列表,记录那些内存块是可用的,分配的时候在列表上找到一块足够大的空间分配给对象实例,同时更新列表记录。内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零(不包括对象头),接下来虚拟机还要对对象进行一些必要的设置,如对象是哪个类的实例、如何找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。在上面工作都完成之后,对于虚拟机而言,一个新的对象就产生了。

    



猜你喜欢

转载自blog.csdn.net/cfydaniel/article/details/41441585