Jvm复习总结


别问 问就是为了面试豁出了老命

Jvm数据区

jvm_数据区.png

  • 线程共享区
    • 方法区(运行常量池)
      主要用于存放堆内存中的一些执行逻辑和变量,类的加载信息,常量,静态变量等一系列信息,和堆不同,方法去不需要频繁gc等操作,又叫永久代

    • Java中的类实例,数组都将存放在堆中,gc发生也是在堆中,堆是Jvm内存中最大的一块内存地址
  • 线程私有区
    • 虚拟机栈
      虚拟机栈就是线程所私有Java方法的内存区域,包括方法所引用的基本数据类型和引用。
      Java方法的使用就是压栈过程,基本数据类型和引用则是存在栈帧中,当方法抛出异常或结束则是出栈的操作。
    • 本地方法区
      native修饰的非Java方法
      本地方法栈也有自己的栈帧等,栈帧里面也相应的有局部变量表,操作数栈,动态链接,出口信息等
    • 程序计数器
      Jvm的本质是多线程交替的,但是如何保证每一个线程拿到cpu资源的时候可以从结束的地方接着开始,就需要程序技术器来保证

堆的分类

  • Eden
  • Survivor0
  • Survivor1
  • 老年代

首先分配内存到Eden区,当发生Gc之后转移到s0或者s1中(如果s0先被使用,那么下一次则使用s1,总之是为了保留新生代的内容),每一次gc都会给新生代数据记录次数,一般来说超过15次则进入老年代

运行常量池

  • 字符串内容
  • final修饰的关键字
  • 基本数据类型的值
  • 符号引用
    • 类和结构完全限定名
    • 字段名称和描述符
    • 方法名称和描述符

Java对象的创建过程

  1. 类加载
  2. 分配内存
  3. 初始化零值
  4. 设置对象头
  5. 执行init

类加载过程

类加载过程图

  1. 加载 (所需类的加载过程)

    • 通过类的全限定名获取该类的二进制流(通过class全限定名从本地,网络,专有数据库中的jar或者zip中获取.class文件)
    • 生成java.lang.class对象作为方法区进入该对象的入口
    • 将字节流的存储结构转化到jvm方法区中运行时的数据结构
  2. 验证 (了解Class字节文件是否符合当前虚拟机要求)

    • 文件格式验证: 字节流是否符合class文件规范
    • 元数据验证: 是否符合java的语法规范,例如继承接口,是否实现了接口中的方法
    • 字节码验证: 数据和控制流验证,保证方法中的类型转换有效
    • 符号引用验证: 验证是否可以通过符号引用找到相应的对象和变量
  3. 准备 (为类的变量分配内存和设置类的初始值(即方法区分配这些变量空间))

    • 为变量分配空间
    • 为变量初始化赋值的过程,例如int 赋值为 0 ,对象赋值为 null 等
    • 特殊 private static final a = 1
      正常来说,应该是初始化阶段赋值,但是这个情况下直接在方法去中替换a = 1 ,则在准备阶段就完成赋值
  4. 解析 (虚拟机将常量池中的符号引用转为直接引用的过程)
    • 什么是符号引用?
      用一串不会有歧义的符号来标识引用的对象或者是变量
    • 什么是直接引用?
      正式用指针去引用符号和变量
  5. 初始化

    真正按照程序员的意愿去初始化值

分配内存

分配方法
  1. 指针碰撞
    内存空间比较整洁,直接移动指针分配空间
  2. 空闲链表
    “见缝插针”,空间不连续,则找到空闲的地方插入数据
线程安全问题
  1. 利用CAS不都安的尝试获取内存空间直到成功(目前虚拟机的解决方案)
  2. TLAB: 为每个线程独自分配一部分空间,且独有,分配的时候优先分配到该空间中

设置对象头

例如Synchronized关键字需要Mark Word中的monitor对象(MonitorExit,MonitorEnter)

对象的访问方式

  1. 句柄访问
    句柄.jpg
    reference -> 句柄池中指针 -> 实例数据
  2. 直接访问
    直接访问.jpg
    reference -> 实例数据

  3. 句柄和直接访问的优缺点分析

    论访问速度直接访问最快,但是如果需要删除的话需要直接删除数据
    句柄中则可以直接把句柄值赋null,效率更快

对象死亡的分析方法

  1. 程序计数法 : 清零以后则可以判断死亡
  2. 可达性分析法 : 以Gc root为起点 看看各个对象是否可以连接起来,如果连接不起来,被独立则可以判断回收
  • 什么是Gc root(GC Roots一般在JVM的栈区域里产生)
    • 处于激活状态的线程
    • 栈中的对象
    • JNI中的变量
    • JNI的全局引用
    • 对象头的Monitor对象

引用的类型

  • 强引用 (大部分是强引用,虚拟机就算oom,也不会回收引用)
  • 软引用 (当内存不足的时候,会回收软引用)
  • 弱引用 (只要被回收器判定为垃圾,则直接回收,可以用于判断是否被gc回收)
  • 虚引用 (虚引用不在乎引用了什么对象,可以说是一种gc标志,必须配合引用队列,可以在对象被回收之前做一些操作)

垃圾回收算法

  • 标记清除算法(标记为垃圾后,直接删除)
  • 复制算法 (开辟两块空间,直接将为被回收的部分复制到另一部分的空间内)
  • 标记整理算法 (先标记,之后移动,使得内存空间干净)
  • 分代收集算法 (分为年轻代和老年代,分别使用不同的算法,新生代用复制算法, 老年代用标记整理算法)

垃圾回收器

  • Serial (单线程)

    单线程回收垃圾,回收的时候需要暂停其它的一切线程

  • Parnew (多线程)

    对Serial的升级,其它不变,就是变成多线程

  • Parallel Scavenge (多线程)

    提升吞吐量(程序运行时间/CPU使用时间),提高回收次数,减少回收时间

  • CMS (真正意义上的多线程)
    • 标记清除算法
    • 过程
      1. 初始标记 (暂停所有线程,标记Gc Root相连的对象)
      2. 并发标记 (进行可达性分析,标记一系列回收对象)
      3. 重新标记 (修正并发标记期间用户的修改)
      4. 并发清除 (开始清理)
    • 缺点
      • 对CPU资源敏感
      • 无法清理浮动垃圾
      • 有太多的空间碎片
  • G1 (多线程,不区分新生代和老年代)
    • 预览图
      region
    • 分区 Region (G1对内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位。)

      不区分老年代和年轻代都会直接被划分成Region,每个Region还会被细分为若干个大小为512 Byte的Card,卡片还会记录在全局卡片表(Global Card Table)中.
      不同卡中的对象可能会互相引用,还可能是跨域Region引用,如果存在的话会直接写屏障(并发标记阶段)

    • 分区模型
      • 预览图
      • 巨型对象 Humongous Region
        由于太大会导致分区出现问题,因此直接在老年代中分配空间
      • 已记忆集合 Remember Set (RSet) (存在于Region中)
        • 记录引用分区内对象的卡片索引
        • 内部使用Per Region Table (PRT) 记录使用引用的情况(稀少,细粒度,粗粒度)
          • 稀少 (记录到卡)
          • 细粒度 (记录到region)
          • 粗粒度 (记录到引用的数目)
      • 过程
        1. 初始标记 (GC Root的标记)
        2. 并发标记 (三色标记法开始)
        3. 最终标记 (三色标记的最终标记)
        4. 筛选回收 (复制清除算法)
      • 三色标记
        • white (不可达回收)
        • grey (子对象还未扫描完)
        • black (gc root)
      • STAB

        本质是上对对象的一次快照,快照的内容就是查看对象的颜色(三色标记)以维持并发的效率。

Jvm什么时候发生full gc

  1. 永久代满的时候
  2. 老年代空间不足

双清委派模型

双亲委派模型.jpg

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类

双亲委派模型的破坏 (借鉴CSDN博主的文章说的很完整了)

原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
————————————————
版权声明:本文为CSDN博主「Jack老师」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/luoyang_java/article/details/92598142

参考文章