zoukankan      html  css  js  c++  java
  • JVM总结

    ref:https://blog.csdn.net/ithomer/article/details/6252552

         http://gityuan.com/2016/01/09/java-memory/

         https://www.cnblogs.com/xing901022/p/7725961.html

       https://blog.csdn.net/u012152619/article/details/46968883

         http://www.cnblogs.com/ityouknow/p/5603287.html

         https://blog.csdn.net/boyupeng/article/details/47951037

         http://www.cnblogs.com/ityouknow/p/5603287.html

         https://www.cnblogs.com/qiuyong/p/6407418.html

       https://blog.csdn.net/seu_calvin/article/details/51892567

         https://www.cnblogs.com/ityouknow/p/5614961.html

    一、Java内存模型

      按照官方的说法:Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。

      JVM主要管理两种类型内存:堆和非堆,堆内存(Heap Memory)是在 Java 虚拟机启动时创建,非堆内存(Non-heap Memory)是在JVM堆之外的内存。

      简单来说,堆是Java代码可及的内存,留给开发人员使用的;非堆是JVM留给自己用的,包含方法区、JVM内部处理或优化所需的内存(如 JIT Compiler,Just-in-time Compiler,即时编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。

      Java内存模型,往往是指Java程序在运行时内存的模型,而Java代码是运行在Java虚拟机之上的,由Java虚拟机通过解释执行(解释器)或编译执行(即时编译器)来完成,故Java内存模型,也就是指Java虚拟机的运行时内存模型。

      作为Java开发人员来说,并不需要像C/C++开发人员,需要时刻注意内存的释放,而是全权交给虚拟机去管理,那么有就必要了解虚拟机的运行时内存是如何构成的。运行时内存模型,分为线程私有和共享数据区两大类,其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含Java堆、方法区,在方法区内有一个常量池。

    图1 运行时数据区域

      (1)线程私有区:

    • 程序计数器,记录正在执行的虚拟机字节码的地址;
    • 虚拟机栈:方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧;
    • 本地方法栈:虚拟机的Native方法执行的内存区。

      (2)线程共享区:

    • Java堆:对象分配内存的区域;
    • 方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据;
    • 常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。

      对于大多数的程序员来说,Java内存比较流行的说法便是堆和栈,这其实是非常粗略的一种划分,这种划分的”堆”对应内存模型的Java堆,”栈”是指虚拟机栈,然而Java内存模型远比这更复杂,想深入了解Java的内存,还是有必要明白整个内存模型。

      这几个存储区最主要的就是栈区和堆区,那么什么是栈?什么是堆呢?说的简单点,栈里面存放的是基本的数据类型和引用,而堆里面则是存放各种对象实例的。

    图2 栈和堆示意图

      堆与栈分开设计是为什么呢?

    • 栈存储了处理逻辑、堆存储了具体的数据,这样隔离设计更为清晰
    • 堆与栈分离,使得堆可以被多个栈共享。
    • 栈保存了上下文的信息,因此只能向上增长;而堆是动态分配

      栈的大小可以通过-XSs设置,如果不足的话,会引起java.lang.StackOverflowError的异常

      栈区

      线程私有,生命周期与线程相同。每个方法执行的时候都会创建一个栈帧(stack frame)用于存放局部变量表、操作栈、动态链接、方法出口。

      堆

      存放对象实例,所有的对象的内存都在这里分配。垃圾回收主要就是作用于这里的。

    • 堆的内存由-Xms指定,默认是物理内存的1/64;最大的内存由-Xmx指定,默认是物理内存的1/4。
    • 默认空余的堆内存小于40%时,就会增大,直到-Xmx设置的内存。具体的比例可以由-XX:MinHeapFreeRatio指定
    • 空余的内存大于70%时,就会减少内存,直到-Xms设置的大小。具体由-XX:MaxHeapFreeRatio指定。

      因此一般都建议把这两个参数设置成一样大,可以避免JVM在不断调整大小。

    二、 详细模型

      运行时内存分为五大块区域(常量池属于方法区,算作一块区域),前面简要介绍了每个区域的功能,那接下来再详细说明每个区域的内容。

    图3 jvm内存总体结构图

       程序计数器PC

      程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 

      当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)。 

      此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

       虚拟机栈(参考文章

      线程私有,生命周期与线程相同,是Java方法执行的内存模型。每个方法(不包含native方法)执行的时候都会创建一个栈帧(stack frame)用于存放局部变量表、操作栈、动态链接、方法出口。方法执行过程,对应着虚拟机栈的入栈到出栈的过程。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。

      栈帧(Stack Frame)结构

      栈帧是用于支持虚拟机进行方法执行的数据结构,是属性运行时数据区的虚拟机栈的栈元素。见上图, 栈帧包括:

      局部变量表 (locals大小,编译期确定),一组变量存储空间, 容量以slot为最小单位。

      一、操作栈(stack大小,编译期确定),操作栈元素的数据类型必须与字节码指令序列严格匹配。

      二、动态连接, 指向运行时常量池中该栈帧所属方法的引用,为了动态连接使用。

        1、前面的解析过程其实是静态解析;

        2、对于运行期转化为直接引用,称为动态解析。

      三、方法返回地址

        1、正常退出,执行引擎遇到方法返回的字节码,将返回值传递给调用者

        2、异常退出,遇到Exception,并且方法未捕捉异常,那么不会有任何返回值。

      四、额外附加信息,虚拟机规范没有明确规定,由具体虚拟机实现。

      因此,一个栈帧的大小不会受到。

      活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。  

    图4 栈针结构说明

      本地方法栈

      本地方法栈则为虚拟机使用到的Native方法提供内存空间,而前面讲的虚拟机栈式为Java方法提供内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如非常典型的Sun HotSpot虚拟机。

      异常(Exception):Java虚拟机规范规定该区域可抛出StackOverFlowError和OutOfMemoryError。

        Java堆

      Java堆,线程共享的,在虚拟机启动时创建,是Java虚拟机管理的最大的一块内存,也是GC的主战场,里面存放的是几乎所有的对象实例和数组数据。生命周期与虚拟机相同,可以不使用连续的内存地址。
      垃圾回收的主要区域。根据分代收集算法可以分为新生代和老年代。JIT编译器有栈上分配、标量替换等优化技术的实现导致部分对象实例数据不存在Java堆,而是栈内存。

      1、从内存回收角度,Java堆被分为新生代和老年代;这样划分的好处是为了更快的回收内存;

      2、从内存分配角度,Java堆可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);这样划分的好处是为了更快的分配内存;

      存放对象实例,所有的对象的内存都在这里分配。垃圾回收主要就是作用于这里的。

      1、堆的内存由-Xms指定,默认是物理内存的1/64;最大的内存由-Xmx指定,默认是物理内存的1/4。

      2、默认空余的堆内存小于40%时,就会增大,直到-Xmx设置的内存。具体的比例可以由-XX:MinHeapFreeRatio指定。

      3、空余的内存大于70%时,就会减少内存,直到-Xms设置的大小。具体由-XX:MaxHeapFreeRatio指定。

      因此一般都建议把这两个参数设置成一样大,可以避免JVM在不断调整大小。  

      如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

      对象创建的过程是在堆上分配着实例对象,那么对象实例的具体结构如下:

    图5 对象实例结构图

      对于填充数据不是一定存在的,仅仅是为了字节对齐。HotSpot VM的自动内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例数据不是8的倍数,便需要填充数据来保证8字节的对齐。该功能类似于高速缓存行的对齐。

      另外,关于在堆上内存分配是并发进行的,虚拟机采用CAS加失败重试保证原子操作,或者是采用每个线程预先分配TLAB内存。

      方法区

      方法区主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。GC在该区域出现的比较少。

      方法区在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。

      简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化。而装载后的结果就是由.class文件转变为方法区中的一段特定的数据结构。这个数据结构会存储如下信息:

       类型信息

            这个类型的全限定名

            这个类型的直接超类的全限定名

            这个类型是类类型还是接口类型

            这个类型的访问修饰符

            任何直接超接口的全限定名的有序列表 

      字段信息

            字段名

            字段类型

            字段的修饰符 

      方法信息

            方法名

            方法返回类型

            方法参数的数量和类型(按照顺序)

            方法的修饰符 

      其他信息

            除了常量以外的所有类(静态)变量

            一个指向ClassLoader的指针

            一个指向Class对象的指针

            常量池(常量数据以及对其他类型的符号引用) 

      JVM为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,integer,和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。 

      每个类的这些元数据,无论是在构建这个类的实例还是调用这个类某个对象的方法,都会访问方法区的这些元数据。

      构建一个对象时,JVM会在堆中给对象分配空间,这些空间用来存储当前对象实例属性以及其父类的实例属性(而这些属性信息都是从方法区获得),注意,这里并不是仅仅为当前对象的实例属性分配空间,还需要给父类的实例属性分配,到此其实我们就可以回答第一个问题了,即实例化父类的某个子类时,JVM也会同时构建父类的一个对象。从另外一个角度也可以印证这个问题:调用当前类的构造方法时,首先会调用其父类的构造方法直到Object,而构造方法的调用意味着实例的创建,所以子类实例化时,父类肯定也会被实例化。

      类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在JVM使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。 

      方法区主要有以下几个特点:

      1、方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待。

      2、方法区的大小不必是固定的,JVM可根据应用需要动态调整。同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是JVM自己的堆)中自由分配。

      3、方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集。 

      可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。

      对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(PermanentGeneration),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。

      相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

      当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

      总结

    图6 总结图

       内存溢出和内存泄漏

      内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

      内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

      memory leak会最终会导致out ofmemory。 

      Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。

      要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

      如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。

      如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。 

      内存分配过程

      1、JVM 会试图为相关Java对象在Eden Space中初始化一块内存区域。

      2、当Eden空间足够时,内存申请结束;否则到下一步。

      3、JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收)。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。

      4、Survivor区被用来作为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区。

      5、当Old区空间不够时,JVM 会在Old区进行完全的垃圾收集(0级)。

      6、完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现“outofmemory”错误。 

      对象访问

      对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码: 

      Object obj = new Object();

       假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

      由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。 

      如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

      实际情况中,大部分都是采用直接指针方式进行数据访问。

      第一种:直接指针访问

    图7 直接指针访问示意图

       第二种:句柄访问 

    图8 句柄访问示意图

    二、类加载机制(类加载过程和类加载器)

    • 为什么要使用类加载器?

      Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性。例如:

      1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类;
      2.用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分;(这个是Android插件化,动态安装更新apk的基础)

    • 什么是类的加载?

      类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

    • 类加载过程

    图9 类加载流程图

       其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

      加载:查找并加载类的二进制数据

         加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

          1、通过一个类的全限定名来获取其定义的二进制字节流。

          2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

          3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

      相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

      加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

      连接

       – 验证:确保被加载的类的正确性

      验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

      文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

      元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

      字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

      符号引用验证:确保解析动作能正确执行。

      验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

      – 准备:为类的静态变量分配内存,并将其初始化为默认值

      准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

      1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

      2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

      假设一个类变量的定义为:public static int value = 3;

      那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。 

      这里还需要注意如下几点:

      1>对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。

      2>对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。

      3>对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。

      4>如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

       3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

         假设上面的类变量value被定义为: public static final int value = 3;

      编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。

      – 解析:把类中的符号引用转换为直接引用

      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

      直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

      初始化

       初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

        ①声明类变量是指定初始值

        ②使用静态代码块为类变量指定初始值

       JVM初始化步骤

       1、假如这个类还没有被加载和连接,则程序先加载并连接该类

       2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

       3、假如类中有初始化语句,则系统依次执行这些初始化语句

      类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

      – 创建类的实例,也就是new的方式

      – 访问某个类或接口的静态变量,或者对该静态变量赋值

      – 调用类的静态方法

      – 反射(如Class.forName(“com.shengsiyuan.Test”))

      – 初始化某个类的子类,则其父类也会被初始化

      – Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类 

      结束生命周期

      在如下几种情况下,Java虚拟机将结束生命周期

      – 执行了System.exit()方法

      – 程序正常执行结束

      – 程序在执行过程中遇到了异常或错误而异常终止

      – 由于操作系统出现错误而导致Java虚拟机进程终止

      使用

      正常使用。

      卸载

      GC把无用对象从内存中卸载。

    • 类加载器

      JVM设计者把类加载阶段中的“通过'类全名'来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

      1、类与类加载器

      对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。

      2、双亲委派模型

      从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。

      从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:
      1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
      2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。
      3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

      我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示:

    图10 类加载器之间的关系图 

      如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。

      3、JVM类加载机制

      1>全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入;

      2>父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类;

      3>缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

      4、类的加载

      类加载有三种方式:

      1、命令行启动应用时候由JVM初始化加载

      2、通过Class.forName()方法动态加载

      3、通过ClassLoader.loadClass()方法动态加载

      5、双亲委派模型

      双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

       6、自定义类加载器

      通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类。

    三、GC

      Java内存分配

      Java的内存管理实际上就是变量和对象的管理,其中包括对象的分配和释放。

    图11 Java内存分配图

      JVM内存申请过程如下:

      1、JVM 会试图为相关Java对象在Eden中初始化一块内存区域

      2、当Eden空间足够时,内存申请结束;否则到下一步

      3、JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区

      4、Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区

      5、当OLD区空间不够时,JVM 会在OLD区进行完全的垃圾收集(0级)

      6、完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory”错误

      GC概述  

      垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了。

      jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

      GC基本原理

      GC(Garbage Collection),是JAVA/.NET中的垃圾收集器。

      Java是由C++发展来的,它摈弃了C++中一些繁琐容易出错的东西,引入了计数器的概念,其中有一条就是这个GC机制(C#借鉴了JAVA)

      编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。所以,Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。

      对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为"不可达的".GC将负责回收所有"不可达"对象的内存空间。

      对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。但是,为了保证 GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。因此,不同的JVM的实现者往往有不同的实现算法。这也给Java程序员的开发带来许多不确定性。本文研究了几个与GC工作相关的问题,努力减少这种不确定性给Java程序带来的负面影响。

      增量式GC

      增量式GC(Incremental GC),是GC在JVM中通常是由一个或一组进程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CPU。

      当GC进程运行时,应用程序停止运行。因此,当GC运行时间较长时,用户能够感到Java程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低,这意味着还有很多应该回收的对象没有被回收,仍然占用大量内存。因此,在设计GC的时候,就必须在停顿时间和回收率之间进行权衡。一个好的GC实现允许用户定义自己所需要的设置,例如有些内存有限的设备,对内存的使用量非常敏感,希望GC能够准确的回收内存,它并不在意程序速度的快慢。另外一些实时网络游戏,就不能够允许程序有长时间的中断。

      增量式GC就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然,增量式GC在整体性能上可能不如普通GC的效率高,但是它能够减少程序的最长停顿时间。

      Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在运行Java程序时增加-Xincgc的参数。

      HotSpot JVM增量式GC的实现是采用Train GC算法,它的基本想法就是:将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁高和具有相关性的对象放在一队中,随着程序的运行,不断对组进行调整。当GC运行时,它总是先回收最老的(最近很少访问的)的对象,如果整组都为可回收对象,GC将整组回收。这样,每次GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。

      对象存活判断

      判断对象是否存活一般有两种方式:

      引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

      可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

      在Java语言中,GC Roots包括:

        虚拟机栈中引用的对象。

        方法区中类静态属性实体引用的对象。

        方法区中常量引用的对象。

        本地方法栈中JNI引用的对象。

      四种引用类型

      1、强引用

      Object obj = new Object();

      这里的obj引用便是一个强引用,强引用不会被GC回收。即使抛出OutOfMemoryError错误,使程序异常终止。 

      2、软引用

      如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。

      软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。 

      3、弱引用

      弱引用与软引用的区别在于:垃圾回收器一旦发现了弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些弱引用的对象。

      弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

      4 、虚引用

      虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器发现一个对象有虚引用时,首先执行所引用对象的finalize()方法,在回收内存之前,把这个虚引用对象加入到引用队列中,你可以通过判断引用队列中是否有该虚引用对象,来了解这个对象是否将要被垃圾回收。然后就可以利用虚引用机制完成对象回收前的一些工作。

      这里特别需要注意:当JVM将虚引用插入到引用队列的时候,虚引用执行的对象内存还是存在的。但是PhantomReference并没有暴露API返回对象。所以如果我想做清理工作,需要继承PhantomReference类,以便访问它指向的对象。  

      GC分代划分

      JVM内存模型中Heap区分两大块,一块是 Young Generation,另一块是Old Generation

    图12 虚拟机堆结构

      1) 在Young Generation中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from、to),它们的大小总是一样,它们用来存放每次垃圾回收后存活下来的对象。

      2) 在Old Generation中,主要存放应用程序中生命周期长的内存对象。

      3) 在Young Generation块中,垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个SurvivorSpace,当Survivor Space空间满了后,剩下的live对象就被直接拷贝到OldGeneration中去。因此,每次GC后,Eden内存块会被清空。

      4) 在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。

      5) 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

      垃圾收集算法

      标记 -清除算法

      “标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

      它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    图13 标记-清除算法示意图 

      复制算法

      “复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

      这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

    图14 复制算法示意图 

      标记-压缩算法

      复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

      根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 

    图15 标记-压缩算法示意图 

      分代收集算法

      GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

      “分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

     垃圾收集器

       如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。 

      Serial收集器

      串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)。

      参数控制:-XX:+UseSerialGC  串行收集器 

    图16 串行收集器示意图 

      ParNew收集器

      ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

      参数控制:-XX:+UseParNewGC  ParNew收集器

              -XX:ParallelGCThreads 限制线程数量 

    图17 ParNew收集器示意图 

      Parallel收集器

      Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩。

      参数控制:-XX:+UseParallelGC  使用Parallel收集器+ 老年代串行         

      Parallel Old 收集器

      Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供。

      参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行 

      CMS收集器

      CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

      从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括: 

      初始标记(CMS initial mark)

      并发标记(CMS concurrent mark)

      重新标记(CMS remark)

      并发清除(CMS concurrent sweep)

      其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。 
          由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

      优点:并发收集、低停顿 

      缺点:产生大量空间碎片、并发阶段会降低吞吐量

      参数控制:-XX:+UseConcMarkSweepGC  使用CMS收集器

              -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

                   -XX:+CMSFullGCsBeforeCompaction  设置进行几次Full GC后,进行一次碎片整理

                   -XX:ParallelCMSThreads  设定CMS的线程数量(一般情况约等于可用CPU数量) 

    图18 CMS收集器示意图         

      G1收集器

      G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

      1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

      2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

      上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

    图19 G1收集器示意图 

      G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

       收集步骤:

      1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)

      2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。

      3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

    图20 Concurrent Marking阶段示意图 

      4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

      5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。 

    图21 Copy/Clean up阶段示意图

      6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。 

    图22 复制/清除阶段示意图

     注:以上内容来源于博客,开篇已给出原博客链接。在此特别感谢!!! 

  • 相关阅读:
    Hibernate 持久化对象的状态
    Hibernate 主键生成策略
    Hibernate 环境搭建
    Struts2 UI标签
    Struts2 处理表单重复提交
    Struts2 模型驱动及页面回显
    Struts2 之 ognl
    Struts2 框架验证
    Struts2 手动验证
    Struts2 自定义拦截器
  • 原文地址:https://www.cnblogs.com/banjinbaijiu/p/9015035.html
Copyright © 2011-2022 走看看