初探JVM

初探JVM

    • 运行时数据区域
      • 线程隔离数据区
      • 线程共享数据区
    • HopSpot虚拟机的对象
      • 对象的创建过程
      • 对象的内存布局
      • 对象的访问定位
    • 判断一个对象是否有用
      • 引用计数法
      • 可达性分析法
    • 引用的类型
      • 强引用
      • 软引用
      • 弱引用
      • 虚引用
    • 回收方法区(HostSpot的永久代)
      • 回收废弃常量
      • 回收无用的类
    • 垃圾收集算法
      • 标记-清除算法
      • 复制算法
      • 标记-整理算法
      • 分代收集算法
    • 垃圾收集器
      • Serial收集器和 Serial Old收集器
      • ParNew收集器 、 Parallel Scavenge收集器和Parallel Old收集器
      • CMS(Concurrent Mark Sweep)收集器
      • G1收集器
    • 内存分配与回收策略
      • 新生代GC(Minor GC)
      • 老年代GC(Full GC)
      • 分配规则
    • 类加载机制
      • 类加载的时机
      • 类加载全过程
    • 类加载器
      • 双亲委派模型
      • 双亲委派机制的过程

运行时数据区域

初探JVM_第1张图片

线程隔离数据区

  • 程序计数器

    • 主要记录了当前线程正在执行的字节码指令地址
  • Java虚拟机栈

    • 每一个方法从调用到执行完成的过程,就是对应一个栈帧在虚拟机栈中入栈到出栈的过程。

    • 局部变量表

      • 存放编译期可知的各种基本数据类型、对象引用、returnAdress类型(指向一条字节码指定的地址)
    • 每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 本地方法栈

    • 与虚拟机栈的区别

      • 本地方法栈为虚拟机执行Native方法服务
      • 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务

线程共享数据区

  • Java堆

    • 主要存放对象实例
    • 垃圾回收管理的主要区域,也被称为“GC堆”,可以细分为新生代 和老年代
    • 并发情况下,可能划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
  • 方法区

    • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译的代码等数据

      • Class文件加载进方法区
    • 运行时常量池

      • 存放编译期生成的各种字面量和符号引用
    • 这部分内存回收主要针对常量池的回收和对类型的卸载

HopSpot虚拟机的对象

对象的创建过程

  • 1、当虚拟机遇到一条new指令时,检查指令参数是否能在常量池定位到一个类的符号引用,并检查该符号引用是否已经被加载、解析、和初始化。如果没有,执行相应的类加载过程。

  • 2、类加载检查通过后,虚拟机为新生对象分配内存

    • 空闲列表分配方式
    • 指针碰撞分配方式
  • 3、内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。

  • 4、虚拟机对对象进行必要设置,例如对象是哪个类的实例、类的元数据信息地址、对象的哈希码、对象的GC分代年龄等存放对象头的信息。

  • 5、执行Init方法,按照程序员意愿初始化对象。

对象的内存布局

  • 实例数据

    • 存放对象有效信息,例如字段内容等
  • 对齐填充

  • 对象头

    • 存放对象自身运行的数据,如哈希码、GC分代年龄、锁状态标志等
    • 类型指针,通过该指针确定对象是哪个类的实例
    • 数组对象的对象头还必须要有记录数组长度的数据

对象的访问定位

  • 句柄
  • 直接指针方式

判断一个对象是否有用

引用计数法

  • 统计对象被引用的次数,当对象的引用次数为0时,说明该对象没有用了

可达性分析法

  • 通过一个“GC Roots”的对象作为起始点,向下探索,搜索所走过的路径成为“引用链“。当一个对象到GC Roots没有任何引用链,说明该对象不可用,可以被回收了。
  • 可作为GC Roots的节点主要在 全局性的应用(例如常量或者类静态属性)与执行上下文(例如栈帧中的本地变量表)中。

引用的类型

强引用

  • 程序普遍存在的引用,即通过 new 来创建的对象。只要强引用存在,该对象就不会被回收

软引用

  • 描述一些有用 但是并非必要的对象。系统将要发生内存溢出时,将会对这些对象列入回收范围之中,进行第二次回收。

弱引用

  • 描述非必需的对象。强度比软引用更弱;无论内存是否足够,都会回收被弱引用关联的对象。

虚引用

  • 最弱的一种引用关系。为一个对象设置虚引用目的是为了在回收的时候能够收到系统的通知。

回收方法区(HostSpot的永久代)

回收废弃常量

  • 与回收java堆中对象类似

回收无用的类

  • 无用类的满足条件

    • 该类所有的实例被回收
    • 该类的ClassLoader被回收
    • 该类对用的Class对象没有在任何地方被引用

垃圾收集算法

标记-清除算法

  • 标记需要回收的对象,再统一回收所有被标记的对象

  • 缺点

    • 标记和清除两个过程效率不高
    • 标记清除后会产生大量不连续的内存碎片,后续分配大对象时无法获得足够的连续内存,不得不提前触发另一次GC

复制算法

  • 将内存按容量划分为两块,每次只用一块,当进行垃圾回收时,将存活的对象复制到另外一块内存,对已使用过的内存进行全部清理。

    • 有些分一个 eden区和两个survivor区
  • 主要用来回收 新生代

  • 缺点

    • 能用的内存空间缩小一半
    • 当对象存活率较高时,需要进行较多复制操作,效率变低

标记-整理算法

  • 标记需要回收的对象,将所有存活的对象往一端移动,然后直接清理端边界以外的内存

分代收集算法

  • 根据对象存活周期,将内存划分为几块。一般把Java堆分为新生代 和 老年代。
  • 对于新生代,每次回收时发现由大量对象死去,只要少量对象存活,就是用复制算法。
  • 对于 老年代,对象存活率高,没有额外空间进行分配担保,只能使用 “标记-清理 ” 或者 “ 标记-整理” 算法进行回收。

垃圾收集器

Serial收集器和 Serial Old收集器

  • 单线程收集器

ParNew收集器 、 Parallel Scavenge收集器和Parallel Old收集器

  • 多线程收集器

CMS(Concurrent Mark Sweep)收集器

  • 基于“标志-清除”算法

  • 主要用在 老年代

  • 过程

    • 初始标记

      • 仅仅标记GC Roots能直接关联的对象,速度很快
    • 并发标记

      • 进行GC RootsTracing的过程,标记存活对象。耗时最长。
    • 重新标记

      • 修正并发标记阶段因用户程序继续运行而导致标记产生的变动的对象的标记记录。
    • 并发清除

  • 缺点

    • 占用CPU资源,导致应用程序变慢

    • 无法处理浮动垃圾

      • 浮动垃圾,即在标记过程后,程序运行不断产生的新垃圾。
      • 只能等待下一次GC
    • 由于采用 “ 标记-清除”算法,回收结束后,会出现大量内存碎片。

      • 使用-XX: +UseCMSCompactAtFullCollection 开关参数,用于进行FullGC开启内存碎片合并整理。
      • 使用-XX: CMSFullGCsBeforeCompaction参数,设置执行多少次不压缩的Full GC后,跟着来一次带压缩的

G1收集器

  • 总体基于 “标记-整理“算法,局部基于”复制“算法

  • 过程

    • 初始标记
    • 并发标记
    • 最终标记
    • 筛选回收
  • 特点

    • 并行和并发

    • 分代收集

    • 空间整合

      • 不会产生内存碎片
    • 可预测的停顿

      • 建立可预测的停顿时间模型,使用者可以指定收集时间片段,优先回收价值大的Region

内存分配与回收策略

对象主要分配在新生代的Eden区,少数情况分配在老年代

新生代GC(Minor GC)

  • 发生在新生代的垃圾收集动作,频繁且回收速度快

老年代GC(Full GC)

  • 发生在老年代的GC, 回收速度比新生代GC 慢10倍以上。

分配规则

  • 对象优先分配在Eden区

    对象优先分配在新生代的Eden区,当Eden区内存不足以分配时,虚拟机将触发一次Minor GC,当Minor GC 期间发现survivor区也不足以分配已标记的存活对象,则只能通过分配担保机制将已标记的存活对象分配到老年代中。

  • 大对象直接进入老年代

    大对象,指的是需要大量连续内存空间的对象,例如数组、字符串等。像这些对象,如果直接分配在新生代,则需要提前触发GC来获取大量连续的空间,同时也会导致新生代GC时发生大量内存复制。(新生代主要是“复制”算法)

  • 长期存活的对象进入老年代

    虚拟机给每个对象定义一个对象年龄计数器,对象经过一次的Minor GC 后,如果能被Survivor容纳的话,将移动到Survivor区中,并且对象年龄设为1,。对象在Survivor区中,每经历一次GC,年龄加1,当年龄达到一定程度(默认15),就会被晋升到老年代中。

  • 动态对象年龄判定

  • 空间分配担保

类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并且对数据进行校验、解析和初始化后,最终形成可以被虚拟机使用的java类型。

类加载的时机

  • 有且仅有的5种情况对类进行初始化(主动引用)

    • 遇到new关键字实例化对象时、读取或设置一个类的静态字段时、以及调用一个类的静态方法时
    • 对类进行反射调用时
    • 初始化子类时,父类还没有初始化,先触发父类的初始化
    • 包含main()方法的执行主类需要先初始化
    • 使用jdk1.7 的动态语言支持时,java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatci方法句柄对应的类没有初始化,则需要先初始化
  • 被动引用情况

    • 通过子类引用父类定义的静态字段,只会触发父类的初始化而不触发子类的初始化。(对于静态字段,只有定义该字段的类才被初始化)
    • 对于数组类型对象,不会触发该类的初始化,但会触发另一个名为“[Lxxx.xxx.xxx”的类的初始化。该类是由虚拟机自动生成的一个继承Object的子类。
    • 对于静态常量,编译阶段通过常量传播优化,存储到了NotInitialization类的常量池中,并没有通过定义该常量的类的引用,因此定义该常量的类不会触发初始化
    • 接口的初始化并不要求父接口全部初始化;接口通过() 类构造器初始化成员变量

类加载全过程

  • 加载

    • 通过一个类的全限定名获取定义此类的二进制字节流
    • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成该类的java.lang.Class对象
  • 验证

    • 验证Class文件字节流是否符合虚拟机要求

      • 文件格式验证
      • 元数据验证
      • 字节码验证
      • 符号引用验证
  • 准备

    • 为类变量(被static 修饰的变量)分配内存以及设置类变量的初始值(数据类型的零值)
  • 解析

    • 将常量池内的符号引用解析为直接引用
  • 初始化

    • 执行类中定义的java代码,初始化类变量和其他资源

      初始化阶段也就是 执行类构造器()方法的过程。
      类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块)中的语句合并产生的。
      编译器收集的顺序按照语句在源文件中的出现顺序决定,静态代码块只能访问到定义在静态代码块之前的变量,定义在之后的变量,在前面的静态代码块可以赋值,但是不能访问。
      子类的()方法执行之前,父类的()方法已经执行完毕。而接口不需要先执行父接口的()方法。只有当父接口中定义变量使用时,父接口才会去初始化。
      接口的实现类在初始化时,也不会执行接口的()方法。
      多线程环境下,虚拟机会保证一个类的()方法被正确加锁、同步,如果多个线程去初始化一个类,那么只有一个线程去执行该类的初始化,其他线程阻塞。

类加载器

对于任意一个类,都需要有加载它的类加载器和该类本身一同确定其在Java虚拟机的唯一性

双亲委派模型

除了顶层的启动类加载器外,其他类加载器都应该有自己的父类加载器。类加载器的父子关系一般不会以继承的关系实现,而是以组合的关系来复用父加载器的代码。

  • 启动类加载器

    • 负责将JAVA_HOME/lib下的类库加载到内存
    • 被-Xbootclasspath参数指定的路径的类库
    • 程序员不可以调用,如果需要加载请求委派给该类加载器,则直接返回 null 即可
  • 扩展类加载器

    • 负责JAVA_HOME/lib/ext下的类库加载到内存
    • 被java.ext.dirs系统变量指定的路径类库
  • 应用程序类加载器(系统类加载器)

    • 负责加载用户类路径(ClassPath)下指定的类库

双亲委派机制的过程

  • 当一个类加载器收到类加载请求时,它首先不会自己去加载这个类,而是把这个请求委派给它的父类加载器,其父类加载器也是如此,因此所有的类加载请求最终都是会委派到 顶层的启动类加载器去加载;只有当父类加载器无法完成加载请求时,才会交由子类加载器去自己加载。
  • 好处:对于一个类的加载,最终还是委派给顶层的启动类加载器来完成,这样就保证了该类在所有的类加载器环境中都是同一个类。

注意:虚拟机执行垃圾回收操作时,需要停顿所有java执行线程,才能够保证进行可达性分析时,对象的引用不是动态变化的,从而能够标记出需要回收的对象。停顿java线程的事件,被称为“Stop The World”。

XMind - Trial Version

你可能感兴趣的:(JVM,jvm,java,面试)