JVM的内部结构与详述

JVM

概念

  • JVM(Java Virtual Machine):Java虚拟机,简称JVM,是运行所有Java程序的假想计算机,是Java程序的运行环境,Java最具吸引力的特性之一。Java代码,都运行在JVM之上。
  • JRE(Java Runtime Enviroment):Java程序的运行时环境,包含**JVM和运行时所需要的核心类库**。
  • JDK(Java Development Kit):Java程序开发工具包,包含**JRE开发人员使用的工具**。
  • 总结:JDK > JRE > JVM
    • 我们想要运行一个已有的Java程序,那么只需安装JRE
    • 我们想要开发一个全新的Java程序,那么必须安装JDK

JVM运行原理

  • JVMjava的核心和基础,在java编译器和os平台之间的虚拟处理器java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。
  • JVM执行程序的过程:1. 加载.class文件;2. 管理并分配内存;3. 执行垃圾回收机制
  • 一段Java代码的在JVM中的执行流程如下:

在这里插入图片描述

JVM基本结构

在这里插入图片描述

  • 类加载器(ClassLoader):JVM启动时或者在类运行时将需要的class加载到JVM
  • 执行引擎:负责执行.class文件中包含的字节码指令
  • 内存空间:是在JVM运行的时候操作所分配的内存区。运行时内存主要可以划分为以下几个区域:
    • 方法区(Method Area):是各个线程共享的区域,存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆)。方法区还包含一个运行时常量池
    • Java(Heap):也是**线程共享的区域,存储Java实例或者对象的地方。**这里GC的主要区域。
    • Java(Stack)每个线程私有的区域,它的生命周期与线程相同。每执行一个方法就会往栈中压入一个元素,这个元素叫栈帧用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。
    • 本地方法栈(Native Method Stack):和Java栈类似,只不过是为JVM使用到的native方法服务的。
    • 程序寄数器(PC Register)每个线程私有的区域,用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方。还有程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是程序寄数器在管,它的作用就是控制程序指令的执行顺序。 执行引擎就是根据程序寄数器调配的指令顺序,依次执行程序指令。
  • 本地方法接口:连接本地方法库和Java栈。
  • 垃圾收集器:进行垃圾回收机制

JVM内存分配

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

垃圾回收算法

引用计数法

  • 引用计数法顾名思义,就是对一个对象被引用的次数进行计数,当增加一个引用计数就加1,减少一个引用计数就减1。当引用计数为0时,对象的内存空间将会被回收掉。引用计数算法原理非常简单,是最原始的回收算法,但是java中没有使用这种算法。原因是因为:
    • 频繁的计数影响性能
    • 无法处理循环引用的问题

标记-清除法

  • 算法原理如名,先进行标记,再进行清除
    • 标记:遍历所有的GC Roots,并将从GC Roots可达的对象设置为存活对象;
    • 清除:遍历堆中的所有对象。将没有被标记可达的对象清除。
  • 不足
    • 标记 - 清除算法在执行时,Java程序将被暂停,产生stop the world
    • 因为涉及大量的内存遍历工作,所以执行性能较低,从而效率低,这也会导致Java程序暂停时间较长。
    • 对象被清除之后,被清除的对象留下内存的空缺位置,造成内存不连续,空间浪费。

标记-压缩法

  • 在进行完标记清除之后,对内存空间进行压缩,节省内存空间,解决了标记清除算法内存不连续的问题。
  • 标记压缩算法也会产生stop the world,不能和Java程序并发执行。在压缩过程中一些对象内存地址会发生改变,Java程序只能等待压缩完成后才能继续。

分代收集算法

  • 分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

  • 堆内存按对象的生命周期的不同划分为:年轻代(Young Generation)年老代(Old Generation)持久代(Permanent Generation)。其中持久代主要存放的是类信息,所以与Java对象的回收关系不大,与回收息息相关的是年轻代和年老代。

    • 年轻代:被分为3个部分,Enden区和两个Survivor(From和to)。当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。**经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。**通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,fromto是相对的。

在这里插入图片描述

  • 年老代:年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存
  • 持久代:用于存放静态文件,比如Java类、方法等。持久代对垃圾回收没有显著的影响

垃圾回收器

串行收集器

  • 串行收集器就是使用单线程进行垃圾回收。对新生代的回收使用复制算法,对老年代使用标记压缩算法。
  • 串行收集器是最古老最稳定的收集器,尽管它是串行回收,回收时间较长,但其稳定性是优于其他回收器的,综合来说是一个不错的选择。
  • 执行垃圾回收时,应用程序线程暂停,GC线程开始(开始垃圾回收),垃圾回收完成后,应用程序线程继续执行。

并行收集器

  • ParNew回收器
    • **这个回收器只针对新生代进行并发回收,老年代依然使用串行回收。**回收算法依然和串行回收一样,新生代使用复制算法,老年代使用标记压缩算法。在多核条件下,它的性能显然优于串行回收器
    • 在进行垃圾回收时应用程序线程依然被暂停,GC线程并行开始执行垃圾回收,垃圾回收完成后,应用程序线程继续执行。
  • Parallel回收器
    • 依然是并行回收器,但这种回收器有两种配置。
      • 一种类似于ParNew:新生代使用并行回收、老年代使用串行回收。​它与ParNew的不同在于它在设计目标上更重视吞吐量,可以认为在相同的条件下它比ParNew更优。
      • Parallel回收器另外一种配置则不同于ParNew,对于新生代和老年代均适应并行回收。
    • Parallel回收器的流程和ParNew的流程是一致的。在进行回收时,应用程序暂停,GC使用多线程并发回收,回收完成后应用程 序线程继续运行。

CMS回收器

  • CMS回收器:Concurrent Mark Sweep并发标记清除。
  • 并发表示它可以与应用程序并发执行、交替执行。 标记清除表示这种回收器不是使用的是标记压缩算法,这和前面介绍的串行回收器和并发回收器有所不同。需要注意的是**CMS回收器是一种针对老年代的回收器,不对新生代产生作用**。
  • 这种回收器优点在于减少了应用程序停顿的时间,因为它不需要应用程序完成暂定等待垃圾回收,而是与垃圾回收并发执行。
  • 主要工作流程
    • 初始标记:标记从GC Root可以直接可达的对象;
    • 并发标记(和应用程序线程一起):主要标记过程,标记全部对象;
    • 重新标记:由于并发标记时,用户线程依然运行,因此在正式清理前,再依次重新标记,进行修正。
    • 并发清除(和用户线程一起):基于标记结果,直接清理对象。
  • CMS的优点显而易见,就是减少了应用程序的停顿时间,让回收线程和应用程序线程可以并发执行。
  • 但它也不是完美的,从他的运行机制可以看出,因为它不像其他回收器一样集中一段时间对垃圾进行回收,并且在回收时应用程序还是运行,因此它的回收并不彻底。这也导致了CMS回收的频率相较其他回收器要高,频繁的回收将影响应用程序的吞吐量。

G1回收器

  • 不同于其他的回收器、G1将堆空间划分成了互相独立的区块。每块区域既有可能属于老年代、也有可能是新生代,并且每类区域空间可以是不连续的(对比CMS的老年代和新生代都必须是连续的)。这种将老年代区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时**G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。**这也是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块。
  • G1相对CMS回收器来说优点在于:
    • 因为划分了很多区块,回收时减小了内存碎片的产生;
    • G1适用于新生代和老年代,而CMS只适用于老年代。

类加载器

类加载流程

在这里插入图片描述

  • 加载:加载是类装载的第一步,首先通过.class文件的路径读取到二进制流,解析二进制流将里面的**元数据(类型、常量等)**载入到方法区,在Java堆中生成对应的java.lang.Class对象。
  • 连接:连接又分为三步,验证、准备、解析。
    • 验证:主要的目的就是判断.class文件的合法性。除此之外,还会对元数据、字节码进行验证。
    • 准备:就是分配内存,给类的一些字段设置初始值。
    • 解析解析过程就是将符号引用替换为直接引用。例如某个类继承java.lang.Object,原来的符号引用记录的是"java.lang.Object"这个符号,凭借这个符号并不能找到java.lang.Object这个对象在哪里,而直接引用就是要找到java.lang.Object所在的内存地址,建立直接引用关系,这样就方便查询到具体对象。
  • 初始化:初始化过程,主要包括执行类构造方法、static变量赋值语句,static{}语句块,需要注意的是如果一个子类进行初始化,那么它会事先初始化其父类,保证父类在子类之前被初始化。所以,在Java中初始化一个类,必然初始化了java.lang.Object

类加载器种类

  • 类加载器ClassLoader,它是一个抽象类,ClassLoader负责把具体实例的Java字节码读取到JVM当中,ClassLoader还可以定制以满足不同字节码流的加载方式,比如从网络加载、从文件加载。ClassLoader的负责整个类装载流程中的加载阶段。
  • 系统中的ClassLoader
    • BootStrap ClassLoader(启动ClassLoader)C/C++写的,看不到源码也获取不到该类的对象。
    • Extension ClassLoader(扩展ClassLoader) – 加载位置:jre\lib\ext
    • **App ClassLoader(应用 ClassLoader) ** – 加载位置:classpath
    • Custom ClassLoader(自定义ClassLoader) – 必须继承ClassLoader
  • 每个ClassLoader都有另外一个ClassLoader作为父ClassLoaderBootStrap Classloader除外,它没有父ClassLoader注意:父ClassLoader不代表是它的父类。
  • 加载方式,采用双亲委托机制:自下而上的检查类是否被加载,自上而下的尝试加载类。需要注意的是,即使两个类来源于相同的.class文件,如果使用不同的类加载器加载,加载后的对象是完全不同的。

猜你喜欢

转载自blog.csdn.net/lingboo111/article/details/88568545