JVM基本原理

一.JVM的内存划分

JVM的运行时数据区(Runtime Data Area)内存划分为五个区域,分别是:程序计数器,虚拟机栈,本地方法栈,堆,方法区

现在分别介绍这五个区域的功能与特点:

        程序计数器一个较小的内存空间,可以把程序计数器中的记录数看做是前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。它是虚拟机规范中唯一一个没有规定任何OutOfMemoryError异常的区域。

        虚拟机栈:虚拟机栈描述的是java方法执行时的内存模型。每个java方法执行时虚拟机栈内都会生成一个栈帧(Frame),这个栈帧中保存了该方法的局部变量表,方法出口,操作栈,动态链接等信息。每个方法被调用的过程其实就是对应一个栈帧入栈和出栈的过程。当一个栈帧申请的内存大小超过虚拟机栈允许的范围时会报出StackOverflowError,而虚拟机栈是可以动态扩展的,内存空间却是有限的,当其扩展到不能再扩展时便会报出OutOfMemoryError。    

        局部变量表主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类原始数据类型、对象引用(reference),以及returnAddress类型。局部变量表所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中。局部变量表存储的只是方法中局部变量的值,而成员变量的值是不存储在局部变量表中的,它存储在对象实例数据的内存空间中,一般来说是存储在java堆内存中的。简单来说,与线程上下文相关的数据都是存在java栈区的,反之则存储于java堆中。局部变量表可以看做是专门用于存储局部变量值的一种类似于线性表(Linear List)的数据结构。局部变量表的最小单元是变量槽(slot),一个Slot可以存储一个类型为boolean、byte、char、short、float、reference以及returnAddress小于或等于32bit的数值,2个Slot可以存储一个类型为long或double的64bit数值。JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,访问索引从0开始到小于局部变量表最大的Slot长度。JVM使用局部变量表来完成方法调用时的参数传递,当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上。访问索引为0的Slot一定存储的是与被调用实例方法相对应的对象引用(通过Java语法层面的“this”关键字便可访问到这个参数),而后续的其他方法参数和方法体内定义的成员变量则会按照顺序从局部变量表中索引为1的Slot位置处展开复制


        动态链接每个栈帧都有一个运行时常量池的引用(current class constant pool reference)。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。

           操作栈操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。

        本地方法栈:与虚拟机栈的功能类似,本地方法栈处理的是本地方法,这些方法由native修饰,可以由C实现,同样的它也会报出StackOverflowError和OutOfMemoryError异常。

      java堆:java堆一般是虚拟机管理的内存中最大的一块,它最大的作用就是存储新生成的对象实例。Java 是被所有线程共享的一块内存区域,在虚拟机启动时创建。GC也主要是对它进行操作。

        方法区也可以理解为永久代(仅适用于hotspot虚拟机,其他虚拟机不存在永久代这个概念),是各线程共享的内存区域,他主要存储了类信息,常量,静态变量以及即时编译器编译的代码。

        运行时常量池运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区。运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常运行时常量池中。

有三个概念要弄清楚:

常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。

字符串池/字符串常量池(String Pool/String Constant Pool)是常量池中的一部分,存储编译期类中产生的字符串类型数据。JDK1.6之前字符串常量池位于方法区之中。 JDK1.7字符串常量池已经被挪到堆之中

运行时常量池(Runtime Constant Pool)方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。

常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。

字面量:文本字符串、声明为final的常量值等;

符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符

JVM内存图解:



二,垃圾回收GC

(一)如何确定某个对象是“垃圾”?

引用计数法在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法

可达性分析:VM的垃圾回收机制中,判断一个对象是否死亡,并不是根据是否还有对象对其有引用,而是通过可达性分析。对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,从这些树根往下搜索,搜索走过的链称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明这个对象是不可用的,该对象会被判定为可回收的对象。

那么那些对象可作为GC Roots呢?主要有以下几种

1.虚拟机栈(栈帧中的本地变量表)中引用的对象。 
2.方法区中类静态属性引用的对象。 
3.方法区中常量引用的对象 
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。

另外,Java还提供了软引用和弱引用,这两个引用是可以随时被虚拟机回收的对象,我们将一些比较占内存但是又可能后面用的对象,比如Bitmap对象,可以声明为软引用或弱引用。但是注意一点,每次使用这个对象时候,需要显示判断一下是否为null,以免出错。

(二)典型的垃圾回收算法

由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

1.Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:


        从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。



  2.Copying(复制)算法

  为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:


        这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

     很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。


        3.Mark-Compact(标记-整理)算法

  为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:



    4.Generational Collection(分代收集)算法

  分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

  目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,比例一般为8:1:1,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor1中还存活的对象复制到另一块Survivor2空间中,然后清理掉Eden和Survivor1空间。之后使用Eden和保存对象的survivor2,将刚清理的survivor1当做空闲空间,再次进行GC操作时将存活的对象复制到空闲的survivor1中,然后清理Eden和survivor2,如此反复,这也是为什么要设置两个survivor的原因。


  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

      对新生代的GC算法又叫做minorGC,对老年代的回收算法又叫做majorGC。

  注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

(三)典型的垃圾回收器

垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。

1.Serial/Serial Old

  Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿


  2.ParNew

  ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。


  3.Parallel Scavenge

  Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

  4.Parallel Old

  Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法

  5.CMS

  CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

初始标记(initial mark) stop-the-world,标记gc roots 直接关联对象。

并发标记(concurrent mark)trace gc roots

重新标记(remark) stop-the-world

并发清除(concurrent sweep)

优点:并发收集,低停顿; 

缺点:大量碎片;并发阶段;



          6.G1

   G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

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

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

内存分为多个大小相等的独立区域(Region)

收集步骤:

  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)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。 
  4. Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

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

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

(四)关于垃圾回收的前辈总结:

        从上面的推导可以得出很多结论,下面是前辈的经验总结与自已的认识


        1.JVM堆的大小决定了GC的运行时间。如果JVM堆的大小超过一定的限度,那么GC的运行时间会很长。
        2.对象生存的时间越长,GC需要的回收时间也越长,影响了回收速度。
       3.大多数对象都是短命的,所以,如果能让这些对象的生存期在GC的一次运行周期内,wonderful!
        4.应用程序中,建立与释放对象的速度决定了垃圾收集的频率。
        5.如果GC一次运行周期超过3-5秒,这会很影响应用程序的运行,如果可以,应该减少JVM堆的大小了。

        6.前辈经验之谈:通常情况下,JVM堆的大小应为物理内存的80%。



三,JVM类加载系统

            



从这个图中可以看到,JVM 是运行在操作系统之上的,它与硬件没有直接的交互。我们再来看下JVM 有哪些组成部分,如下图所示:



Class Loader 类加载器

        类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java 程序,然后通过javac 编译成class 文件,那怎么才能加载到内存中被执行呢?Class Loader 承担的就是这个责任,那不可能随便建立一个.class 文件就能被加载的,Class Loader 加载的class 文件是有格式要求,在《JVM Specification 》中式这样定义Class 文件的结构:

    ClassFile {

      u4 magic;

      u2 minor_version;

       u2 major_version;

      u2 constant_pool_count;

      cp_info constant_pool[constant_pool_count-1];

      u2 access_flags;

      u2 this_class;

      u2 super_class;

      u2 interfaces_count;

      u2 interfaces[interfaces_count];

      u2 fields_count;

      field_info fields[fields_count];

      u2 methods_count;

      method_info methods[methods_count];

      u2 attributes_count;

      attribute_info attributes[attributes_count];

    }

        需要详细了解的话,可以仔细阅读《JVM Specification 》的第四章“The class File Format ”,这里不再详细说明。

        友情提示:Class Loader 只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由Execution Engine 负责的。

Execution Engine 执行引擎

        执行引擎也叫做解释器(Interpreter) ,负责解释命令,提交操作系统执行。

Native Interface 本地接口

        本地接口的作用是融合不同的编程语言为Java 所用,它的初衷是融合C/C++ 程序,Java 诞生的时候是C/C++ 横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++ 程序,于是就在内存中专门开辟了一块区域处理标记为native 的代码,它的具体做法是Native Method Stack 中登记native 方法,在Execution Engine 执行时加载native libraies 。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java 程序驱动打印机,或者Java 系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket 通信,也可以使用Web Service 等等,不多做介绍。

Runtime data area 运行数据区

        运行数据区是整个JVM 的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java 生态系统如此的繁荣,得益于该区域的优良自治,上面已经介绍过。

        整个JVM 框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行,瞧,一个完整的系统诞生了!

JVM加载class文件的原理机制 

        1.Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中 

        2.java中的类大致分为三种: 

        a.系统类 

        b.扩展类 

        c.由程序员自定义的类 

        3.类装载方式,有两种 

   a.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中

            b.显式装载, 通过class.forname()等方法,显式加载需要的类 

        隐式加载与显式加载的区别:

4.类加载的动态性体现 

        一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现 。

        5.java类装载器 

       Java中的类装载器实质上也是类,功能是把类载入jvm中,值得注意的是jvm的类装载器并不是一个,而是三个,层次结构如下: 

      Bootstrap Loader  - 负责加载系统类 

            | 

          - - ExtClassLoader  - 负责加载扩展类 

                          | 

                      - - AppClassLoader  - 负责加载应用类 

      为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型,下面会谈到该模型 



  1. Bootstrap ClassLoader,主要加载JVM自身工作需要的类。
  2. Extension ClassLoader,主要加载%JAVA_HOME%\lib\ext目录下的库类。
  3. Application ClassLoader,主要加载Classpath指定的库类,一般情况下这是程序中的默认类加载器,也是ClassLoader.getSystemClassLoader() 的返回值。(这里的Classpath默认指的是环境变量中配置的Classpath,但是可以在执行Java命令的时候使用-cp 参数来修改当前程序使用的Classpath)

       注意:Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体。

6.双亲委托模型

JVM加载类的实现方式,我们称为 双亲委托模型

如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委托给自己的父加载器,每一层的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的Bootstrap ClassLoader中,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己加载。



  1. 自定义ClassLoader向自己的上层(Application ClassLoader)请求
  2. Application ClassLoader继续向上层(Extension ClassLoader)请求
  3. Extension ClassLoader继续向上层(Bootstrap ClassLoader)请求
  4. Bootstrap ClassLoader是最上层类加载器,所以它尝试在自己的路径中查找要加载类,如果查找到了就加载类,否则向下层(Extension ClassLoader)反馈自己无法加载类。
  5. Extension ClassLoader从自己的路径中寻找要加载类,找到则加载,找不到则继续向下返回。
  6. Application ClassLoader从自己的路径中寻找要加载类,找到则加载,找不到则继续向下返回。
  7. 自定义ClassLoader从自己的路径中寻找要加载类,找到则加载。由于类加载请求是自定义ClassLoader发起的,所以当它自己也找不到要加载的类时会终止加载并抛出 
    ClassNotFoundException。
7. 预先加载与依需求加载 

        Java 运行环境为了优化系统,提高程序的执行速度,在 JRE 运行的开始会将 Java 运行所需要的基本类采用预先加载( pre-loading )的方法全部加载要内存当中,因为这些单元在 Java 程序运行的过程当中经常要使用的,主要包括 JRE 的 rt.jar 文件里面所有的 .class 文件。 

        当 java.exe 虚拟机开始运行以后,它会找到安装在机器上的 JRE 环境,然后把控制权交给 JRE , JRE 的类加载器会将 lib 目录下的 rt.jar 基础类别文件库加载进内存,这些文件是 Java 程序执行所必须的,所以系统在开始就将这些文件加载,避免以后的多次 IO 操作,从而提高程序执行效率。 
 

       相对于预先加载,我们在程序中需要使用自己定义的类的时候就要使用依需求加载方法( load-on-demand ),就是在 Java 程序需要用到的时候再加载,以减少内存的消耗,因为 Java 语言的设计初衷就是面向嵌入式领域的。 

8. 自定义类加载机制 

        利用 Java 提供的 java.net.URLClassLoader 类就可以实现。

9. 类加载器的阶层体系 

        讨论了这么多以后,接下来我们仔细研究一下 Java 的类加载器的工作原理: 

        当执行 java ***.class 的时候, java.exe 会帮助我们找到 JRE ,接着找到位于 JRE 内部的 jvm.dll ,这才是真正的 Java 虚拟机器 , 最后加载动态库,激活 Java 虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰写而成,这个 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的 ExtClassLoader ,并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoader 。然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。这里要请大家注意的是, Launcher$ExtClassLoader.class 与 Launcher$AppClassLoader.class 都是由 Bootstrap Loader 所加载,所以 Parent 和由哪个类加载器加载没有关系。 

        下面的图形可以表示三者之间的关系: 

        BootstrapLoader <---(Extends)---- ExtClassLoader<---(Extends)---- AppClassLoader

        这三个加载器就构成我们的 Java 类加载体系。


参考资料:

        http://www.cnblogs.com/dingyingsi/p/3760447.html

        http://www.cnblogs.com/dolphin0520/p/3783345.html

        http://blog.csdn.net/hui_yan2012/article/details/70194449

        http://blog.csdn.net/gfangxiong/article/details/7425563

        http://blog.csdn.net/markzy/article/details/53192993

          http://blog.csdn.net/jiafu1115/article/details/7024323

              注:引用资料过多,如未在此给出引用资料原链接还望原作者海涵,文中有不对之处还请指出,十分感谢。

                         另外  欢迎转载!

猜你喜欢

转载自blog.csdn.net/lintiyan/article/details/79558453
今日推荐