JVM虚拟机架构图

image.png

一、类加载器(类加载子系统)

image.png

1、作用

负责把class字节码文件加载到内存中
image.png

2、类加载过程

a. 加载

  • 通过一个类的全限定类名获取此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口
  • 使用IDEA插件 jclasslib-bytecode-viewer 可以查看
  • image.png

    b. 连接

  • 验证

    • 保证加载类的正确性
  • 准备
    • 为类变量分配内存,设置变量的初始值
  • 解析
    • 相当于翻译的过程
  • image.png

    c. 初始化

  • 初始化阶段就是执行类构造器方法()的过程

  • 收集静态变量赋值和静态代码块中的语句合并而来
  • ()不同于类的构造器
  • image.png

    3、类加载器的分类 (重点)

    a. 概念

  • 引导类加载器和自定义加载器

  • image.png
  • image.png

    b. 虚拟机自带的类加载器

  • 启动类加载器

    • image.png
  • 扩展类加载器
    • image.png
  • 应用程序类加载器

    • image.png

      c. 类加载规则

  • 自己写的类—应用程序类加载器

  • JDK核心库—启动类加载器

  • 用户自定义类加载器实现步骤
  • image.png

4、双亲委派模型(面试)

image.png

a. 概念

  • 要加载类,先坑爹(向上委托)一层一层往上找
  • 直到找到顶部启动类加载器,如果启动类加载器能加载,就直接加载
  • 如果不能再往下找,坑爹失败
  • image.png
  • image.png

    b. 是否可以自己编写一个String类

  • 不能自己编写一个String类,并且在java核心类的包下编写会出现 SecurityException: Prohibited package name: java.lang
  • image.png

    c. 沙箱安全机制

  • 我们如果要编写和核心类库全限定名一模一样的类,JDK为了保证核心代码的安全,阻止你的代码使用和它全限定名相同的命名

  • 保护原生JDK的安全

    d. 优点

  • 避免类的重复加载

  • 保护程序安全,防止核心API被随意篡改
    • 自定义类 java.lang.String
    • 自定义类 java.lang.Shkstart
  • image.png

二、运行时数据区内部结构

image.png
image.png

1. PC寄存器

a. 概念

  • 对物理PC寄存器的一种抽象模型
  • image.png

    b. 作用(面试题)

  • 用来存储下一条指令的地址,即将要执行的指令代码,由执行引擎执行

  • image.png
  • image.png

    2. 本地方法(掌握)

    a. 概念

  • 一个Native Method就是一个Java调用非Java代码的接口

    b. 作用

  • 与java环境外交互

  • 与操作系统交互
  • Sun’s java

    c. 举例

  • Thread里面的方法

    d. 本地方法栈

  • 用于管理本地方法的调用

  • image.png
  • image.png

    三、虚拟机栈(重点)

    1. 概念

  • 栈是运行时的单位,程序如何运行,如何处理数据

  • image.png

    • 类比—佛跳墙的菜谱步骤
    • image.png

      2. 作用

      主管java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用与返回
  • 局部变量 VS 成员变量(或属性)

  • 基本数据变量 VS 引用类型变量(类、数组、接口)

    3. 栈帧

  • a. 存储内容

    • 局部变量表(局部变量数组或本地变量表)
      • 定义为一个数字数组,主要用于存储方法参数和定义在方法内的局部变量,这些数据类型包括各类基本数据类型、对象引用,以及returnAddress类型。
      • 由于局部变量表是建立在线程的栈上,是线程的私有数据,所以不存在数据的线程安全问题。
      • 局部变量表所需的容量大小是在编译期确认下来的,并保存在方法的Code属性的maximum local variables数据项中,在方法运行期间是不会改变局部变量表的大小的
      • 方法的嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法的嵌套调用次数越多。
      • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。 当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
    • 操作数栈
      • 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈。
      • 操作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)、出栈(pop)。
        • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再将结果压入栈。
        • 比如: 执行复制、交换、求和等操作
        • image.png
        • image.png
  • b. 运行原理

    • 先进后出

      4. 五道面试题

    1. 举例栈溢出的情况?(StackOverflowError)
      • 通过设置-Xss1m参数调整栈空间大小
    1. 调整栈大小,就能保证不出现栈溢出吗?
      • 不能,如果写个递归并且没有出口,那么迟早会出现栈溢出
    1. 分配的栈内存越大越好吗?
      • 不是,如果给栈分配过大内存将会导致其他部分没有可用内存
    1. 垃圾回收是否会涉及到虚拟机栈?
      • 不会涉及
    1. 方法中定义的局部变量是否存在线程安全?
      • 具体问题具体分析 ```java //s1的声明方式是线程安全的 public static void method1(){ //StringBuffer:线程不安全的 StringBuffer s1 = new StringBuffer(); s1.append(“A”); s1.append(“B”); … }

解释: 这个例子是线程安全的,s1为局部变量,返回值为void,只能被当前线程操作,是线程安全的。 注意:这里返回值为空(void)

  1. ```java
  2. 注意:把StringBudiler作为参数传进去
  3. //先说结论,这个是线程不安全的
  4. //StringBuilder是线程不安全的
  5. public static void method2(StringBuiler sBuilder){
  6. sBuilder.append("A");
  7. sBuilder.append("B");
  8. ...
  9. }
  10. //我们在main操作一下
  11. public static void main(String[] args){
  12. StringBuilder s = new StringBuilder();
  13. new Thread(()->{
  14. s.append("a");
  15. s.append("b");
  16. }).start();
  17. method2(s);
  18. }
  19. //它们会抢s的资源,是线程不安全的
  20. //严格的来说sBuilder不是方法内的局部变量,它是形参的局部变量,形参也会存在局部变量表中
  1. 这次不传参数,而是返回
  2. //结论:不是线程安全的(有可能存在问题)
  3. public static StringBuilder method3(){
  4. StringBuilder s1 = new StringBuilder();
  5. s1.append("A");
  6. s1.append("B");
  7. ...
  8. return s1;
  9. }
  10. //引用类型和基本类型不用多说了把
  11. //基本类型包括:byte,short,int,long,char,float,double,Boolean,
  12. //引用类型包括:类类型,接口类型和数组。
  13. //因为StringBuilder是类,一返回出去可能被其他位置上的多个线程所调用
  1. 返回StringString有点特殊,因为它具有不变性,看源码是Stringfinal声明
  2. //结论:线程安全的
  3. public static String method4(){
  4. StringBuilder s1 = new StringBuilder();
  5. s1.append("A");
  6. s1.append("B");
  7. ...
  8. return s1.toString();
  9. }

2与3发生了逃逸,作用域不止在方法内部了
1与4未发生逃逸,是安全的


总结: 创建对象不一定是在堆空间上创建,还可以在栈上创建。所以这道题方法中定义的局部变量不一定线程安全。

四、堆空间(面试)

1. 堆的概述

  • 一个JVM实例对应一个进程实例,一个JVM实例有一个运行时数据区(Runtime)
  • 一个Runtime就有一个独立的方法区和堆
  • 一个进程有多个线程,多个线程共享一个方法区和堆空间
  • 每个线程拥有自己独立的程序计数器/本地方法栈/虚拟机栈
  • 为了解决多个线程访问出现线程不安全问题—>TLAB(线程私有空间)
  • 垃圾回收只会在堆(方法区)当中进行回收

    2. 堆内存细分

  • a. 基本划分

    • 新生代:老年代=1:2
    • 新生代=eden:from:to=8:1:1
    • 创建对象在eden区
    • image.png
    • image.png
  • b. jvisualvm插件安装
    • 工具—>插件—>可用插件—>Visual GC
      • image.png
    • idea中安装
      • 安装插件visualvm-launcher
        • image.png
      • 配置jdk路径
        • image.png
  • c. OOM内存溢出
    • OutOfMemoryError: Java heap space
  • d. 内存分配策略
    • 默认对象分配在eden(伊甸园)区
    • 如果一个对象回收次数达到一个阈值就把该对象放到老年代
    • 大对象直接分配到老年代
      • 尽量避免程序中出现过多的大对象
    • 对于体积不大的对象优先分配该对象到eden区的TLAB区
    • 对象还有可能分配在栈空间(逃逸分析)
  • e. TLAB区(重点)
    • 为什么要有TLAB区?
      • 堆空间是线程共享的,在高并发分配内存的时候,容易出现线程不安全问题
      • 为了解决线程不安全问题,采用加锁机制,会影响性能
    • TLAB区的作用
      • TLAB区是线程私有的一块空间,可以避免线程安全问题
      • 多个线程即使同时分配内存,也可以避免一系列的非线程安全问题,提高内存分配的吞吐量,因此我们将这种分配内存的方式称为快速分配策略
      • JVM将TLAB区作为内存分配的首选区域
        • -XX:-UseTLAB,内存空间很小,仅占eden区的1%(可调)
      • image.png
  • f. 图解对象分配过程
    • image.png
    • image.png
    • image.png
    • image.png
    • image.png
    • image.png
    • image.png
  • g. MinorGC/MajorGC/FullGC的对比
    • image.png
    • image.png
    • image.png
    • image.png
  • h. 堆空间分代思想
    • image.png

3. 逃逸分析(亮点、重点)

  • 1. 为什么存在逃逸分析
    • 如果对象在堆内存分配—可能引起GC—导致STW—应用程序卡顿
    • 减少对象在堆内存的分配—减少GC—减少STW—减少卡顿,提升性能
  • 2. 作用
    • 在栈上分配
      • 如果一个对象没有发生逃逸,就可以在栈上分配
        • image.png
      • 随着方法的结束,对象从栈空间被移除(出栈)—不涉及GC—提高程序性能
        • image.png
        • 结论
          • image.png
  • 3. 判断对象是否发生逃逸
    • 当一个对象在方法中被定义之后,对象只在方法内部使用,则认为没有发生逃逸
    • 当一个对象在方法中被定义之后,它被外部方法所引用,则认为发生了逃逸。
      • 例如:作为调用参数传递到其他对方。
  • 4. 逃逸分析优化算法
    • image.png
    • 栈上分配
      • 新建对象尽量写成局部变量
      • image.png
    • 同步省略
      • 同步代码块锁对象只能被一个线程访问,就可以取消同步代码块
      • 取消同步代码块这个过程—锁消除
      • image.png
      • image.png
    • 标量替换
    • image.png
    • image.png
    • image.png
      • 标量
        • 指一个无法被分解的更小的数据结构—java原始数据类型
      • 聚合量
        • 可以进行拆解的—类对象
      • 效果
        • 由于原来需要创建对象,现在不需要了
          • 减少了堆内存分配
          • 减少了内存使用
          • 减少了GC
  • 5. 逃逸分析缺点
    • 由于判定对象是否发生逃逸也需要消耗性能,所以逃逸分析至今还是有一定的缺点,并且技术还没有完全成熟
    • 无法保证逃逸分析的性能消耗一定高于他的消耗。虽然经过逃逸分析可以做到标量替换、栈上分配以及锁消除,但是逃逸分析自身也是需要一系列的复杂分析的,这其实也是一个相对耗时的过程

五、方法区(元空间)(掌握)

image.png

1. 概念

  • 是各个线程共享的一个区域,方法区的大小直接决定系统可以加载多少个类class

    2. 如何设置方法区(元空间)的大小

  • -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

  • 如果方法区不足,就会报错 OutOfMemoryError: Compressed class space

    3. 内部结构

  • 类型信息

    • 类(class)、接口(interface)、枚举(enum)、注解(annotation)等
      • 1、这个类型的完整有效名称(全名=包名+类名)
      • 2、这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
      • 3、这个类型的修饰符(public、abstract、final的某个子集)
      • 4、这个类型直接接口的一个有序列表
  • 域信息
    • JVM必须在方法区保存类型的所有域的相关信息以及域的声明顺序
    • 域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)
  • 方法信息
    • 方法名称
    • 方法的返回值类型(或void)
    • 方法的参数的数量和类型(按顺序)
    • 方法的修饰符public、private、protected、static、final、volatile、transient的某个子集)
    • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
    • 异常表(abstract和native方法除外)
      • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常的类的常量池索引
  • 常量
  • 静态变量、类变量
  • image.png
  • image.png

    4. 方法区垃圾回收

  • 必要又难以让人满意

  • 主要回收常量池里面废弃的常量和不常使用的类型
  • 常量池常量没有被任何地方引用就可以回收

    5. 运行时常量池

  • image.png

  • image.png
  • image.png

六、垃圾回收(理解)

1. 为什么要垃圾回收

  • 如果不进行垃圾回收,内存迟早要消耗完
  • 释放没用的对象,将碎片整理所占用的堆内存移到堆的一端,方便JVM整理内存分配给新的对象
  • 随着应用程序所应付的业务越来越庞大、复杂、用户越来越多,没有GC,就不能保证应用程序的正常执行

    2. 什么是垃圾

  • 在运行程序中没有任何指针指向该对象

    3. 早期垃圾回收方式

  • C/C++时代垃圾回收都是通过手工回收,开发人员可以通过new关键字对内存进行申请,并且使用delete关键字进行内存释放

    4. 自动内存管理

  • 开发人员无需手动参与内存申请与释放,就可以降低内存泄露与内存溢出的风险

    • 优点:让开发人员专注于业务开发
    • 缺点:弱化开发人员解决内存溢出或泄露的能力

      5. 应该关心那些区域进行回收

  • 只有方法区+堆

  • 频繁收集young区
  • 较少收集old区
  • 基本不动perm区/元空间

七、垃圾回收算法

1. 标记

  • 垃圾回收之前,判断对象是存活

    2. 引用计数算法

    image.png

  • 概念

    • 当对象被引用的时候计数器加1
  • 优点
    • 实现简单,垃圾回收对象便于辨识,判定效率高,回收没有延迟性
  • 缺点

    • 致命问题:无法处理循环引用的情况

      3. 可达性分析算法

      image.png
      image.png
  • 概念

    • 从根节点为起始点从上往下搜索根对象所连接对象是否可达
    • 搜索走过的路径被称为引用链
    • 不可达对象称为垃圾
  • GC roots包括以下元素(了解)
    • 方法区中常量引用对象
    • 同步sync关键字持有对象
    • 静态类变量
  • 小技巧

    • 由于root采用栈方式存放变量和指针,所以如果一个指针,他保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就一定是root

      4. 标记清除算法

  • 缺点

    • 效率不算高
    • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
    • 这种方式清理出来的空闲内存是不连续的,产生内存碎片,还需要维护一个空闲列表

      5. 标记压缩算法(老年代)

  • 把存活对象的索引进行移动

    6. MinorGC/MajorGC/FullGC的对比

  • MinorGC

    • 只回收新生代
    • 新生代空间不足的时候,该区域有个特点 对象大部分是朝生夕死
    • 会触发STW 暂停其他用户线程 垃圾收集结束 用户线程才恢复
  • MajorGC
    • 回收老年代
    • 回收速度比MinorGC慢10倍以上 STW时间更长
  • FullGC

    • 回收整个堆与方法区
    • 更应该尽量避免

      7. 复制算法(新生代)

      image.png
      image.png
      image.png
      image.png
  • 步骤

    • 第一次回收是对eden区进行回收
    • 第二次回收是对eden+from作为回收主战场
    • 回收过程中 存活对象还要加1
    • 把from和to进行交换 from变为to to变为from(交换)
  • 优缺点

    • 优点:不会出现空间垃圾碎片
    • 缺点:需要两倍活着对象的空间大小

      8. 分代垃圾回收算法(重重点)

      image.png
  • 新生代和老年代回收算法

  • 新生代回收算法和老年代进行交换

    • 老年代里面的对象经过GC以后大部分都存活 采用复制算法
    • 会产生较大的性能问题

      9. 增量收集算法

      image.png
      image.png
  • 概念

    • 垃圾收集线程每次只收集一小部分内存空间,接着切换到应用线程
    • 依次反复执行完成垃圾收集 可以避免长时间STW
  • 缺点

    • 由于线程进行切换 存在上下文切换开销 造成吞吐量下降

      10. 分区回收算法

      image.png
      image.png
      image.png
  • 把一个内存区域划分为多个内存空间,每次只回收若干个小区域内存

    11. 总结

  • 实际回收要复杂的多,根据具体场景进行选用,目前用的最多的是复合算法

八、垃圾回收日志(会看)