JVM结构、内存分配、垃圾回收算法、垃圾收集器

2020-11-04  本文已影响0人  squirrels

一.JVM主要包括四个部分:

2018110723575143.jpg

1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。(右图表示了从java源文件到JVM的整个过程,可配合理解
2.执行引擎:负责执行class文件中包含的字节码指令(执行引擎的工作机制,这里也不细说了,这里主要介绍JVM结构);
3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域,如图:

运行时内存区.jpg

4.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。

二、内存分配

我觉得了解垃圾回收之前,得先了解JVM是怎么分配内存的,然后识别哪些内存是垃圾需要回收,最后才是用什么方式回收。
Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。
java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。根据上面我们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。
总之Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题;

三、GC机制

随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。
在上面介绍的五个内存区域中,有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。
3.1 查找算法
经典的引用计数算法,每个对象添加到引用计数器,每被引用一次,计数器+1,失去引用,计数器-1,当计数器在一段时间内为0时,即认为该对象可以被回收了。但是这个算法有个明显的缺陷:当两个对象相互引用,但是二者都已经没有作用时,理应把它们都回收,但是由于它们相互引用,不符合垃圾回收的条件,所以就导致无法处理掉这一块内存区域。因此,Sun的JVM并没有采用这种算法,而是采用一个叫——根搜索算法,
基本思想是:从一个叫GC Roots的根节点出发,向下搜索,如果一个对象不能达到GC Roots的时候,说明该对象不再被引用,可以被回收。如上图中的Object5、Object6、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根本就没有持久代这一说法)。
新生代(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算法:复制、标记-清除和标记-压缩
复制:复制算法采用的方式为从根集合进行扫描,将存活的对象移动到一块空闲的区域,当存活的对象较少时,复制算法会比较高效(新生代的Eden区就是采用这种算法),其带来的成本是需要一块额外的空闲空间和对象的移动。
标记-清除:该算法采用的方式是从跟集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,并进行清除。
清除阶段清理的是没有被引用的对象,存活的对象被保留。
标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但由于只是清除,没有重新整理,因此会造成内存碎片。
标记-压缩:该算法与标记-清除算法类似,都是先对存活的对象进行标记,但是在清除后会把活的对象向左端空闲空间移动,然后再更新其引用对象的指针

四、垃圾收集器

在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实时编程

上一篇下一篇

猜你喜欢

热点阅读