Java内存区域及Java垃圾回收机制概述

来源:互联网 发布:淘宝代销赚钱吗 编辑:程序博客网 时间:2024/06/07 07:35

Java内存区域及Java垃圾回收机制概述


参考文档

《深入理解Java虚拟机》
http://bbs.itheima.com/thread-51574-1-1.html
http://www.cnblogs.com/Cratical/archive/2012/08/21/2649985.html
http://blog.csdn.net/ns_code/article/details/18076173


Java内存区域

Java在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途、创建和销毁的时间,有一些是随虚拟机的启动而创建,随虚拟机的退出而销毁,有些则是与线程一一对应,随线程的开始和结束而创建和销毁。
Java虚拟机所管理的内存将会包括以下几个运行时数据区域

程序计数器(Program Counter Register)

它是一块较小的内存空间,它的作用可以看做是当先线程所执行的字节码的信号指示器。
每一条JVM线程都有自己的PC寄存器,各条线程之间互不影响,独立存储,这类内存区域被称为“线程私有”的内存
在任意时刻,一条JVM线程只会执行一条线程中的指令。
如果线程正在执行的是一个Java方法,那PC寄存器保存JVM正在执行的字节码指令的地址。
如果线程正在执行的是一个Native方法,那PC寄存器的值是空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。


Java虚拟机栈(Java Virtual Machine Stacks)

Java虚拟机栈是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAdress类型(指向了一条字节码指令的地址)
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
如果线程请求的栈深度大于虚拟机所允许的深度将抛出StackOverflowError;
如果JVM Stack可以动态扩展,但是在尝试扩展时无法申请到足够的内存时抛出OutOfMemoryError。


本地方法栈(Native Method Stacks)

本地方法栈与虚拟机栈作用相似,java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为虚拟机用到的Native方法服务。
虚拟机规范对于本地方法栈中方法使用的语言,使用方式和数据结构没有强制规定,因此具体的虚拟机可以自由实现它,甚至有的虚拟机(比如HotSpot)直接把二者合二为一。
本地方法栈抛出的异常跟Java虚拟机栈抛出的异常一样。


Java堆

虚拟机管理的内存中最大的一块,同时也是被所有线程所共享的,它在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。(但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化。)
Java堆是垃圾收集器管理的主要区域。因此也叫GC堆(Garbage Collected Heap)。
目前收集器基本都是采用的分代收集算法,所以Java堆可以细分为:新生代和老年代;或者是Eden空间,From Servivor空间,To Survivor空间等。
Java堆的容量可以是固定大小,也可以随着需求动态扩展(-Xms和-Xmx),并在不需要过多空间时自动收缩。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的。当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx和-Xms控制)。。
如果堆中没有内存完成实例分配并且堆也无法扩展,就会抛出OutOfMemoryError异常。


方法区(Method Area)

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它又叫Non-Heap(非堆),目的是与Java堆区分开来。
Java虚拟机规范对这个区域的限制非常宽松,除了具有Java堆的特性外,甚至可以不实现垃圾收集器。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。


运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。


直接内存(Direct Memory)

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK1.4中新加入的NIO类,引入了一种基于通道(Channel)与缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。


对象访问

以如下代码为例:
Object obj = new Object();
“Object obj”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。
“new Object()”这部分反映到Java堆中,形成一块存储了Object类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查找到此对象类型的数据(如对象类型,父类,实现的接口,方法等)的地址信息,这些类型数据则存储在方法区中。
不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
1.句柄访问方式,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。这种方式最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
 
2.直接指针访问方式,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。这种访问方式的好处就是速度更快,它节省了一次指针定位的时间开销。(HotSpot使用的就是这一种方式)
 


java垃圾回收机制

GC需要完成的三件事情

哪些内存需要回收
什么时候回收
如何回收


标记算法(哪些内存需要回收)

引用计数算法

应用计数算法:给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法的实现简单,判定效率高。(COM,python等语言使用),它的缺点是很难解决对象之间的相互循环引用的问题。(Java没有使用这个算法)

根搜索算法

根搜索算法:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(图论中GC Roots到这个对象不可达)时,则证明此对象不可用的。
在Java语言里可作为GC Roots的对象包括下面几种
  • 虚拟机栈(栈中的本地变量表)中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI的引用的对象

Java中引用的分级

  • 强引用(Strong Reference)就是指在程序代码之中普遍存在的“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用(Soft Reference)用来描述一些还有用,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中并进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
  • 弱引用(Weak Reference)也是用来描述非必需对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用(Phantom Reference)是最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知

什么时候回收

实际上,在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。Java中不建议使用finalize()方法做任何事。


垃圾收集算法(如何回收)

判定除了垃圾对象之后,便可以进行垃圾回收了。下面介绍一些垃圾收集算法,由于垃圾收集算法的实现涉及大量的程序细节,因此这里主要是阐明各算法的实现思想,而不去细论算法的具体实现。

标记—清除算法

    标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象。标记—清除算法的执行情况如下图所示:
  
    该算法有如下缺点:
  • 标记和清除过程的效率都不高。
  • 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。

复制算法

    复制算法是将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。
复制算法有如下优点:
  • 每次只对一块内存进行回收,运行高效。
  • 只需移动栈顶指针,按顺序分配内存即可,实现简单。
  • 内存回收时不用考虑内存碎片的出现。
它的缺点是:可一次性分配的最大内存缩小了一半。
复制算法的执行情况如下图所示:
 

标记—整理算法

    复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:
 

分代收集

    当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。





0 0