Java教程

Java JVM学习笔记

本文主要是介绍Java JVM学习笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

JVM

  • 基本概念:JVM是可运行Java代码的假象计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方法域。JVM是运行在操作系统之上的它与硬件没有直接的交互。

  • Java代码的执行:

    • java代码编译为class-javac:Java源文件通过编译期产生相应的.Class文件(Java到JVM的中间字节码)
    • 装载class-ClassLoader:将class文件装载到虚拟机(JVM)中
    • 执行class:虚拟机(JVM)将字节码文件通过解释器编译成机器上的机器码。(解释执行的方式)
      • 解释执行
      • 编译执行(Java引入了JIT,当JIT第一次编译完成后就会将机器码保存下来,下次可以直接使用。这种方式属于编译执行
        • client compiler
        • server compiler
    • 每一种平台的解释器是不同的但是实现的虚拟机是相同的。
  • 当一个程序开始运行,虚拟机就实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭虚拟机就会消亡,多个虚拟机之间数据不能共享。

JVM线程

  • JVM允许一个应用并发执行多个线程,每个线程都是虚拟机执行过程中的一个线程实体。Hotspot JVM中的Java线程与原生操作系统有直接的映射关系。

  • 操作系统负责调度所有线程,并把它们分配到任何可以使用的cpu上。当原生线程初始化完毕,就会调用Java run()方法。当线程结束时,就会释放原生线程和Java线程的所有资源。

    • 当线程本地存储,缓冲区分配,同步对象,栈,程序计数器等准备好以后,就会创建一个操作系统原生线程。
    • Java线程结束,原生线程随之被回收。

Hotspot后台运行线程

线程 说明
虚拟机线程(VM thread) 这个线程等待JVM到达安全点操作出现。这些操作必须要在独立的线程中执行,因为当堆修改无法进行时,线程都需要JVM位于安全点。这些操作有:stop-the-world垃圾回收,线程栈dump,线程暂停,线程偏向锁(biased blocking)解除。
周期性任务线程 这线程负责定时器事件(也就是中断),用来调度周期性操作的进行。
GC线程 这些线程支持JVM中不同的垃圾回收活动。
编译期线程 这些线程在JVM运行期间,将字节码动态编译为平台相关的机器码。
信号分发线程 这个线程在接收发送到JVM的信号并调用适当的JVM方法处理。

JVM内存区域

JVM中线程主要分为线程私有区域【程序计数器,虚拟机栈,本地方法区】,线程共享区【JAVA堆,方法区】,直接内存。

直接内存不是Java运行时数据区的一部分不归JVM管理,在NIO中存在DirectByteBuffer可以使用堆外内存,避免了从内核空间(Native堆)到用户空间(Java堆)的频繁复制

  • 线程私有数据区域声明周期与线程相同,依赖用户线程的启动/结束而创建/销毁(Hotspot中这部分内存区域和本地线程生死对应)
  • 线程共享区域随着虚拟机的启动和关闭而创建和销毁

程序计数器(线程私有)

是一块较小的内存区域,是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型中,字节码解释器的工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器完成。(参考计算机中的pc指针)

由于Jaava虚拟机的多线程是通过多线程轮流切换,分配处理器执行时间的方式来实现,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换的正确进行,每条线程都需要一个独立的程序计数器,每个计数器之间互不影响,独立储存(这类内存区域被称为线程私有的区域)

如果正在执行Java方法,计数器记录的是当前的指令地址(虚拟机字节码指令地址),如果是Native方法则为空。

程序计数器是唯一一个没有内存溢出错误(OutOfMemoryError)的区域

虚拟机栈(线程私有)

虚拟机栈也是线程私有的,她的生命周期与线程相同。

虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

  • 局部变量表:存放了编译期可知的各种Java虚拟机基本数据类型(boolean,byte,...)、对象引用(reference类型,对象起始地址的引用指针)和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中 64 位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

栈帧(Stack Frame):用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking),方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁(抛出异常也算结束)

虚拟机栈有两种异常情况:

  1. 当线程请求深度大于虚拟机所允许深度,抛出:StackOverflowError异常

  2. 如果Java虚拟机栈容量可以动态扩展,当栈无法申请到足够的内存会抛出OutofMemoryError异常

    HotSpot 虚拟机的栈容量是不可以动态扩展的,以前的 Classic 虚拟机倒是可以。所以在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常——只要线程申请栈空间成功了就不会有 OOM,但是如果申请时就失败,仍然会出现OOM异常)

本地方法区(线程私有)

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native)方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和 Java 虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowEror(栈溢出) 和 OutOfMemoryEror(堆溢出) 异常。

Hotspot虚拟机的栈容量是不允许动态扩展的(一个线程的虚拟机栈申请到的空间是多少就是固定的),以前的 Classic 虚拟机倒是可以。所以在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常——只要线程申请栈空间成功了就不会有 OOM,但是如果申请时就失败,仍然会出现OOM异常)

堆(Heap-线程共享)-运行时数据区

  • 对于Java应用程序来说,Java堆是虚拟机管理的内存中最大的一块。

  • Java堆(Java Heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。

    Java堆(Java Heap)内存的唯一目的就是存放对象实例,Java中几乎所有对象实例都在这里分配内存。(创建的对象和数组都保存在Java堆内存中)

    ("所有对象实例以及数组都应当在堆上分配")但随着即使编译技术和逃逸技术分析的日渐强大,栈上分配,标量替换优化手段已经导致一些微妙的变化悄然发生,现在Java对象都在栈上分配已经不那么绝对了。

  • Java堆是垃圾收集器管理的内存区域(是垃圾收集器进行垃圾收集的最重要的内存区域),因此一些资料中称Java堆(Java Heap)为GC堆(Garbage Collected Heap)

    现代VM采用分代收集算法,因此Java堆从GC的角度还可以细分为新生代(Eden区,From Survivor区和To Survivor区)和老年代。

  • (Thread Local Allocation Buffer,TLAB)缓存区:从内存分配的角度看,所有的线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。不过不管如何划分Java堆中存储的都只能是对象的实例,将Java Heap细分的目的只是为了更好的回收内存或者分配内存。

    Java堆可以处于物理上的不连续内存空间中,但在逻辑上应该被视为连续的(抽象出来就是俩数据结构)。

  • Java堆可以被实现成固定大小的,也可以是可扩展的(参数-Xmx (memory max最大可分配),-Xms (memory start启动时内存)设定内存大小)。当内存不够也无法扩展时会抛出OutOfMemoryError异常。

  • 运行时数据区的GC

作为垃圾回收器管理的区域,Java堆从GC的角度可以分为新生代(Eden区,From Survivor区和To Survivor区)和老年代。

  • 新生代:是用来存放新生的对象,一般占据堆的1/3空间。用于频繁创建对象,所以新生代会频繁触发MinorGC进行回收。新生代又分为Eden区,ServivorFrom、ServivorTo三个区

    • Eden区:Java新对象的出身地若(若新创建的对象占用内存很大,直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代进行一次垃圾回收。

    • SurvivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。

    • SurvivorTo:保留了一次MirrorGC过程中的幸存者。

    • MirrorGC:采用复制算法(复制->清空->互换)

      1. Eden,SurvivroFrom复制到SurvivorTo,年龄+1:首先吧Eden和Survivor中的存活的对象复制到SurvivorTo区域(如果有对象的年龄达到了老年代的标准则复制到老年代),同时吧这些对象年龄+1(若Survivor不够位置就放到老年代区)

      2. 清空Eden,SurvivorFrom中的对象。

      3. 最后将SurvivorTo和SurvivorFrom互换,源ServivorTo中的对象称为下一次GC时的ServivorFrom区

  • 老年代:主要存放应用程序中生命周期长的内存对象。

    • 老年代的对象比较稳定,所以MajorGC不会频繁执行。
      • 在进行MajorGC之前一般都先进行了一次MinorGC,使得有新生代的对象晋升老年代,导致用户空间不足时才会触发(MajorGC的回收耗费事件所以不能频繁进行)。
      • 当无法找到足够大的连续空间分配给新创建的较大的对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
  • 永久代(一种方法区的实现方式):主要存放Class和Meta(元数据)的信息

    • Class在被加载的时候放入永久区域,和存放实例的区域不同,GC不会在主程序运行期间对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终导致OOM。

方法区/永久代(线程共享)

基本信息:

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,他用于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器(JIT)代码缓存等数据(保存着被加载过的每一个类的信息,这些信息由类加载器在加载类的时候,从类的源文件中加载出来)。
    • 常常将方法区称为永久代因为Java8之前Hotspot VM使用Java堆的永久代方法来实现方法区,将GC扩展至方法区使得Hotspot的垃圾收集器能够像管理Java堆一样管理这部分内存,而不必为方法区来开发一个专门的内存管理器(永久代的内存回收的主要目标是针对常量池的回收和类型的卸载,收益很小)。
    • 方法区是线程共享的,当有多个线程都使用到了一个类,这个类还未被加载,这时进行类初始化的话可能会引起多个线程阻塞。
    • 方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jvm也可以允许用户和程序指定方法区的初始大小,最小和最大限制;
    • 垃圾收集器在这个区域较少出现,但永久代不代表永久存在。
      • 通过用户自定义的类加载器可以动态的扩展Java程序,这样可能会导致一些类不再被使用,变为fw。这时需要垃圾清理对类进行回收
        • 该类所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
        • 加载该类的ClassLoader已经被回收
        • 该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

方法区/元空间(线程共享)

  • 永久代在Java8中被移除,取而代之的是元空间(元数据区)的实现方式。
    • 永久代中存放元数据的大小由虚拟机决定,存在内存溢出的情况。而元空间实现的方法区默认情况下仅受本地内存限制。
    • 永久代的内存回收(Full GC)仅针对常量池的回收和类型的卸载,收益低。
  • 类的元数据放入本地内存,字符串池和静态常量放入Java堆中,这样可以加载多少类的元数据由系统的实际可用空间来决定。

运行时常量池(Runtime Constant Pool)

  • 运行时常量池是方法区中的一部分(jdk1.8之后方法区实现由永久代变为元空间 Metaspace),在加载类和接口到虚拟机后就会创建运行时常量池。

  • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量(static final),也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。

    • 每一个Class文件中都维护着一个常量池(非运行时常量池)存放着字面量和符号引用。这个常量池的内容在类加载的时候,被复制到方法区的运行时常量池。
      • 字面量:string字符串和基本数据类型以及他们的包装类的值,以及final修饰的变量
      • 符号引用:一组符合JVM规范定义的符号,用于描述所引用的目标,在编译的时候由虚拟机翻译成为真正的地址。

    Class的常量池将在类加载存放到方法区的运行时常量池中。

  • 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

方法区结构

  • 虚拟机加载类会在方法区保存已加载的类信息(不是Class对象,Class对象保存在堆中是加载的最终产品,方法区的类信息中保存一个Class对象和ClassLoader的引用),类信息存放形式根据不同虚拟机不同实现。

  • 类信息:

    • 类型信息:

      • 类的完整名称(权限定名称)
      • 类的直接父类的完整名称(父类索引)
      • 类的实现接口的有序列表
      • 类的修饰符,访问标志
    • 类型的常量池:

      • JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。(这个类用到的常量的有序集合)
      • 为什么需要常量池?
        • 一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池。
      • 运行时常量池(Runtime Constant Pool)是方法区的一部分。
      • 常量池表(Constant Pool Table)是Class字节码文件的一部分,
    • 字段信息(实例域中的字段信息):

      • 声明的顺序
      • 修饰符
      • 数据类型
      • 字段名称
    • 方法信息:

      • 声明的顺序
      • 修饰符
      • 返回值名称
      • 方法名称
      • 参数列表(有序保存)
      • 异常表(抛出的异常)
      • 方法字节码(Native、abstract方法除外)
      • 操作数栈和局部变量表大小
    • 类变量(static静态变量,静态域中的字段信息)

      • 静态域非final变量随着类的加载而加载,他们成为类数据在逻辑上的一部分。

        JDK1.7 字符串常量池在堆,运行时常量池在方法区(永久代) 。

        JDK1.8 字符串常量池在堆,运行时常量池在方法区(元空间)。

      • 被声明为final的类变量在编译的时候就会被分配了,被保存在类的常量池中,在加载类的时候复制进运行时常量池中,每一个使用它的类保存着一个对其的引用(复用)。

    • 对类加载器的引用

      • JVM必须知道一个类型是由启动加载器还是用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中(方法区存在用户自定义ClassLoader信息)。
    • 对Class类的引用

      • JVM为每一个加载的类都为其在堆上创建一个java.lang.Class实例(我们通过三种方式获得的Class实例是在这个类被加载的时候保存在堆上的)。
      • JVM必须以某种方式将Class的这个实例和存储在方法区中的类的元数据联系起来。所以方法区的类信息中存在一个对Class的引用。
        • 元数据:又称中介数据中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。JAVA中的元数据可以参考博客
    • 方法表

      • 为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。
      • jvm对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)。JVM可以通过方法表快速激活实例方法。可以像数组那样使用索引快速访问到方法表上存储的方法,正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的(个人认为java的设计者 始终是把安全放在效率之上的,所以java才更适合于网络开发)

字符串常量池 StringTable 为什么要调整位置?

  • JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。(这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。)

垃圾回收与算法

垃圾回收之前应该搞清楚三点:

  1. 那些内存需要回收(引用计数,可达性分析)
  2. 什么时候回收(分代)
  3. 如何回收(复制,标记清除)

在Java内存模型中,程序计数器、虚拟机栈、本地房发栈随线程的而生,随线程而亡,栈中的栈帧随着方法的进入和退出有条不紊的进行着入栈和出栈。因此这几个线程私有的区域的内存分配和回收都具备确定性。当方法退出时内存就随着回收了。

而Java堆和方法区这两个区域具有着很显著的不确定性:同一个接口的多个实现类可能需要内存不同,只有处于运行期间我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存回收和管理是动态的。

如何确定垃圾

  • 引用计数法

    • 在Java中,引用和对象是有关联的。对对象的操作必然是通过引用进行的,这样就能通过引用计数来判断一个对象是否可以回收。
    • 在对象之中添加一个引用计数器,每当有一个地方引用她时,她的计数器就加一;当引用失效时计数器值就减一;任何时刻计数器为0的对象就是不在被使用的(就是可以回收的)。
    • 但引用计数需要考虑很多例外情况,必须要配合大量额外处理才能保证正确工作。(例如:单纯的引用计数很难解决对象的循环引用)
  • 可达性分析

    • 为了解决循环引用的问题,Java使用了可达性分析的算法。通过一系列的“GC roots”对象作为起点搜索。(如果在“GC roots”和一个对象之间没有可达路径(引用链),则称对象是不可达的)。

    • GC roots:

      • 虚拟机栈中引用的对象
      • 方法区中静态引用的对象
      • 方法区中常量引用的对象
      • 本地方法引用的对象
      • 虚拟机内部的引用如Class对象,系统类加载器。
      • 同步锁持有的对象
      • JMXBean,JVMTI中注册的回调,本地代码缓存

标记清除算法

  • 最基础的垃圾回收算法,分为两个阶段:标记和清除(在标记期间标记出需要回收的对象,在清除期间清除需要回收的对象)

  • 存在内存碎片化严重的问题(标记出的需要回收的内存是零散的,可能导致大对象找不到可利用连续空间的情况)

复制算法

  • 按内存容量将内存划分为等大小的两块。每次只使用其中一块,当一块内存满了(触发垃圾回收)就将存活的对象复制到另一块。
  • 存在内存利用率低(浪费了50%空间),对象存活数多时Coping算法效率大大降低。

标记整理算法

  • 先将内存中需要回收的内存进行标记,然后将存活的对象移动到内存的一端。

  • 移动活动对象,特别是老年代有大量对象存活的区域是一种负担极大的操作。

分代收集算法

目前大部分JVM都采用分代收集算法,根据对象的存活不同生命周期将内存划为不同的域(如:老年代,新生代)。老年代每次回收的对象较少,而新生代回收的对象较多,所以两个域可以使用不同的垃圾回收算法。

  • 新生代与复制算法(Copying)
    • 新生代中每次垃圾回收都要回收大量对象(大对数对象都是“朝生夕灭”的),新生代每次都要回收大部分对象(所以Eden区比较大),即复制操作比较少。
    • 每次使用Eden和一块Survivor,回收后将存活的对象复制到另外一块Survivor中。
  • 老年代与标记复制算法(Mark-Compact)
    • 老年代每次只回收少量对象,所以使用标记复制算法
    • 永生代(1.8之前)的回收主要包括废弃常量和无用的类
    • 当新生对象过大,也会直接分配到老年代。
    • 年龄(熬过的GC次数,默认是15)达到条件会从新生代移动到老年代

GC分代收集算法VS分区收集算法

  • 当前主流都是采用分代收集算法,这种算法会根据存活周期将内存分块,然后对不同的块采用不同的垃圾收集算法
    • 分区收集算法:将整个堆空间划分为连续的不同小区间(局部化),每个小区间单独使用,独立回收。这样做可以控制一次回收多少个区间,根据目标停顿的时间,合理的分配一次回收多少个区间,减少了GC所产生的停顿

JAVA中的四种引用类型

强引用

  • Java中最常见的引用,吧一个对象赋值给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用时,他处于可达状态,它是不可能被垃圾回收机制回收的,即该对象以后永远不会被用到JVM也不会被回收。(造成内存泄漏的主要原因之一)

软引用

  • 软引用需要使用SoftReference类来实现,对于软引用对象来说,当系统内存足够时它不会被回收,当系统内存不足时才会被回收。

弱引用

  • 弱引用需要使用WeakReference(WeakHashMap),对于只有弱引的对象来说,不管系统内存是不是足够,GC总会将其回收

虚引用

  • 弱引用使用PhantomReference来实现,她不能单独使用,必须和引用队列联合使用,虚引用的主要作用是跟踪对象被垃圾回收的状态。

GC垃圾收集器

Java内存被划分为了新生代和老年代两部分,新生代主要使用复制标记算法,老年代主要使用复制整理算法。所以,Java为这两个块提供了许多不同的垃圾收集器。

  1. Serial收集器

    Serial是一个单线程收集器,他不但只使用一个线程去完成垃圾回收工作,而且必须暂停所有其他线程直到垃圾回收结束。

    • 采用串行回收
    • 使用复制算法

    Serial Old新生代使用单线程复制算法,老年代使用单线程标记整理算法。

  2. ParNew收集器

    Serial的多线程版本,也是用复制算法,除了多线程以外和之前的Serial收集器完全一样。

    • 多线程
    • 使用复制算法
  3. Parallel Scavenge收集器

    Parallel Scavenge的关注点是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间),同时Parallel Scavenge具有自适应调节策略。

    • 多线程回收
    • 使用复制算法

    Parallel Old的老版本中新生代采用多线程复制算法,老年代采用多线程标记整理算法。

  4. CMS收集器

    • 主要目标是获取最短垃圾回收停顿时间
    • Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)
    • 使用标记-清除算法

    CMS整个过程分为四个阶段

    初始标记

    • 仅做标记:标记GC Roots能直接关联到的对象

    并发标记

    • 遍历整个对象图的流程(是和垃圾收集器一起并发运行的,不需要暂停用户线程)

    重新标记

    • 由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正

    并发清除

    • 基于标记结果,直接清理对象

    CMS收集器特点:

    • 尽可能降低停顿

    • 会影响系统整体吞吐量和性能:比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
      清理不彻底

    • 因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理

    • 因为和用户线程一起运行,不能在空间快满时再清理(因为也许在并发GC的期间,用户线程又申请了大量内存,导致内存不够)

    CMS的提出是想改善GC的停顿时间,在GC过程中的确做到了减少GC时间,但是同样导致产生大量内存碎片,又需要消耗大量时间去整理碎片,从本质上并没有改善时间。

  5. G1收集器

  • 基于标记整理算法,不产生内存碎片。

  • 可以预测停顿时间,在不牺牲吞吐量的情况下,实现低停顿垃圾回收(局部回收)。

  • G1收集器开创了面向局部收集的设计思路和基于Region的内存布局形式。

G1不再坚持固定大小以及固定数量的的分代内存区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代的Eden,Survivor空间,或者老年代空间。这样新生代和老生代就成为了一系列的区域的动态集合

Humongous区域:专门用于存放大对象,,G1认为只要大小超过了Region容量的一半大小就会被认为是大对象。

G1的垃圾回收每次回收的都是Region大小的整数倍(这样就使得停顿时间可以预测),同时会对跟踪各个Region中垃圾堆的价值大小并维护一个优先级表。优先回收最有价值的区域(最垃圾的区域)。

G1将完整的堆局部化为了一个一个的小堆(或许能和缓冲区关联起来),每个小堆都可以扮演一个域角色(域不再固定,而是动态的局部化的)

Region 区域划分和优先级区域回收策略确保G1收集器可以在有限时间内获得最高的垃圾回收集效率。

JVM类加载机制

一个类型从被加载到虚拟机内存中开始,到卸载为止,她的整个生命周期将会经历加载,验证,准备,解析,初始化,使用,卸载七个阶段。

其中加载,验证,准备,初始化,卸载的顺序是确定的。

Java虚拟机规范中规定了只有6中情况必须立即对类进行初始化(此前已经加载验证准备过了)。

  • 遇到new,geststatic,pustatic或invokestatic这四条字节码时如果类型没有过初始化,则需要先触发初始化阶段。
    • 使用new关键字实例化对象的时候
    • 读取或设置一个类型的静态字段(被final修饰,已经在编译期吧结果放入常量池的静态字段除外)的时候。
    • 调用一个类型的静态方法的时候
  • 使用java.lang.reflect包对类进行反射调用的时候
  • 当初始化类的时候,如果发现父类还没有被初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动的时候,用户需要指定一个要执行的主类
  • JDK7动态语言支持中java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且方法句柄对应的类没有进行过个初始化,那么就要先对这个类进行初始化。
  • JDK8中,接口加入了默认方法(default),如果有这个接口的实现类发生了初始化,那接口就要在之前被初始化。
flowchart LR A[加载\nLoading];B[验证\nVerification];C[准备\nPreparation];D[解析\nResolution];E[初始化\nInitialization];F[使用\nUsing];G[卸载\nUnloading]; A-->B subgraph 连接 B-->C-->D end D-->E--类初始化结束-->F-->G

加载(Loading)

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

  • 通过一个类的权限定类名来获取定义此类的二进制字节流。(可以是网络,任何可以读取的文件,数据库,运行时计算生成等等等)
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存(Java Heap)中生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口(类型必须与类加器一起确定唯一性)。

对于数组类型的加载创建需要遵循以下过程:

  • 如果数组组件类型是引用类型,就递归采用上面的定义去加载这个组件,数组C将被标识在加载该类组件类型的类的类加载器的类名称空间上。
  • 如果组件是非引用类型,Java虚拟机将会把数组C标记为与引导类加载器相关联。
  • 数组类的可访问性默认与她的组件的可访问性一致,如果组件类型不是引用类型她的默认可访问性为public,可被所有类和接口访问到。

加载结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中(方法区数据存储格式依据JVM不同而不同),类型数据存放在方法区之后,会在Java堆内存中实例化一个对应的Class对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

验证(Verification)

这一阶段的目的是确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并不会危害虚拟机的自身安全。

  • 文件格式验证

  • 元数据验证

  • 字节码验证

  • 符号引用验证

  • ...

准备(Preparation)

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段(静态变量),即在方法区中分配这些变量所使用的内存空间。

JDK1.7时将static区移动到了Java Heap中

而静态常量(final)在编译阶段会生成ConstantValue属性,在准备阶段会根据ConstantValue属性将v赋值为8080。

解析(Resolution)

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。

  1. 类或接口的解析
  2. 字段解析
  3. 方法解析
  • 符号引用:CONSTANT_Class_info,CONSTANT_Filed_info,CONSTANT_Method_info等类型的常量。

    • 序号引用是与虚拟机实现无关的,明确规定在了Java虚拟机规范的Class文件格式中。
  • 直接引用:

    • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定是已经存在内存中。

除invokeddynamic指令外,虚拟机可以实现对第一次解析结果进行缓存(如运行时直接引用常量池中的记录),并把常量标识为已解析状态。

初始化(Initialization)

初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器之外,其他操作都由JVM主导,到了初始阶段才真正开始执行类中定义的Java程序代码。

  • 初始化阶段是执行类构造器方法的过程(是由编译期自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句产生的),编译器收集的顺序是语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在之前可以赋值但不能访问。
  • 虚拟机保证在子类的方法执行之前父方法已经执行完毕。

类加载器

Java虚拟机将加载动作放到JVM外部实现,让程序员可以自己决定如何获取所需的类(通过类加载器实现)。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。

对于两个类只要类加载器不同那么久不相等

  • 启动类加载器/引导类加载器

    负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可的(按文件名识别,如rt.jar)的类。

  • 扩展类加载器(JDK9中被平台类加载器所取代)

    负责加载JAVA_HOME\lib\ext目录中的,或者通过java.ext.dirs系统变量指定路径中的类库。

  • 应用程序类加载器/系统类加载器

    负责加载用户路径(classpath)上的类库。

    JVM通过双亲委派模型进行类的加载,当然可以可以通过java.lang.ClassLoader实现自定义的类加载器。

双亲委派模型

  • 从Java虚拟机角度来看,只存在两种不同的类加载器:一种是启动类加载器(由C++实现,是虚拟机的一部分),一种是其他加载器(由Java实现,独立于虚拟机之外,并全部继承自java.lang.ClassLoader)。

  • 双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,他首先不会自己尝试加载这个类,而是把这个请求委派给父类去完成,每一个层次的类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中。只有当父加载器发现自己无法完成这个请求的时候(在她的加载路径之下没有发现要加载的Class文件),子类才会尝试去加载。

  • 双亲委派模型的实现:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded 
            Class<?> c = findLoadedClass(name); //判断请求的类是否已经被加载过了
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { //若父加载器加载路径下没有目标类(抛出ClassNotFoundException)
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) { //若父类无法加载
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name); //子类加载器自身的findClass
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment(); //增加寻找到得类数量
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

OSGI

OSGi(Open Service Gateway Initiative):动态模型系统(使得Java应用程序能像电脑外设一样随意更换,即插即用等),是面向Java的动态模型系统,是Java动态模块化系统的一系列规范。

动态改变构造

  • OSGi服务平台提供多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGI技术提供一种面向服务的架构,他是这些组件动态的发现对方。

模块化编程与热插拔

  • OSGi旨在为实现Java程序员的模块化编程提供基础条件,基于OSGi的程序很可能可以实现模块级的热插拔功能,当程序更新时,可以只停用,重新安装然后启动程序的一部分,这对企业及程序开发来说是非常具有诱惑力的选择。
  • OSGi描绘了一个很好的模块化开发目标,而且定义了实现这个目标的所需要服务与架构,同时也有成熟的框架进行实现支持。但并非所有的应用都是和采用OSGi作为基础架构,它在提供强大功能的同时,也引入了额外的复杂度,因为她破坏了双亲委派模型。
这篇关于Java JVM学习笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!