查看原文
其他

Java内存管理,看这篇就对了!

点击上方 "程序员小乐"关注公众号, 星标或置顶一起成长

每天早上8点20分, 第一时间与你相约

每日英文

Life's greatest regret,than the wrong insist,and easily give up.

人生最大的遗憾,莫过于错误的坚持,和轻易的放弃。


每日掏心话

很喜欢这三句话:知人不必言尽,言尽则无友。责人不必苛尽,苛尽则众远。敬人不必卑尽,卑尽则少骨。


来自:warmor | 责编:乐乐

链接:blog.csdn.net/wdong_love_cl/article/details/51597854

程序员小乐(ID:study_tech)第 651 次推文   图片来自网络


往日回顾:成为一名优秀程序员的101个小建议



   正文   

  

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


Java内存管理分为两个方面:内存分配和垃圾回收,下面我们一一的来看一下。


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



第一、程序计数器(PC、Program Counter Register)

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来取下一条需要执行的字节码指令,分支、跳转、循环、异常处理、线程恢复等基础功能都需要这个计数器来完成。


当线程正在执行的是一个Java方法,这个计数器记录的是在正在执行的虚拟机字节码指令的地址;当执行的是Native方法,这个计数器值为空。


注:每条线程都会有一个独立的程序计数器,唯一不会出现OOM的。


第二、Java栈/虚拟机栈(VM Stack)


Java栈就是Java中的方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,这个栈帧用于存储局部变量表、操作数栈、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用、方法返回地址等信息,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。


局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。


操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。


指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。


方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。


异常可能性:对于栈有两种异常情况,如果线程请求的栈深度大于栈所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态拓展,在拓展的时无法申请到足够的内存,将会抛出OutOfMemoryError异常

第三、本地方法栈(Native Method Stack)


本地方法栈与Java栈所发挥的作用是非常相似的,它们之间的区别不过是Java栈执行Java方法,本地方法栈执行的是本地方法。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。


异常可能性:和Java栈一样,可能抛出StackOverflowError和OutOfMemeryError异常

第四、Java堆(Heap)


对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的目的就是存放对象实例以及数组(当然,数组引用是存放在Java栈中的),几乎所有的对象实例都在这里分配内存,当然我们后面说到的垃圾回收器的内容的时候,其实Java堆就是垃圾回收器管理的主要区域。


从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。在实现上,既可以实现固定大小的,也可以是扩展的。


异常可能性:如果堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemeryError异常


第五、方法区(Method Area)


方法区它用于存储已被虚拟机加载的类信息(包括类的名称、方法信息、字段信息)、常量、静态变量、以及编译器编译后的代码等数据。


1)运行时常量池


运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载器进入方法区后的运行时异常常量池存放。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非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方法能有多少个参数类型?这个好奇Coder做了个实验

如何写一个更好的Python函数?


关注微信公众号「程序员小乐」,收看更多精彩内容
嘿,你在看吗?

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存