天天看点

JVM结构原理与GC工作原理

jvm–结构原理

jvm主要由四个部分组成:类加载器、运行时数据区、执行引擎、本地接口

一、类加载器

在jvm启动时或在类运行时将需要的class加载到JVM中

类加载的过程

类从被加载到jvm的开始与到卸载的为止,共经历7个过程:加载、验证、准备、解析、初始化、使用、卸载(验证、准备、解析统称为连接(linking))。其中类的加载、验证、准备、初始化、卸载这五步的顺序是固定不变的,而解析阶段则不一定(他在某些情况下可在初始化之后再开始,java语言的运行时绑定)。

类初始化

  1. 创建类的实例
  2. 对类进行反射调用时,发现类未初始化,则需触发其初始化
  3. 当初始化该类时,父类并未初始化,则需先初始化父类
  4. 当虚拟机启动时,用户需要指定一个执行的主类,虚拟机则先初始化该类

注:类的实例化和初始化是不同的概念

类的实例化:是指创建一个类的实例

类的初始化:是指类中的属性(如statis变量)赋初始值的过程,是类的生命周期中的一个过程 。

ClassLoader的等级加载机制

Java默认提供三个ClassLoader

**Bootrap ClassLoader:**启动类加载器,是指类加载层次中最顶层的类加载器,负责加载jdk中核心类库

Extension ClassLoader: 扩展类加载器,主要负责加载java的扩展类库,java虚拟机的实现会提供一个扩展目录,该类加载器在此目录里面查找并加载java类

APP ClassLoader: 系统类加载器,主要加载应用程序classpath目录下所有的jar和class文件。通常java应用的类都通过它来完成加载,可以通过ClassLoader.getSystemClassLoader()来获取它。

ClassLoader加载类的原理

ClassLoader是使用的双亲委托机制来搜索加载类的,每一个ClassLoader都有一个父类加载器的引用(非继承关系,组合关系),虚拟机内置的类加载器(Bootrap ClassLoader)本身没有父类加载器,但他可用作其他ClassLoader实例的父类加载器。当一个ClassLoader实例需要加载某个类时,他会在亲自搜索某个类前,委托给他的父类加载器,这个过程是由上至下的一次检查。首先由BootstrapClassLoader尝试加载,未加载则往下交给ExtensionClassLoader进行加载,还未加载则交给APPClassLoader进行加载,如果他也没有加载到的话,则返还给委托的发起者,由他到指定的文件系统或者网络url中加载该类,如果都没有加载到该类,则抛出ClassNotFoundException异常。如果加载到该类,则生成该类的定义,并将它加载到内存中,最后返还这个类在类村中的class对象。

二、运行时数据区

运行时数据区实在jvm运行的时候操作所分配的内存区。主要分为五个区域:方法区、Java堆、java栈、程序计数器、本地方法栈。

方法区

用于存储类结构信息的地方,包括常量池,静态变量,构造函数。虽然jvm规范把方法区描述为堆的一个逻辑部分,但他有个别名非堆(non-heap)。

jdk1.8后将方法区移植到内存中(1、提高jvm的io性能。2、整个永久代有一个jvm本身设置的固定大小上线,无法调整,而元空间使用的时直接内存,受本机控制,并且永远不会oom。

可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。

-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。)。

Java堆

存储Java实例或者对象的地方,这块是gc的主要区域。从存储内容上可以看到Java堆和方法区是被Java线程共享的。

Java栈

Java栈总是和线程相关联的,每当创建一个线程时,JVM就会为这个线程创建一个对应的Java栈。在这个栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量,操作栈,方法返还值等。每一个方法从调用直至完成的过程,就对应栈帧在Java栈中的出栈与入栈的过程。所以Java栈是私有的。

程序计数器

用于保存当前线程执行的内存地址。由于JVM是多线程执行的(线程轮转切换),所以为了保证线程切换回来很能保证原先状态,则需要一个独立的计数器来保存之前的中断的地址。因此,程序计数器也是私有的。

本地方法栈

与Java栈类似,用于服务执行native方法。

三、执行引擎

负责执行class中的字节指令。

负责执行class中的字节指令。### 四、本地接口

主要用于调用c或c++本地方法和返回结果。

JVM内存分配

Java虚拟机是一次性分配一块较大的内存空间,每次创建实例时都在该空间进行分配与释放,减少了系统的调用次数,节省了一定的开销,类似内存池的概念。有了这块空间,如何对内存进行分配与回收就需要由gc来控制。

Java一般内存申请有两种:静态内存、动态内存。

静态内存:编译时就可以确定的内存就是静态内存,即内存时固定的,如int变量。

动态内存:在程序执行时,才知道要分配的储存空间大小,比如Java对象的内存空间。

综上所述,Java栈、程序技术器和本地方法栈都是线程私有的,因此内存分配与回收是固定的。而Java堆与方法区则是在程序运行时才得知需要创建哪些对象,所以这部分内存即为动态内存,一般而言垃圾回收也是针对这部分。

垃圾检测和回收算法

垃圾回收一般需要完成两件事:检测垃圾与回收垃圾。

垃圾检测算法

引用计数算法:给对象增加一个引用计数器,每当有地方引用这个对象时,则在计数器上加一,反之减一。

可达性分析算法:以根集(Java堆中的引用对象,方法去中常量池引用的对象)对象为起点进行搜索,如果有对象不可达,即为垃圾对象。

垃圾回收算法

JVM在做垃圾回收时,会检查堆中所有的对象是否会被这些根集对象所引用,不能够被引用的对象就会被垃圾收集器回收。

标记-清除算法:分为两个阶段,标记和清除。标记所有需要回收的对象,然后统一回收。(缺点:效率低,标记清除后会产生大量的碎片)

复制算法:将内存空间划分为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前区域,把正在使用中的对象复制到另外的区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,复制过去的同时还能进行相应的内存整理,不会出现“内存碎片问题”,但需要双倍内存。

标记整理算法: 此算法结合了标记清除与复制算法的两个优点,也是两个阶段。第一阶段从根节点开始标记所被引用的对象。第二阶段遍历整个堆,把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“复制算法”的空间问题。

分代收集算法:当前商业虚拟机最常用的算法。分代的垃圾回收策略,是基于:不同的对象拥有不同的生命周期。因此不同生命周期的对象采取不同的收集方式,以便提高回收效率。在Java程序运行过程中,会产生大量的对象,因每个对象所能承担的职责和功能不同,所以也有着不同的生命周期,有的对象生命周期长,有的对象生命周期短。如果不对对象存活时间进行区分的话,那么每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对较长,而对于存活时间较长的对象进行扫描的工作是徒劳的。因此引入了分治思想,将对象进行代的划分。‘

将对象划分成:年轻代(young)、老年代(old)、持久代(permanent)。其中持久代主要存放的是类的信息,所以与Java对象的回收关系不大,与回收信息相关的时年轻代和老年代。

年轻代:是所有新对象产生的地方。年轻代被分为三个部分:Ender(出生)区和两个Survivor(幸存)区。当Ender区被占满时,就会执行MinorGC(复制算法)。并把所有存活下来的对象转移到Survivor(来自Ender)区,MinorGC同样会检查存活下来的对象,并把他们转移到另一个Survivor(来自上一个Survivor)区,这样总会有一个Survivor区空着。经过多次GC后,仍然存活下来的对象就会被转移到老年代。

老年代:在年轻代经过多次GC后仍然存活下来的对象都会被放到老年代,都是生命周期较长的对象。当老年代内存被占满时,则触发FullGC(标记清楚算法),回收整个堆内存。

持久代:用于存放静态文件,比如Java类,方法等。持久代对垃圾回收没有明显的影响。在jdk1.8中移除持久代,换成元空间(MetaSpace),元空间同持久代一样,只不过将使用本地内存。

jvm