不少Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收确定要涉及到JVM内存管理机制,Java语言的执行效率一直被C、C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确实很低,一方面,Java语言采用面向对象思想,这也决定了其必然是开发效率高,执行效率低。另外一方面,Java语言对程序员作了一个美好的承诺:程序员无需去管理内存,由于JVM有垃圾回收(GC),会去自动进行垃圾回收。java
其实否则:c++
一、垃圾回收并不会按照程序员的要求,随时进行GC。程序员
二、垃圾回收并不会及时的清理内存,尽管有时程序须要额外的内存。面试
三、程序员不能对垃圾回收进行控制。算法
由于上面这些事实,以至咱们在写程序的时候,只能根据垃圾回收的规律,合理安排内存,这就要求咱们必须完全了解JVM的内存管理机制,这样才能为所欲为,将程序控制于鼓掌之中!本章系Java之美[从菜鸟到高手演变]系列之JVM内存管理及垃圾回收,学完本章知识,读者对JVM就会有基本的了解。编程
本博客永久更新,若有转载,数组
请说明出处:http://blog.csdn.net/zhangerqing缓存
若有问题,请联系本人: egg性能优化
邮箱:xtfggef@gmail.com服务器
微博:http://weibo.com/xtfggef
1、JVM内存的构
Java虚拟机会将内存分为几个不一样的管理区,这些区域各自有各自的用途,根据不一样的特色,承担不一样的任务以及在垃圾回收时运用不一样的算法。整体分为下面几个部分:
程序计数器(Program Counter Register)、JVM虚拟机栈(JVM Stacks)、本地方法栈(Native Method Stacks)、堆(Heap)、方法区(Method Area)
以下图:
一、程序计数器(Program Counter Register)
这是一块比较小的内存,不在Ram上,而是直接划分在CPU上的,程序员没法直接操做它,它的做用是:JVM在解释字节码文件(.class)时,存储当前线程所执行的字节码的行号,只是一种概念模型,各类JVM所采用的方式不一样,字节码解释器工做时,就是经过改变程序计数器的值来选取下一条要执行的指令,分支、循环、跳转、等基础功能都是依赖此技术区完成的。还有一种状况,就是咱们常说的Java多线程方面的,多线程就是经过现程轮流切换而达到的,同一时刻,一个内核只能执行一个指令,因此,对于每个程序来讲,必须有一个计数器来记录程序的执行进度,这样,当现程恢复执行的时候,才能从正确的地方开始,因此,每一个线程都必须有一个独立的程序计数器,这类计数器为线程私有的内存。若是一个线程正在执行一个Java方法,则计数器记录的是字节码的指令的地址,若是执行的一个Native方法,则计数器的记录为空,此内存区是惟一一个在Java规范中没有任何OutOfMemoryError状况的区域。
二、JVM虚拟机栈(JVM Stacks)
JVM虚拟机栈就是咱们常说的堆栈的栈(咱们经常把内存粗略分为堆和栈),和程序计数器同样,也是线程私有的,生命周期和线程同样,每一个方法被执行的时候会产生一个栈帧,用于存储局部变量表、动态连接、操做数、方法出口等信息。方法的执行过程就是栈帧在JVM中出栈和入栈的过程。局部变量表中存放的是各类基本数据类型,如boolean、byte、char、等8种,及引用类型(存放的是指向各个对象的内存地址),所以,它有一个特色:内存空间能够在编译期间就肯定,运行期不在改变。这个内存区域会有两种可能的Java异常:StackOverFlowError和OutOfMemoryError。
三、本地方法栈(Native Method Stacks)
从名字便可看出,本地方法栈就是用来处理Java中的本地方法的,Java类的祖先类Object中有众多Native方法,如hashCode()、wait()等,他们的执行不少时候是借助于操做系统,可是JVM须要对他们作一些规范,来处理他们的执行过程。此区域,能够有不一样的实现方法,向咱们经常使用的Sun的JVM就是本地方法栈和JVM虚拟机栈是同一个。
四、堆(Heap)
堆内存是内存中最重要的一块,也是最有必要进行深究的一部分。由于Java性能的优化,主要就是针对这部份内存的。全部的对象实例及数组都是在堆上面分配的(随着JIT技术的逐渐成熟,这句话视乎有些绝对,不过至少目前还基本是这样的),可经过-Xmx和-Xms来控制堆的大小。JIT技术的发展产生了新的技术,如栈上分配和标量替换,也许在不久的几年里,即时编译会诞生及成熟,那个时候,“全部的对象实例及数组都是在堆上面分配的”这句话就应该稍微改改了。堆内存是垃圾回收的主要区域,因此在下文垃圾回收板块会重点介绍,此处只作概念方面的解释。在32位系统上最大为2G,64位系统上无限制。可经过-Xms和-Xmx控制,-Xms为JVM启动时申请的最小Heap内存,-Xmx为JVM可申请的最大Heap内存。
五、方法区(Method Area)
方法区是全部线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量等数据,通常来讲,方法区属于持久代(关于持久代,会在GC部分详细介绍,除了持久代,还有新生代和旧生代),也难怪Java规范将方法区描述为堆的一个逻辑部分,可是它不是堆。方法区的垃圾回收比较棘手,就算是Sun的HotSpot VM在这方面也没有作得多么完美。此处引入方法区中一个重要的概念:运行时常量池。主要用于存放在编译过程当中产生的字面量(字面量简单理解就是常量)和引用。通常状况,常量的内存分配在编译期间就能肯定,但不必定全是,有一些可能就是运行时也可将常量放入常量池中,如String类中有个Native方法intern()<关于intern()的详细说明,请看另外一篇文章:http://blog.csdn.net/zhangerqing/article/details/8093919>
此处补充一个在JVM内存管理以外的一个内存区:直接内存。在JDK1.4中新加入类NIO类,引入了一种基于通道与缓冲区的I/O方式,它可使用Native函数库直接分配堆外内存,即咱们所说的直接内存,这样在某些场景中会提升程序的性能。
2、垃圾回收
有句话说的好:Java和C++之间有一堵有内存分配和垃圾回收技术围成的墙,墙外的人想进去,墙里的人想出去!这句话的意思,请读者本身去琢磨。总的来讲,C、C++程序员有时苦于内存泄露,内存管理是件使人头痛的事儿,可是Java程序员呢,又羡慕C++程序员,本身能够控制一切,这样就不会在内存管理方面显得一筹莫展,的却如此,做为Java程序员咱们很难去控制JVM的内存回收,只能根据它的原理去适应,尽可能提升程序的性能。下面开始讲解Java垃圾回收,即Garbage Collection,GC。从如下四个方面进行:
一、为何要进行垃圾回收?
随着程序的运行,内存中存在的实例对象、变量等信息占据的内存愈来愈多,若是不及时进行垃圾回收,必然会带来程序性能的降低,甚至会由于可用内存不足形成一些没必要要的系统异常。
二、哪些“垃圾”须要回收?
在咱们上面介绍的五大区中,有三个是不须要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。由于它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,因此只有方法区和堆须要进行GC。具体到哪些对象的话,简单概况一句话:若是某个对象已经不存在任何引用,那么它能够被回收。通俗解释一下就是说,若是一个对象,已经没有什么做用了,就能够被当废弃物被回收了。
三、何时进行垃圾回收?
根据一个经典的引用计数算法,每一个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是能够被回收得了。可是,这个算法有明显的缺陷:当两个对象相互引用,可是两者已经没有做用时,按照常规,应该对其进行垃圾回收,可是其相互引用,又不符合垃圾回收的条件,所以没法完美处理这块内存清理,所以Sun的JVM并无采用引用计数算法来进行垃圾回收。而是采用一个叫:根搜索算法,以下图:
基本思想就是:从一个叫GC Roots的对象开始,向下搜索,若是一个对象不能到达GC Roots对象的时候,说明它已经再也不被引用,便可被进行垃圾回收(此处 暂且这样理解,其实事实还有一些不一样,当一个对象再也不被引用时,并无彻底“死亡”,若是类重写了finalize()方法,且没有被系统调用过,那么系统会调用一次finalize()方法,以完成最后的工做,在这期间,若是能够将对象从新与任何一个和GC Roots有引用的对象相关联,则该对象能够“重生”,若是不能够,那么就说明完全能够被回收了),如上图中的Object五、Object六、Object7,虽然它们3个依然可能相互引用,可是整体来讲,它们已经没有做用了,这样就解决了引用计数算法没法解决的问题。
补充引用的概念:JDK 1.2以后,对引用进行了扩充,引入了强、软、若、虚四种引用,被标记为这四种引用的对象,在GC时分别有不一样的意义:
a> 强引用(Strong Reference).就是为刚被new出来的对象所加的引用,它的特色就是,永远不会被回收。
b> 软引用(Soft Reference).声明为软引用的类,是可被回收的对象,若是JVM内存并不紧张,这类对象能够不被回收,若是内存紧张,则会被回收。此处有一个问题,既然被引用为软引用的对象能够回收,为何不去回收呢?其实咱们知道,Java中是存在缓存机制的,就拿字面量缓存来讲,有些时候,缓存的对象就是当前无关紧要的,只是留在内存中若是还有须要,则不须要从新分配内存便可使用,所以,这些对象便可被引用为软引用,方便使用,提升程序性能。
c> 弱引用(Weak Reference).弱引用的对象就是必定须要进行垃圾回收的,无论内存是否紧张,当进行GC时,标记为弱引用的对象必定会被清理回收。
d> 虚引用(Phantom Reference).虚引用弱的能够忽略不计,JVM彻底不会在意虚引用,其惟一做用就是作一些跟踪记录,辅助finalize函数的使用。
最后总结,什么样的类须要回收呢?无用的类,何为无用的类?需知足以下要求:
1> 该类的全部实例对象都已经被回收。
2> 加载该类的ClassLoader已经被回收。
3> 该类对应的反射类java.lang.Class对象没有被任何地方引用。
四、如何进行垃圾回收?
本块内容以介绍垃圾回收算法为主,由于咱们前面有介绍,内存主要被分为三块,新生代、旧生代、持久代。三代的特色不一样,造就了他们所用的GC算法不一样,新生代适合那些生命周期较短,频繁建立及销毁的对象,旧生代适合生命周期相对较长的对象,持久代在Sun HotSpot中就是指方法区(有些JVM中根本就没有持久代这中说法)。首先介绍下新生代、旧生代、持久代的概念及特色:
新生代:New Generation或者Young Generation。上面大体分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace 和ToSpace。新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代的大小能够由-Xmn来控制,也能够用-XX:SurvivorRatio来控制Eden和Survivor的比例.
旧生代:Old Generation。用于存放新生代中通过屡次垃圾回收仍然存活的对象,例如缓存对象。旧生代占用大小为-Xmx值减去-Xmn对应的值。
持久代:Permanent Generation。在Sun的JVM中就是方法区的意思,尽管有些JVM大多没有这一代。主要存放常量及类的一些信息默认最小值为16MB,最大值为64MB,可经过-XX:PermSize及-XX:MaxPermSize来设置最小值和最大值。
常见的GC算法:
标记-清除算法(Mark-Sweep)
最基础的GC算法,将须要进行回收的对象作标记,以后扫描,有标记的进行回收,这样就产生两个步骤:标记和清除。这个算法效率不高,并且在清理完成后会产生内存碎片,这样,若是有大对象须要连续的内存空间时,还须要进行碎片整理,因此,此算法须要改进。
复制算法(Copying)
前面咱们谈过,新生代内存分为了三份,Eden区和2块Survivor区,通常Sun的JVM会将Eden区和Survivor区的比例调为8:1,保证有一块Survivor区是空闲的,这样,在垃圾回收的时候,将不须要进行回收的对象放在空闲的Survivor区,而后将Eden区和第一块Survivor区进行彻底清理,这样有一个问题,就是若是第二块Survivor区的空间不够大怎么办?这个时候,就须要当Survivor区不够用的时候,暂时借持久代的内存用一下。此算法适用于新生代。
标记-整理(或叫压缩)算法(Mark-Compact)
和标记-清楚算法前半段同样,只是在标记了不须要进行回收的对象后,将标记过的对象移动到一块儿,使得内存连续,这样,只要将标记边界之外的内存清理就好了。此算法适用于持久代。
常见的垃圾收集器:
根据上面说的诸多算法,天天JVM都有不一样的实现,咱们先来看看常见的一些垃圾收集器:
首先介绍三种实际的垃圾回收器:串行GC(SerialGC)、并行回收GC(Parallel Scavenge)和并行GC(ParNew)。
一、Serial GC。是最基本、最古老的收集器,可是如今依然被普遍使用,是一种单线程垃圾回收机制,并且不只如此,它最大的特色就是在进行垃圾回收的时候,须要将全部正在执行的线程暂停(Stop The World),对于有些应用这是难以接受的,可是咱们能够这样想,只要咱们可以作到将它所停顿的时间控制在N个毫秒范围内,大多数应用咱们仍是能够接受的,并且事实是它并无让咱们失望,几十毫米的停顿咱们做为客户机(Client)是彻底能够接受的,该收集器适用于单CPU、新生代空间较小及对暂停时间要求不是很是高的应用上,是client级别默认的GC方式,能够经过-XX:+UseSerialGC来强制指定。
二、ParNew GC。基本和Serial GC同样,但本质区别是加入了多线程机制,提升了效率,这样它就能够被用在服务器端(Server)上,同时它能够与CMS GC配合,因此,更加有理由将它置于Server端。
三、Parallel Scavenge GC。在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。如下给出几组使用组合:
四、CMS (Concurrent Mark Sweep)收集器。该收集器目标就是解决Serial GC 的停顿问题,以达到最短回收时间。常见的B/S架构的应用就适合用这种收集器,由于其高并发、高响应的特色。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大体分为4个步骤:
初始标记(CMS initial mark)、并发标记(CMS concurrenr mark)、从新标记(CMS remark)、并发清除(CMS concurrent sweep)。
其中初始标记、从新标记这两个步骤任然须要停顿其余用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会断定对象是否存活。而从新标记阶段则是为了修正并发标记期间,因用户程序继续运行而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。因为整个过程当中耗时最长的并发标记和并发清除过程当中,收集器线程均可以与用户线程一块儿工做,因此总体来讲,CMS收集器的内存回收过程是与用户线程一块儿并发执行的。
CMS收集器的优势:并发收集、低停顿,可是CMS还远远达不到完美。
CMS收集器主要有三个显著缺点:
a>.CMS收集器对CPU资源很是敏感。在并发阶段,虽然不会致使用户线程停顿,可是会占用CPU资源而致使引用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
b>.CMS收集器没法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而致使另外一次Full GC的产生。因为CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出如今标记过程以后,CMS没法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是因为在垃圾收集阶段用户线程还须要运行,即须要预留足够的内存空间给用户线程使用,所以CMS收集器不能像其余收集器那样等到老年代几乎彻底被填满了再进行收集,须要预留一部份内存空间提供并发收集时的程序运做使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也能够经过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以下降内存回收次数提升性能。要是CMS运行期间预留的内存没法知足程序其余线程须要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来从新进行老年代的垃圾收集,这样停顿时间就很长了。因此说参数-XX:CMSInitiatingOccupancyFraction设置的太高将会很容易致使“Concurrent Mode Failure”失败,性能反而下降。
c>.最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来不少麻烦,好比说大对象,内存空间找不到连续的空间来分配不得不提早触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC以后增长一个碎片整理过程,还可经过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC以后,跟着来一次碎片整理过程。
五、G1收集器。相比CMS收集器有很多改进,首先基于标记-整理算法,不会产生内存碎片问题,其次,能够比较精确的控制停顿,此处再也不详细介绍。
六、Serial Old。Serial Old是Serial收集器的老年代版本,它一样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
七、Parallel Old。Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
八、RTSJ垃圾收集器,用于Java实时编程,后续会补充介绍。
3、Java程序性能优化
gc()的调用
调用gc 方法暗示着Java 虚拟机作了一些努力来回收未用对象,以便可以快速地重用这些对象当前占用的内存。当控制权从方法调用中返回时,虚拟机已经尽最大努力从全部丢弃的对象中回收了空间,调用System.gc() 等效于调用Runtime.getRuntime().gc()。
finalize()的调用及重写
gc 只能清除在堆上分配的内存(纯java语言的全部对象都在堆上使用new分配内存),而不能清除栈上分配的内存(当使用JNI技术时,可能会在栈上分配内存,例如java调用c程序,而该c程序使用malloc分配内存时)。所以,若是某些对象被分配了栈上的内存区域,那gc就管不着了,对栈上的对象进行内存回收就要靠finalize()。举个例子来讲,当java 调用非java方法时(这种方法多是c或是c++的),在非java代码内部也许调用了c的malloc()函数来分配内存,并且除非调用那个了 free() 不然不会释放内存(由于free()是c的函数),这个时候要进行释放内存的工做,gc是不起做用的,于是须要在finalize()内部的一个固有方法调用free()。
优秀的编程习惯
(1)避免在循环体中建立对象,即便该对象占用内存空间不大。
(2)尽可能及时使对象符合垃圾回收标准。
(3)不要采用过深的继承层次。
(4)访问本地变量优于访问类中的变量。
本版块会不断更新!
4、常见问题
一、内存溢出
就是你要求分配的java虚拟机内存超出了系统能给你的,系统不能知足需求,因而产生溢出。
二、内存泄漏
是指你向系统申请分配内存进行使用(new),但是使用完了之后却不归还(delete),结果你申请到的那块内存你本身也不能再访问,该块已分配出来的内存也没法再使用,随着服务器内存的不断消耗,而没法使用的内存愈来愈多,系统也不能再次将它分配给须要的程序,产生泄露。一直下去,程序也逐渐无内存使用,就会溢出。
本章内容以理论为主,后续我会不断地增长一些实际的操做,如验证垃圾回收效果、或者内存监测什么的,同时也但愿读者会不断给出指导、建议,若有任何问题,请联系:egg:
邮箱:xtfggef@gmail.com
微博:weibo.com/xtfggef
若有转载,请说明出处(http://blog.csdn.net/zhangerqing),谢谢!
The End