神刀安全网

Java – 自动内存管理机制

在学习Java的时候,我们通常会将其与c++进行对比,Java在c++的基础上作了许多改进,摒弃了c++中很多很少使用、难以理解的且容易混淆的特性,例如头文件、指针、运算符重载、多继承等等。Java与c++相比有很多不同之处,其中就包括自动内存管理机制。在c++中,对于new分配的内存最终都需要使用对应的delete进行释放。而对于Java来说,Java虚拟机的自动内存管理机制在内存管理方面帮我们作了很多工作,避免了很多内存方面的问题。本文主要简单总结一下自动内存管理方面的内容,如有错误之处,还请指出,共同学习。

一、内存分配   1.JVM体系结构   2.运行时数据区域   3.内存分配 二、内存回收   1.垃圾收集算法   2.垃圾收集器 三、相关参考

一、内存分配

1.JVM体系结构

在了解自动内存管理的内存分配之前,我们先看下JVM的体系结构。代码编译的结果是从本地机器码转变为字节码,经过类加载器加载到虚拟机后才能执行程序。JVM的体系结构主要如下图所示:

Java - 自动内存管理机制
JVM体系结构

2.运行时数据区域

在上图中我们可以清楚地看到,JVM在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,分别是程序计数器、Java虚拟机栈、本地方法栈、方法区(包括运行时常量池)、堆。下面逐一介绍。

  • 程序计数器
    线程私有,不会出现OOM,是一块较小的内存空间,作用可以看作是当前线程所执行的字节码的行号指示器,因为JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程间的计数器互不影响,独立存储。
  • 虚拟机栈
    线程私有,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,虚拟机栈表示Java方法执行的内存模型,每调用一个方法,就会生成一个栈帧(Stack Frame)用于存储方法的本地变量表、操作栈、方法出口等信息,当这个方法执行完后,就会弹出相应的栈帧。这部分区域,如果请求的栈的深度过大,虚拟机可能会抛出StackOverflowError异常,如果虚拟机的实现中允许虚拟机栈动态扩展,当内存不足以扩展栈的时候,会抛OutOfMemoryError异常。
  • 本地方法栈
    线程私有,本地方法栈与虚拟机栈类似,只是在执行本地方法时使用。

  • 线程共享,几乎所有的对象实例以及数组都是在这个区域进行分配,这里也是垃圾回收的主要区域就是这里(还可能有方法区)。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。从内存回收的角度看,Java堆中可细分为新生代和老年代,再细致点有Eden空间、From Survivor空间、To Survivor空间等。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,就会抛出OOM异常。
    Java - 自动内存管理机制
    堆内存

  • 方法区
    线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等信息。当方法区无法满足内存分配需求时,会抛出OOM异常。运行时常量池是方法区的一部分,主要用来存储编译时生成的字面量和符号引用。

下面这张运行时数据区域图可能更形象一点。

Java - 自动内存管理机制

3.内存分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次新生代GC(Minor GC)。

在设置虚拟机参数的时候,-Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=8的含义是-Xmx与-Xms相等限制了堆大小为20MB,-Xmn表示新生代大小为10MB,剩下的10MB分配给老年代,-XX:SurvivorRatio=8表示新生代中Eden区与一个Survivor区空间比例大小为8:1,所以Eden区大小为8192K,一个Survivor区大小为1024K,新生代总可用空间为9216K。

大对象直接进入老年代,大对象是指需要连续内存空间的Java对象,比如很长的字符串及数组。长期存活的对象将进入老年代。

有道题是这样的:

Java - 自动内存管理机制
问题

这道题是自己在之前遇到过的,题目本身表达有点歧义,答案是堆和字符串常量池中,当new String(“abc”)时,其实会先在字符串常量区生成一个abc的对象,然后new String()时会在堆中分配空间,然后此时会把字符串常量区中abc复制一个给堆中的String,故abc应该在堆中和字符串常量区。

二、内存回收

垃圾收集器在对堆进行回收前,首先需要判断对象是否存活,即是否可能再被使用。这里就要提到一个引用计数算法,每当一个对象被引用时,计数器就加1,引用失效时就减1,当计数器为0时就不可能被使用了,这是一个简单的判断对象是否存活的算法,但是Java中并没有选用,主要是因为它很难解决对象间相互循环引用的问题。

Java中判断对象是否存活,使用的是根搜索算法,基本思路就是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,这个对象就是不可用的。

1.垃圾收集算法

在确定哪些内存需要回收后,接下来就是怎么回收的问题了,也就是垃圾收集算法,常用的几种如下:标记-清除算法、复制算法、标记-整理算法以及分代回收算法。

  • 标记-清除算法
    这是最基础的收集算法,首先标记出所有需要回收的对象,标记完成后统一回收掉所有被标记的对象。缺点有两个,一个是效率问题,标记和清除过程的效率不高,另一个是空间问题,标记清除后会产生大量的不连续的内存碎片。
  • 复制算法
    这个算法主要是为了解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块的内存用完时,将活着的对象复制到另外一块上面,然后把已经使用过的内存空间清理掉。优点是每次都是对其中一块内存进行回收,不用考虑内存碎片等情况,实现简单,运行高效,缺点是内存缩小为原来的一半。
  • 标记-整理算法
    复制收集算法在对象存活率较高时就要执行较多的复制操作,效率会变低,标记整理算法首先标记出所有需要回收的对象,之后让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
  • 分代收集算法
    根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,根据各个年代的特点采用适当地收集算法。在新生代中,每次垃圾收集时都有大量对象死去,只有少量存活,就选用复制算法,老年代中因为对象存活率高、没有额外空间进行 分配担保,就采用标记-清理或者标记-整理回收。
2.垃圾收集器

垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,下面介绍几种常见的垃圾收集器。

  • Serial收集器
    最基本、历史最悠久的收集器,在JDK 1.3.1之前是新生代收集的唯一选择,它是一个单线程收集器,在工作时必须暂停其他所有的工作线程。
  • ParNew收集器
    Serial收集器的多线程版本,其它方面基本与Serial一致。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
  • Parallel Scavenge 收集器
    吞吐量优先的垃圾回收器,作用在新生代,使用复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。其它收集器主要尽可能地缩短垃圾收集时用户线程的停顿时间,停顿时间短适合需要与用户交互的程序,而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
  • Serial Old收集器
    Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。
  • Parallel Old收集器
    Parallel Scavenge 收集器的老年代版本,使用标记-整理算法,多线程,这个收集器是JDK 1.6才开始提供的,在此之前新生代的Parallel Scavenge收集器比较尴尬,只能与Serial Old搭配,性能有待提高,Parallel Old出现后与Parallel Scavenge搭配很不错。
  • CMS(Concurrent Mark Sweep)收集器
    致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。

三、相关参考

1、《深入理解Java虚拟机》 周志明
2、JVM内幕:Java虚拟机详解 http://www.importnew.com/17770.html

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Java – 自动内存管理机制

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址