JVM内存模型及GC机制

一、JVM简介

1.1什么是JVM

  JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

1.2JVM原理
  (1)jvm是java的核心和基础,在java编译器和os平台之间的虚拟处理器,可在上面执行字节码程序。
  (2)java编译器只要面向jvm,生成jvm能理解的字节码文件。java源文件经编译成字节码程序,通过jvm将每条指令翻译成不同的机器码,通过特定平台运行。

1.3 JVM执行程序的过程
  (1) 加载.class文件

  (2) 管理并分配内存 

  (3) 执行垃圾收集

 

   如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

二、JVM内存模型

JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)

如图所示jvm内存划分为五部分

  方法区(Method Area)

  堆区(Heap)

  虚拟机栈(VM Stack)

  本地方法栈(Native Method Stack)

  程序计数器(Program Counter Register)

2.1堆区(Heap)

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

       在Hotspot虚拟机中,这块区域对应持久代(Permanent Generation),一般来说,方法区上执行GC的情况很少,因此方法区被称为持久代的原因之一,但这并不代表方法区上完全没有GC,其上的GC主要针对常量池的回收和已加载类的卸载。在方法区上进行GC,条件相当苛刻而且困难。

       运行时常量池(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的区域。

 三、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.2GC机制

  内存主要被分为三块:年轻代(Youn Generation)、年老代(Old Generation)、永久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同。

  

  年轻代(Young Generation)适合存放生命周期较短,快速创建和销毁的对象。分为Eden、from survivor(survivor 0)、to survivor(survivor 1)三个区,默认比例为8:1:1。新创建的对象都是从年轻代分配内存,年轻代今进行垃圾回收的时候会触发Minor GC(也称Young GC)。

  当Eden区内存不足时会触发Minor GC,会先把Eden区中存活的对象复制到to survivor中。然后再看from survivor中,如果有复制次数达到年老代标准的就复制到年老代中,复制次数没达到年老代标准的则复制到to survivor中。然后调换to survivor和from survivor的名字,保证每次to survivor都是空的等待对象复制到那里的。

  年老代(Old Generation)用于存放年轻代多次回收依旧存活的对象,如缓存对象。当年老代满了就会触发Major GC(也称Full GC)。

  永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,大多数JVM没有这一代。在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间。 永久代包含了JVM需要的应用元数据,这些元数据描述了在应用里使用的类和方法。注意,永久代不是Java堆内存的一部分。永久代存放JVM运行时使用的类。永久代同样包含了Java SE库的类和方法。永久代的对象在full GC时进行垃圾收集。

3.3GC算法

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

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

 

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

3.3.2标记——清除算法

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

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

 

  标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但由于只是清除,没有重新整理,因此会造成内存碎片。

3.3.3标记——压缩算法

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

由于进行了移动规整动作,该算法避免了标记-清除的碎片问题,但由于需要进行移动,因此成本也增加了。(该算法适用于年老代)

参考资料:https://blog.csdn.net/anjoyandroid/article/details/78609971

参考资料:https://blog.csdn.net/m0_37698652/article/details/79690656

参考资料:https://www.cnblogs.com/fubaizhaizhuren/p/4976839.html

参考资料:http://www.cnblogs.com/dolphin0520/p/3613043.html

猜你喜欢

转载自www.cnblogs.com/bulushengse/p/10494819.html