码迷,mamicode.com
首页 > 编程语言 > 详细

Java内存管理

时间:2016-06-06 22:13:15      阅读:308      评论:0      收藏:0      [点我收藏+]

标签:

Java程序实际上是把内存控制的权力交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误将会成为一项异常艰难的工作。而且了解了Java的内存管理,有助于优化JVM,从而使得自己的应用获得最佳的性能体验。所以还等什么,赶紧跟着我来一起学习这方面的知识吧~
Java内存管理分为两个方面:内存分配和垃圾回收,下面我们一一的来看一下。

Jvm定义了5个区域用于存储运行时的数据,如下:

技术分享

第一、程序计数器(PC、Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来取下一条需要执行的字节码指令,分支、跳转、循环、异常处理、线程恢复等基础功能都需要这个计数器来完成。
当线程正在执行的是一个Java方法,这个计数器记录的是在正在执行的虚拟机字节码指令的地址;当执行的是Native方法,这个计数器值为空。
注:每条线程都会有一个独立的程序计数器,唯一不会出现OOM的。
第二、Java栈/虚拟机栈(VM Stack)
Java栈就是Java中的方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,这个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表中存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
异常可能性:对于栈有两种异常情况,如果线程请求的栈深度大于栈所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态拓展,在拓展的时无法申请到足够的内存,将会抛出OutOfMemoryError异常
第三、本地方法栈(Native Method Stack)
本地方法栈与Java栈所发挥的作用是非常相似的,它们之间的区别不过是Java栈执行Java方法,本地方法栈执行的是本地方法。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。
异常可能性:和Java栈一样,可能抛出StackOverflowError和OutOfMemeryError异常
第四、Java堆(Heap)
对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,当然我们后面说到的垃圾回收器的内容的时候,其实Java堆就是垃圾回收器管理的主要区域。
从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。在实现上,既可以实现固定大小的,也可以是扩展的。
异常可能性:如果堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemeryError异常
第五、方法区(Method Area)
方法区它用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。
1)运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载器进入方法区后的运行时异常常量池存放。
常量池中存放的是运行过程中的常量,我们知道String类型的intern方法就是将字符串的值放到常量池中的。
相对而言,垃圾收集行为在这个区域比较少出现,但并非数据进了方法区就永久的存在了,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
异常可能性:当方法区无法满足内存分配需求时,将抛出OutOfMemeryError异常
直接内存
直接内存不是虚拟机运行时数据区的一部分,在NIO类中引入一种基于通道与缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,或直接使用Unsafe.allocateMemory,但不推荐这种方式。
直接内存的分配不会受到Java堆大小的限制,但是会受到本机内存大小的限制,所有也可能会抛OutOfMemoryError异常。
说明:
线程私有:程序计数器,Java栈,本地方法栈
线程共享:Java堆,方法区

垃圾回收(garbage collection,简称GC)可以自动清空堆中不再使用的对象,由于不需要手动释放内存,程序员在编程中也可以减少犯错的机会。利用垃圾回收,程序员可以避免一些指针和内存泄露相关的bug(这一类bug通常很隐蔽),但另一方面,垃圾回收需要耗费更多的计算时间,垃圾回收实际上是将原本属于程序员的责任转移给了计算机。

在Java中,对象的是通过引用使用的,如果不再有引用指向对象,那么我们就再也无从调用或者处理该对象。这样的对象将不可到达(unreachable)。垃圾回收用于释放不可到达对象所占据的内存。这是垃圾回收的基本原则。

Java中的引用类型有4种,由强到弱依次如下:
1) 强引用(StrongReference)是使用最普遍的引用,类似:“Object obj = new Object()” 。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
2) 软引用(Soft Reference)是用来描述一些有用但并不是必需的对象,如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
3) 弱引用(WeakReference)也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
4) 虚引用(PhantomReference)也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。Weak引用对象更容易、更快被GC回收。虽然GC在运行时一定回收Weak对象,但是复杂关系的Weak对象群常常需要好几次 GC的运行才能完成。Weak引用对象常常用于Map结构中,引用数据量较大的对象,一旦该对象的强引用为null时,GC能够快速地回收该对象空间。 

Soft Reference的主要特点是据有较强的引用功能。只有当内存不够的时候,才进行回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用图片的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。
下面就是一个使用弱引用的例子:
    // 申请一个图像对象  
 Image image=new Image();       // 创建Image对象   
 // 使用 image  
 …  
 // 使用完了image,将它设置为soft 引用类型,并且释放强引用;  
 SoftReference sr=new SoftReference(image);  
 image=null;  
 …  
 // 下次使用时  
 if (sr!=null)   
	image=sr.get();  
 else{  
	image=new Image();  //由于GC由于低内存,已释放image,因此需要重新装载;  
	sr=new SoftReference(image);  
 }  

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 

下面的例子是将虚引用关联到引用队列:

ReferenceQueue refQueue = new ReferenceQueue(); //reference will be stored in this queue for cleanup
DigitalCounter digit = new DigitalCounter();
PhantomReference<DigitalCounter> phantom = new PhantomReference<DigitalCounter>(digit, refQueue);


GC算法

“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

技术分享

它的主要缺点是:
1、效率问题,标记和清除过程的效率都不高;
2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作;

“复制”(Copying)算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

技术分享

它的主要缺点是:
1、只是这种算法是将内存缩小为原来的一半,有点过于浪费;
2、对象存活率较高时就要执行较多的复制操作,效率将会变低;

“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这样话连续的内存空间就比较多了。

技术分享

上面几种算法是通过分代回收(generational collection)混合在一起的,一般是把Java堆分为Young Generation(新生代),Old Generation(老年代)和Permanent Generation(持久代),这样就可以根据各个年代的特点采用最适当的回收算法。

技术分享

1) 在Young Generation中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from、to),它们的大小总是一样,它们用来存放每次垃圾回收后存活下来的对象。
3) 在Young Generation块中,垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个SurvivorSpace,当Survivor Space空间满了后,剩下的live对象就被直接拷贝到OldGeneration中去。因此,每次GC后,Eden内存块会被清空。
4) 在Old Generation块中主要存放应用程序中生命周期长的内存对象,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。
5)在Permanent Generation中,主要用来放JVM自己的反射对象,比如类对象和方法对象等。
6) 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收Old段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾,内存溢出通常发生于Old段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

说明:
from, to: 这两个区域大小相等,相当于copying算法中的两个区域,当新建对象无法放入eden区时,将出发minor collection。JVM采用copying算法,将eden区与from区的可到达对象复制到to区。经过一次垃圾回收,eden区和from区清空,to区中则紧密的存放着存活对象。随后from区成为新的to区, to区成为新的from区。如果进行minor collection的时候,发现to区放不下,则将部分对象放入成熟世代。另一方面,即使to区没有满,JVM依然会移动世代足够久远的对象到成熟世代。如果成熟世代放满对象,无法移入新的对象,那么将触发major collection(Full回收)。

Java内存管理

标签:

原文地址:http://blog.csdn.net/wdong_love_cl/article/details/51597854

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!