深刻理解JVM的内存结构及GC机制

1、前言

       JAVA GC(Garbage Collection,垃圾回收)机制是区别C++的一个重要特征,C++须要开发者本身实现垃圾回收的逻辑,而JAVA开发者则只须要专一于业务开发,由于垃圾回收这件繁琐的事情JVM已经为咱们代劳了,从这一点上来讲,JAVA仍是要作的比较完善一些。但这并不意味着咱们不用去理解GC机制的原理,由于若是不了解其原理,可能会引起内存泄漏、频繁GC致使应用卡顿,甚至出现OOM等问题,所以咱们须要深刻理解其原理,才能编写出高性能的应用程序,解决性能瓶颈。java

       想要理解GC的原理,咱们必须先理解JVM内存管理机制,由于这样咱们才能知道回收哪些对象、何时回收以及怎么回收。算法

2、JVM内存管理

       根据JVM规范,JVM把内存划分红了以下几个区域:编程

1.方法区(Method Area)
2.堆区(Heap)
3.虚拟机栈(VM Stack)
4.本地方法栈(Native Method Stack)
5.程序计数器(Program Counter Register)复制代码

image.png
image.png

       其中,方法区和堆全部线程共享。数组

2.1 方法区(Method Area)

       方法区存放了要加载的类的信息(如类名、修饰符等)、静态变量、构造函数、final定义的常量、类中的字段和方法等信息。方法区是全局共享的,在必定条件下也会被GC。当方法区超过它容许的大小时,就会抛出OutOfMemory:PermGen Space异常。缓存

       在Hotspot虚拟机中,这块区域对应持久代(Permanent Generation),通常来讲,方法区上执行GC的状况不多,所以方法区被称为持久代的缘由之一,但这并不表明方法区上彻底没有GC,其上的GC主要针对常量池的回收和已加载类的卸载。在方法区上进行GC,条件至关苛刻并且困难。bash

       运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译器生成的常量和引用。通常来讲,常量的分配在编译时就能肯定,但也不全是,也能够存储在运行时期产生的常量。好比String类的intern()方法,做用是String类维护了一个常量池,若是调用的字符"hello"已经在常量池中,则直接返回常量池中的地址,不然新建一个常量加入池中,并返回地址。多线程

2.2 堆区(Heap)

       堆区是GC最频繁的,也是理解GC机制最重要的区域。堆区由全部线程共享,在虚拟机启动时建立。堆区主要用于存放对象实例及数组,全部new出来的对象都存储在该区域。架构

2.3 虚拟机栈(VM Stack)

       虚拟机栈占用的是操做系统内存,每一个线程对应一个虚拟机栈,它是线程私有的,生命周期和线程同样,每一个方法被执行时产生一个栈帧(Statck Frame),栈帧用于存储局部变量表、动态连接、操做数和方法出口等信息,当方法被调用时,栈帧入栈,当方法调用结束时,栈帧出栈。并发

       局部变量表中存储着方法相关的局部变量,包括各类基本数据类型及对象的引用地址等,所以他有个特色:内存空间能够在编译期间就肯定,运行时再也不改变。函数

       虚拟机栈定义了两种异常类型StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)。若是线程调用的栈深度大于虚拟机容许的最大深度,则抛出StackOverFlowError;不过大多数虚拟机都容许动态扩展虚拟机栈的大小,因此线程能够一直申请栈,直到内存不足时,抛出OutOfMemoryError。

2.4 本地方法栈(Native Method Stack)

       本地方法栈用于支持native方法的执行,存储了每一个native方法的执行状态。本地方法栈和虚拟机栈他们的运行机制一致,惟一的区别是,虚拟机栈执行Java方法,本地方法栈执行native方法。在不少虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一块儿使用。

2.5 程序计数器(Program Counter Register)

       程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,程序猿没法操做它,它的做用是:JVM在解释字节码(.class)文件时,存储当前线程执行的字节码行号,只是一种概念模型,各类JVM所采用的方式不同。字节码解释器工做时,就是经过改变程序计数器的值来取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术区完成的。

       每一个程序计数器只能记录一个线程的行号,所以它是线程私有的。

       若是程序当前正在执行的是一个java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址,若是执行的是native方法,则计数器的值为空,此内存区是惟一不会抛出OutOfMemoryError的区域。

3、GC机制

       随着程序的运行,内存中的实例对象、变量等占据的内存愈来愈多,若是不及时进行回收,会下降程序运行效率,甚至引起系统异常。

       在上面介绍的五个内存区域中,有3个是不须要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。由于他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。因此,只有方法区和堆区须要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。

3.1 查找算法

        经典的引用计数算法,每一个对象添加到引用计数器,每被引用一次,计数器+1,失去引用,计数器-1,当计数器在一段时间内为0时,即认为该对象能够被回收了。可是这个算法有个明显的缺陷:当两个对象相互引用,可是两者都已经没有做用时,理应把它们都回收,可是因为它们相互引用,不符合垃圾回收的条件,因此就致使没法处理掉这一块内存区域。所以,Sun的JVM并无采用这种算法,而是采用一个叫——根搜索算法,如图:

image.png
image.png

       基本思想是:从一个叫GC Roots的根节点出发,向下搜索,若是一个对象不能达到GC Roots的时候,说明该对象再也不被引用,能够被回收。如上图中的Object五、Object六、Object7,虽然它们三个依然相互引用,可是它们其实已经没有做用了,这样就解决了引用计数算法的缺陷。

       补充概念,在JDK1.2以后引入了四个概念:强引用、软引用、弱引用、虚引用
       强引用:new出来的对象都是强引用,GC不管如何都不会回收,即便抛出OOM异常。
       软引用:只有当JVM内存不足时才会被回收。
       弱引用:只要GC,就会立马回收,无论内存是否充足。
       虚引用:能够忽略不计,JVM彻底不会在意虚引用,你能够理解为它是来凑数的,凑够"四大天王"。它惟一的做用就是作一些跟踪记录,辅助finalize函数的使用。

       最后总结,什么样的类须要被回收:

a.该类的全部实例都已经被回收;
b.加载该类的ClassLoad已经被回收;
c.该类对应的反射类java.lang.Class对象没有被任何地方引用。复制代码

3.2 内存分区

       内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)。三代的特色不一样,造就了他们使用的GC算法不一样,新生代适合生命周期较短,快速建立和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。

image.png
image.png

       新生代(Youn Generation):大体分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是重新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会出发Minor GC(也称做Youn GC)。

       旧生代(Old Generation):旧生代用于存放新生代屡次回收依然存活的对象,如缓存对象。当旧生代满了的时候就须要对旧生代进行回收,旧生代的垃圾回收称做Major GC(也称做Full GC)。

       持久代(Permanent Generation):在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。

3.3 GC算法

       常见的GC算法复制、标记-清除和标记-压缩

       复制:复制算法采用的方式为从根集合进行扫描,将存活的对象移动到一块空闲的区域,如图所示:

image.png
image.png

当存活的对象较少时,复制算法会比较高效(新生代的Eden区就是采用这种算法),其带来的成本是须要一块额外的空闲空间和对象的移动。

       标记-清除:该算法采用的方式是从跟集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,并进行清除。标记和清除的过程以下:

image.png
image.png

上图中蓝色部分是有被引用的对象,褐色部分是没有被引用的对象。在Marking阶段,须要进行全盘扫描,这个过程是比较耗时的。

image.png
image.png

清除阶段清理的是没有被引用的对象,存活的对象被保留。

标记-清除动做不须要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但因为只是清除,没有从新整理,所以会形成内存碎片。

       标记-压缩:该算法与标记-清除算法相似,都是先对存活的对象进行标记,可是在清除后会把活的对象向左端空闲空间移动,而后再更新其引用对象的指针,以下图所示

image.png
image.png

因为进行了移动规整动做,该算法避免了标记-清除的碎片问题,但因为须要进行移动,所以成本也增长了。(该算法适用于旧生代)

4、垃圾收集器

       在JVM中,GC是由垃圾回收器来执行,因此,在实际应用场景中,咱们须要选择合适的垃圾收集器,下面咱们介绍一下垃圾收集器。

4.1 串行收集器(Serial GC)

       Serial GC是最古老也是最基本的收集器,可是如今依然普遍使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的默认配置。比较适合于只有一个处理器的系统。在串行处理器中minor和major GC过程都是用一个线程进行回收的。它的最大特色是在进行垃圾回收时,须要对全部正在执行的线程暂停(stop the world),对于有些应用是难以接受的,可是若是应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒以内,大多数应用仍是能够接受的,并且事实上,它并无让咱们失望,几十毫秒的停顿,对于咱们客户机是彻底能够接受的,该收集器适用于单CPU、新生代空间较小且对暂停时间要求不是特别高的应用上,是client级别的默认GC方式。

4.2 ParNew GC

       基本和Serial GC同样,但本质区别是加入了多线程机制,提升了效率,这样它就能够被用于服务端上(server),同时它能够与CMS GC配合,因此,更加有理由将他用于server端。

4.3 Parallel Scavenge GC

       在整个扫描和复制过程采用多线程的方式进行,适用于多CPU、对暂停时间要求较短的应用,是server级别的默认GC方式。

4.4 CMS (Concurrent Mark Sweep)收集器

       该收集器的目标是解决Serial GC停顿的问题,以达到最短回收时间。常见的B/S架构的应用就适合这种收集器,由于其高并发、高响应的特色,CMS是基于标记-清楚算法实现的。

CMS收集器的优势:并发收集、低停顿,但远没有达到完美;

CMS收集器的缺点:

a.CMS收集器对CPU资源很是敏感,在并发阶段虽然不会致使用户停顿,可是会占用CPU资源而致使应用程序变慢,总吞吐量降低。
b.CMS收集器没法处理浮动垃圾,可能出现“Concurrnet Mode Failure”,失败而致使另外一次的Full GC。
c.CMS收集器是基于标记-清除算法的实现,所以也会产生碎片。复制代码

4.5 G1收集器

       相比CMS收集器有很多改进,首先,基于标记-压缩算法,不会产生内存碎片,其次能够比较精确的控制停顿。

4.6 Serial Old收集器

       Serial Old是Serial收集器的老年代版本,它一样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

4.7 Parallel Old收集器

       Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

4.8 RTSJ垃圾收集器

       RTSJ垃圾收集器,用于Java实时编程。

5、总结

       深刻理解JVM的内存模型和GC机制有助于帮助咱们编写高性能代码和提供代码优化的思路与方向。

相关文章
相关标签/搜索