Java内存管理

2017/2/20 posted in  Java  

Java的内存区域

Java的内存区域按照是否线程共享分为两个区域

  1. 线程共享区域包括:方法区(Method Area)和堆(Heap)
  2. 线程独享的区域包括:虚拟机栈(VM Stack),本地方法栈(Native Method Stack)和程序计数器(Program Counter Register)

程序计数器

程序计数器是一块较小的内存区域,指向当前执行的字节码。如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,则计数器为空。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

Java虚拟机栈

线程私有区域,其生命周期和线程一致。该区域描述的是Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧(方法运行时的基本数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
在Java虚拟机规范中,对这个区域规定了两种异常情况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  2. 如果虚拟机栈可以动态扩展(当前大部分虚拟机实现都是动态的、但是固定长度的栈也是被允许的)在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError异常和OutOfMemoryError异常。

Java堆

是虚拟机管理内存中最大的一块,被所有线程共享,该区域用于存放对象实例,几乎所有的对象都在改区域分配。Java堆是内存回收的主要区域,也被称为GC堆。从内存回收的角度来看,由于现在的垃圾收集器大都采用分代收集算法,所以还可以将Java堆细分为:新生代和老年代,或继续细分为Eden空间、From Survivor空间、To Survivor空间等。
根据Java虚拟机规范,Java堆可以处于物理上不连续的空间,只要逻辑上是连续的就行,在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的都是可扩展的(通过-Xmx-Xms参数控制)。
如果在堆中没有内存完成实例分配,而堆又无法继续扩展时,抛出OutOfMemoryError异常。

方法区

与Java一样,是各个线程所共享的,用于存储已被虚拟机加载类信息、常亮、静态变量、即时编译器编译后的代码等数据。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。运行期间可以将新的常量放入常量池中,用得比较多的就是String类的intern()方法,当一个String实例调用intern时,Java查找常量池中是否有相同的Unicode的字符串常量,若有,则返回其引用;若没有,则在常量池中增加一个Unicode等于该实例字符串并返回它的引用。

Java堆的内存模型

内存模型如图所示:

广泛地说,JVM堆内存被分为两部分——年轻代(Young Generation)和老年代(Old Generation)。

年轻代

年轻代是所有新对象产生的地方。当年轻代内存空间被用完时,就会触发垃圾回收。这个垃圾回收叫做Minor GC。年轻代被分为3个部分——Eden区和2个Survivor区。

年轻代的空间要点

  • 大多数新建对象都位于Eden区;
  • 当Eden区域被填满时,就会触发Minor GC,并把所有存活下来的对象复制到其中一个survivor区域;
  • Minor GC同样会检查存活下来的对象,并把它们转移到另一个Survivor区域,这样在一段时间内,总有一个空的Survivor区;
  • 经过多次GC周期后,仍然存活下来的对象会被转移到年老代空间,通常这是在年轻代有资格提升到老年代前通过年龄阈值来完成的。

年老代

年老代内存里包含了长期存活的对象和经过多次Minor后依然存活下来的对象。年老代在空间不足时,将触发Major GC,将会花费更多时间进行垃圾收集。

垃圾回收

如何确定垃圾对象

Java堆当中几乎储存了所有的对象实例,要想实现自动的垃圾回收,知道哪些对象是‘垃圾’就十分必要了。目前,比较流行的自动垃圾回收,都是基于两个思路实现的。

引用计数法

引用计数法实现简单,效率较高,在大部分情况下是一个不错的算法。其原理是:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,当计数器值为0时表示该对象不再被使用。需要注意的是:引用计数法很难解决对象之间相互循环引用的问题,主流Java虚拟机没有选用引用计数法来管理内存。

可达性分析法

Java虚拟机使用可达性分析法来进行对象的可用性判断。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

Java中的GC ROOT
  • 虚拟机栈中的引用对象
  • 方法区中的静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中本地方法引用的对象
对象的救赎

在可达性分析中不可用的对象并不是立刻被虚拟机销毁,而是被标记为待回收,要真正的宣告一个对象死亡,至少需要经历两次标记:如果对象在可达性分析后发现没有与GC ROOT相连的引用链,那么他会被第一次被标记并进行一次筛选,筛选是判断此对象是否有必要执行finalize()方法,当对象没有覆盖Object类的finalize()方法或该类的finalize()方法已经被虚拟机调用过,虚拟机将视为没有必要执行,对象会被销毁。反之,将会将该对象加入一个优先级非常低的队列中等待执行finalize()方法,但不保证等待其执行完,这是对象进行自我救赎的最后也是唯一的机会。

典型的垃圾回收算法

标记-清楚(Mark-Sweep)算法

这是最基础的垃圾回收算法,标记-清楚算法顾名思义,分为两个阶段,标记和删除。标记阶段的任务是标记出那些对象需要被回收(即我们上面说的不可达或不可用对象),清楚阶段就是回收(释放)被标记对象所占用的内存空间。具体样例如下图:

通过观察图片我们可以非常轻松的发现,标记-清楚算法虽然非常简单,但是有一个比较严重的问题,那就是非常容易产生大量的内存碎片,碎片的数量一旦过多就会导致在后续为大对象分配内存空间时午饭找到足够的内存空间而提前出发新一轮的垃圾收集严重时会导致内存溢出。

复制(Copying)算法

为了解决标记-清楚算法的缺陷,复制算法应运而生。复制算法的思路是,将可用内存按容量划分为大小相同的两块,每次创建对象时只使用其中的一块儿。当这一块儿内存用完时,就触发垃圾回收,将还存活的对象复制到另外一块儿内存空间之上,再把已经使用的内存空间一次释放掉,这样一来就不会出现内存碎片的问题了。操做实例如图:

复制算法虽然实现简单,运行效率高而且也不会出现内存碎片,但是对内存空间的使用却付出了高昂的代价,可用内存缩减为了原先的一半。同时复制算法的效率和存活对象数目的多少直接相关,若存活对象数量较多,效率就会大大的降低。

标记整理算法

为了解决上面两种算法的缺陷,提出了标记整理(Mark-Compact)算法。该算法在标记阶段和标记清理算法完全一致,但是在完成标记之后,不是直接清理可回收对象,而是将存活的对象都向内存一段移动,然后清理端边界以外的内存,具体过程如下图:

分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(一般为8:1:1),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

Java堆的配置参数

Java中的引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
在JDK 1.2之后,Java对引用的概念进行了扩充,提出了四种不同的引用。

强引用

就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用

是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

弱引用

也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

虚引用

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。