JVM总体概述

JVM内存结构

在这里插入图片描述

1、类的加载过程

在这里插入图片描述

1.1加载(Loading)

1.通过编译生成的class文件 获取类的二进制字节流

2.将这个字节流所代表的静态存储结构 转换为方法区的运行时数据结构

3.在内存中生成一个代表这个类的Class对象(Class模板) 为方法区的这个类的各种数据的访问入口(大Class对象在方法区

1.2连接(Linking)

1.验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。cafebabe

2.准备:为类的静态变量分配内存 并将其赋 默认值

1.为静态变量分配内存 这些内存都在方法区中 只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)

2、 对final修饰的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值。

3.这里不会为实例变量分配初始化 ,类变量(静态变量)会在方法区中 而实例变量是会随着对象一起分配到Java堆中

3.解析 将常量池中的符号引用替换为直接引用(内存地址)的过程

在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。

常量池:存在于方法区

1.3初始化(Initialization)

为类的静态变量赋初值 并执行静态代码块

就是执行类构造器方法() 的过程 此方法不需要定义 是java编译器自动收集类中的所有(static)类变量的赋值动作和静态代码块的语句合并而来

init方法

init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。

  • 定义静态变量时指定初始值。如 private static String x="123"

  • 在静态代码块里为静态变量赋值。如 static{ x="123"; }

    注意:只有对类的主动使用才会导致类的初始化

    静态代码块在加载的时候就被执行,代码块是在创建对象的时候被执行,构造器也是在创建对象的时候被执行,且顺序 静态代码块>代码块>构造器

1.4clinit与init

在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法, 另一个是实例的初始化方法。

clinit

clinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。类比init方法是实例化构造器 也就是给创建的对象进行属性初始化 cinit(class init)是类的构造器 那么也就可以想到是给类中的静态变量初始化 和调用静态代码块

注意事项:

  1. 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。

  2. 在执行clinit方法时,必须先执行父类的clinit方法。

  3. clinit方法只执行一次。

  4. static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。如下代码所示:

init

init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。

注意事项:

  1. 如果类中没有成员变量和代码块,那么clinit方法将不会被生成。

  2. 在执行init方法时,必须先执行父类的init方法。

  3. init方法每实例化一次就会执行一次。

  4. init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块。如下代码所示:

1.5类加载总结

1.加载: 将class文件变为二进制流存进方法区 生成java.lang.Class 对象

2.连接: 为静态变量 静态代码块中的值赋默认值final静态字面值常量直接赋初值

3.初始化:为静态变量 静态代码块中的值 赋初值 执行静态代码块 只加载一次

何时会触发类加载

  1. 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
  2. 当调用某个类的静态方法时
  3. 当使用某个类或接口的静态字段时
  4. 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
  5. 当初始化某个子类时
  6. 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)

关于静态代码块何时会被执行

静态代码块的执行时间

静态代码块会在类加载的最后一个过程即初始化阶段被调用,因为在初始化阶段,回调用类的方法,收集各种赋值语句 详情见1.8静态变量与成员变量的初始化过程

父子类代码块 构造器执行顺序

总结就是当创建一个对象时

(先找父类)父类的静态代码块——> 子类的静态代码块——>父类的非静态代码块——>父类的构造器——>子类的非静态代码块——>子类的构造器

对象的初始化顺序:首先执行父类静态的内容,父类静态的内容执行完毕后,接着去执行子类的静态的内容,当子类的静态内容执行完毕之后,再去看父类有没有非静态代码块,如果有就执行父类的非静态代码块,父类的非静态代码块执行完毕,接着执行父类的构造方法;父类的构造方法执行完毕之后,它接着去看子类有没有非静态代码块,如果有就执行子类的非静态代码块。子类的非静态代码块执行完毕再去执行子类的构造方法。总之一句话,静态代码块内容先执行,接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法。

注意:子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法。如果父类没有不带参数的构造方法,那么子类必须用super关键子来调用父类带参数的构造方法,否则编译不能通过

1.6类加载器

类加载器的作用:

1.通过类加载器将对应类的字节码文件加载到JVM中
2.通过类加载器将字节码文件转换为方法区的Class对象

类加载器的分类

  • 1.BootStrap Class Loader (最上层 获取不到) 引导类加载器 (C/C++ 语言编写 嵌套在JVM内部 只加载Java核心类库 例如String)
  • 2.Extension Class Loader 拓展类加载器
  • 3.System Class Loader 系统类加载器(自定义类默认使用)
        //获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
     // sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取其上层 : 拓展类加载器 调用getParent()获取上层加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
          // sun.misc.Launcher$ExtClassLoader@1540e19d
            
            
        //获取其上层 获取不到引导类加载器
ClassLoader bootstrapClassLOader = extClassLoader.getParent(); 
System.out.println(bootstrapClassLOader);
        //  null
        
        //对于自定义用户来说: 默认使用系统类加载器进行加载
 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
 System.out.println(classLoader);
  // sun.misc.Launcher$AppClassLoader@18b4aac2

  //获取String类的加载器发现获取不到 说明String类是使用引导类加载器加载的
        //---> Java 的 核心类库 都是使用引导类加载器加载的
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);
                    //  null
	

ClassLoader 抽象类 除了BootstarpClassLoader外 其余的所有类加载器都继承于ClassLoader

1.7双亲委派

双亲委派

1.如果一个类加载器收到了类加载请求 他并不会自己先去加载 而是把这个请求委托给父亲的加载器去执行

2.如果父类加载器还存在其父类加载器 则进一步向上委托 请求最终达到顶层的引导类(启动类)加载器

3.如果父类加载器可以完成类加载任务 就成功返回 倘若父类加载器无法完成此加载任务 子加载器才会尝试自己去加载 这就是双亲委派机制

例子: 如果你自己定义一个java.lang.String 类 里面定义了一个main方法 那么无法运行 因为当这个String类在加载的时候 类加载器会向上转换 引导类加载器一看这是 java.lang包下的 就会自己去加载 而真实情况是:我们自己写的类都是由系统类加载器进行加载的 此时引导类加载的实际上还是java真实的Stirng类 而真实地String类中没有main方法 就会报错

双亲委派优势

1、首先,保证了java核心库的安全性。如果你也写了一个java.lang.String类,那么JVM只会按照上面的顺序加载jdk自带的String类,而不是你写的String类。
2、保证同一个类不会被加载多次

1.8类的主动使用与被动使用

在这里插入图片描述

举例:当一个类中 有静态代码块只会在初始化的时候才会执行 所以在创建类的实例 或者调用静态方法 对静态属性赋值的时候导致类被初始化 这时静态代码块才会执行

重点:静态变量的初始化过程 与 成员变量的初始化过程(详细)

参考

>1.静态变量与成员变量的初始化过程

类加载过程分为 加载 ——> 连接——>初始化

1.加载: 将class文件变为二进制流存进方法区 生成java.lang.Class 对象 作为各个数据的访问入口

2.连接 又分为

  • 验证 看一下这个二进制文件是否符合Java虚拟机的规范

  • 准备 为静态变量(只有static修饰的成员变量)分配内存空间 并赋默认值final静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值。

  • 解析: 将常量池中的符号引用替换为直接引用(内存地址)的过程

3.初始化

执行 <cinit> 方法 自动收集所有(static)类变量的赋值动作和静态代码块的语句合并而来

在java类的初始化中,一边只初始化一次,类的初始化主要是用以初始化类变量,即静态变量
在初始化的过程中存在着静态变量与静态块两部分,初始化的顺序为先加载静态变量后加载静态块的内容,静态代码块只在类加载的时候执行且只执行一次

public class StaticTest {
	
    //静态变量的直接赋值 
    public static String name = "张三1"; 
    public static int age;
	
    //静态代码块1
    static {
        name = "张三2";
        age = 1;
    }
    
    public static char sex;
   //静态代码块2
    static {
        name = "张三3";
        age = 2;
        sex = '男';
    }
    
    /* <cinit> 会收集所有的静态变量的赋值动作和静态代码块的赋值动作 即name属性的直接声明时的赋值语句 和
    	静态代码块的赋值语句 统一的进行赋值 赋值按照代码中的赋值顺序 哪个赋值语句在前 就先赋值
    	即 所有的赋值语句按照从上到下的顺序依次在cinit方法中执行
    *  cinit(){
    *       name = "张三1";  -- 收集 public static String name = "张三1"; 后面的赋值语句 这个放在第一个
    *       name = "张三2";  -- 收集静态代码块的赋值语句 name = "张三2"
    *       age  = 1;        -- 收集静态代码块的赋值语句 age = 1
    *       name = "张三3";  -- 收集静态代码块2的赋值语句 name = "张三3"
    *       age  = 2;        -- 收集静态代码块2的赋值语句 age = 2
    *       sex = '男';      -- 收集静态代码块2的赋值语句 sex = '男'
    *  }
    * */
    
    public static void main(String[] args){
        System.out.println(name + " " + age);
    }
}

问题一? 我们都知道 一个变量必须是先声明 才能为其赋值(抛出在声明的时候就赋值)
但是下面的代码为什么不报错

JVM在加载这些代码块和声明变量的时候 都会先加载变量的声明 后加载代码块

不过不同的是

1.静态的变量 在类加载的 连接-准备阶段加载先这些静态变量 为其分配内存空间 然后赋默认值(一定是默认值 除了使用 static final声明的变量 会直接赋初始值)

然后在类加载的第三个过程 初始化时 调用方法 这个方法 收集了静态变量的赋值动作 声明时的直接赋值语句+静态代码块的赋值语句 这两处的赋值语句的加载顺序——>所有的赋值语句按照由上带下的赋值顺序依次执行 如上面代码例子所示

所以说静态代码块的赋值可以写在声明变量之前 因为先加载的是变量的声明 后加载变量的赋值 如果静态代码块写在了声明属性前面 只是赋值顺序改变了 但是如果静态代码块(有赋值操作)写在了声明变量的前面 那么在静态代码块不能使用

所以说 静态代码块的内容只在类加载的时候执行一次 这一点区别普通的代码块 因为静态代码只能在类加载的时候执行 而普通的代码块是在创建对象的时候才加载 我们都知道在创建对象的时候 第一步就是判断这个类是否被加载 如果没有被加载 就先加载类 在方法区生成一个Class对象 如果加载过了 就不加载了 也就不执行静态代码块

static {
    name = "张三3";
    System.out.println(name) //报错 根据代码的执行顺序 name没有被声明 
}
public static String name = "张三1";

或 非静态代码块
{
    name = "张三4";
}  

public String name;

2.成员变量的初始化 大致跟静态的类似

1 public class Person{
 2   {
 3     name="李四";
 4     age=56;
 5     System.out.println("初始化age");
 6     address="上海";
 7   }
 8   public String name="张三";
 9   public int age=29;
10   public String address="北京市";
11   public Person(){ //构造器也可以写在定义变量之前 因为他的作用就是收集各种赋值语句 但是在创建对象的时候构造器里的赋值语句 会放在收集的所有赋值语句之后运行 代码块的赋值和显示的赋值代码顺序按照从上到下执行  
12     name="赵六";
13     age=23;
14     address="上海市";
15   }
16 }

只是在创建对象阶段 没有cinit方法收集 那两种赋值语句  这里用 init方法代替 也就是类的构造器 它
去收集各种赋值语句 按照顺序一一 赋值 这里是 声明是赋值语句与代码块的赋值语句进行顺序执行 构造器本身的
赋值语句最后才会执行 这样就顺承了Java开发人员的意愿 即不管是代码块 还是 声明时的直接赋值 都是一个类内部的
赋值方式 即每个类刚造出来属性都是那一个值 但是我们想要使用不同属性的对象 那么我们在创建对象的时候就可以在构造器里重新的为我们的对象的属性赋值 保证创建出我们所需要的对象
  如上面的代码
 public class Person{
     
     public String name;
     public int age;
     public String address;
     public Person(){
        name="李四";  //代码块
     age=56;    //代码块
     System.out.println("初始化age"); //代码块
     address="上海";
         name = "张三";
         age = "29";
         address = "北京";
          name="赵六";
     age=23;
    address="上海市";
         
     }
 }   

特殊的代码块声明在变量之前可以使用变量的

public class Test2 {
    {
        a = 4;
        a += 1; // 这里可以使用a 因为a是静态的 类加载的时候就已经声明并且已经初始化了 而代码块是在创建对象的时候才会执行的 
        System.out.println("代码块"+a); 
    }
    private  static int a; // 必须是静态变量 如果a不是静态报错 2.如果a不是静态变量 那么在静态代码块里使用a也报错
    public static void main(String[] args){
        Test2 test2 = new Test2();
        System.out.println(test2.a);
    }
}

父类的 静态代码块 非静态代码块 构造器 子类的静态代码块 非静态代码块 构造器 在创建子类对象的时候的执行顺序

1.父类的 静态代码块
2.子类的静态代码块
3.父类的代码块
4.父类的构造器
5.子类的代码块
6.子类的构造器

解答:先把这六项分为两部分 一部分是父类的 一部分是子类的

然后分析 静态代码块是在类加载的时候就执行的 每个类的方法收集静态变量的直接赋值语句和静态代码块的赋值语句 我们创建一个子类对象 第一步就是去检查是否加载过子类和父类 如果没有会先加载父类所以父类的静态代码块会第一个执行,然后去加载子类即子类的静态代码块第二个执行,然后分析 代码块和构造器的执行顺序,由上述的知识,我们知道在创建对象的时候 方法会收集所有的非静态成员变量的赋值语句和非静态代码块的所有语句以及构造器内容 且构造器的语句会最后执行,所以在创建对象的时候代码块会先于构造器执行,而我们又知道子类的构造器会默认先调用父类的构造器 所以说在创建子类的对象的阶段 父类的非静态代码块先于父类的构造器执行,然后最后执行子类的代码块,子类的构造器

由上述我们可以知道 一个变量在声明时直接赋值跟在代码块中为其赋值是没有什么区别的 因为最后不管是或是 都会收集两种赋值语句统一赋值 在默认初始化的时候都只会赋类型的默认值(除了final) 所以我们下面的代码

class Test{
public  final  int a; //这里如果没有下面的代码块 就报错 因为final类型的变量要求对象在创建出来的时候就为其赋值
                      //且只能赋一次值 所以这里的a我们在声明时赋值 在代码块里也赋值 就报错 谁的顺序在后谁报错
    {
        a=2;
    }
}

2.运行时数据区(Running Data Area)

2.1程序计数器(Program Counter Register)

每个线程独有 无GC不会报异常

程序计数器用来存储指向下一条指令的地址 即将要执行的指令代码 由执行引擎读取下一条指令

他是程序控制流的的指示器 分支循环 跳转 异常处理 线程恢复等基础功能都需要依赖这个计数器来完成

字节码解释器工作时就是通过改变这个技术器的值来选取下一条需要执行的字节码指令

他是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域

2.1.1两个常见问题

使用PC寄存器存储字节码文件指令地址有什么用呢 ?

为什么使用PC寄存器记录当前线程的执行地址呢 ?

因为CPU需要不停地切换各个线程 这时候切换回来后 就得知道从哪开始继续执行

JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

PC寄存器为什么会被设定为线程私有的?

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个Pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互千扰的情况。
由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复, 如何保证分毫无差呢 每个线程在创建后 都会产生自己的程序计数器和栈帧 程序计数器在各个线程之间互不影响。

2.2虚拟机(Java)栈

每个线程独有 栈生命周期跟线程一致 不存在GC 存在OOM

2.2.1 虚拟机栈主要特点

每个线程在创建的时候都会创建一个虚拟机栈 其内部保存一个个的栈帧 (Stack Frame) 对应着一次次的方法调用

作用:主管Java程序的运行 它保存方法的局部变量 部分结果 并参与方法的调用和返回

栈的特点:JVM直接对Java栈的操作只有两个

  • 每个方法执行 伴随着进栈 (进栈 压栈)
  • 执行结束后的出栈工作

栈不存在垃圾回收

2.2.2虚拟机栈的常见异常与如何设置栈的大小

在这里插入图片描述

2.2.3栈的存储结构和运行原理

栈中存储什么

  • 每个线程都有自己的栈 栈中的数据都是以栈帧的格式存在

  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)

  • 栈帧是一个内存区块 是一个数据集 维系着方法执行过程中的各种数据信息

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作

  • 如果在该方法中调用了其他方法 对应新的栈帧会被创建出来 放在栈的顶端 成为新的当前帧

  • 不同线程所包含的栈帧是不允许存在相互引用的 即不可能在一个栈帧之中引用另外一个线程的栈帧

  • 如果当前方法 调用了其他方法 方法返回之际 当前栈帧会传回此方法的执行结果前一个栈帧 接着虚拟机会丢弃当前栈帧 使得前一个栈帧重新成为当前栈帧

  • Java方法有两种返回函数的方式 一种是正常的函数返回 使用return指令 另一种是抛出异常 不管使用哪种方式 都会导致栈帧被弹出


在这里插入图片描述

2.2.4栈帧的内部结构

在这里插入图片描述

2.2.4.1栈帧组成–局部变量表(Local Variables)

定义:一个数字数组:主要用于存储方法参数和定义在方法体内的局部变量 这些数据类型包括各类的基本数据类型

对象引用(reference) 以及returnAddress类型 使用索引记录

  • 由于局部变量表是建立在线程的栈上 是线程的私有数据 因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译器确定下来的 并保存在方法的Code属性的maxmum local variables数据项中 在方法运行期间是不会改变局部变量表的大小
  • 局部变量表中的变量只在当前方法调用中有效 在方法执行时 虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程 当方法调用结束后 随着方法栈帧的销毁 局部变量表也会随之摧毁
方法的字节码字段详解
public class Maintest {
    public void method1() {
        int a = 1;
        int b = 23;
        int c = 2;
        String str = "ss";
        double d = 2.3;
        Test test = new Test();
    }
}

点击method1

在这里插入图片描述

点开method1 点击code

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

点击LocalVariableTable 这个就是局部变量表 只存变量名 Name 绿色的链接指向了class常量池中的变量名

Descriptior 指向了class常量池中的类型的名字

在这里插入图片描述

局部变量表举例

静态方法举例

 public static void method1() {
        int a = 1;
        int b = 23;
        int c = 2;
        String str = "ss";
        double d = 2.3;
        Test test = new Test();
    }

在这里插入图片描述

非静态方法举例

  public  void method1() {
        int a = 1;
        int b = 23;
        int c = 2;
        String str = "ss";
        double d = 2.3;
        Test test = new Test();
    }

在这里插入图片描述

由上面分析可以得出 在每个非静态方法中的局部变量表中 都有一个this变量 而静态方法的局变量表中没有this

所以可以明白 为什么静态方法中不允许使用类中的非静态属性 没有this无法引用非静态属性

变量的分类及变量的初始化过程

按照数据类型分类 1.基本数据类型 2.引用数据类型

按照在类中声明的位置分

1.成员变量在使用前 都经历过默认初始化赋值

类变量:Linking的Prepare阶段 给类变量默认赋值 -->initial(初始化)阶段 给类变量显示赋值 即静态代码块赋值

实例变量:随着对象的创建 会在堆空间中分配实例变量空间 并进行默认赋值

2.局部变量:没有默认赋值的过程 在使用前必须要进行显示赋值 否则编译不能通过

>slot槽的理解
2.2.4.2栈帧组成–操作数栈(Operand Stack数组实现)
  • 我们说Java虚拟机的解释引擎是基于栈的执行引擎 其中的栈指的就是操作数栈

  • 主要用于保存计算过程的中间结果 同时作为计算过程中变量临时的存储空间

  • 操作数栈就是JVM执行引擎的一个工作区 当一个方法刚开始执行的时候 一个新的栈帧也会随之被创建出来 这个方法的操作数栈是空的

  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值 其所需的最大深度在编译期就定义好了 保存在方法的Code属性中 为max_stack的值

  • 操作数栈中的任何一个元素都是可以任意的Java数据类型

    32bit的类型占用一个栈单位深度

    64bit的类型占用两个栈单位深度

  • 操作数栈并非采用访问索引的方式来进行数据访问的 而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问

2.2.4.2.1反编译代码追踪 操作数栈 局部变量表 指令

https://blog.csdn.net/hudashi/article/details/7062675

push const 是把数字放入操作数栈中

store是把操作数栈中的数字放到 局部变量表中

load是把局部变量表中的数据放到操作数栈中

add数字相加(此时处理后的数据还在操作数栈中)

ldc 该系列命令负责把数值常量或String常量值从常量池中推送至栈顶

2.2.4.2.2栈顶缓存技术

在这里插入图片描述

2.2.4.3栈帧组成–动态链接(Dynamic Linking)
  • 每一个栈帧内部都包含一个指向 运行时常量池(在方法区Method Area) 中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 比如 invokedynamic指令

  • 在Java源文件被编译到字节码文件中 所有变量和方法引用都作为符号引用(Symbolic Reference) 保存在 class文件中的常量池(非方法区的运行时常量池) 中 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的 那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

每个栈帧都保存了一个可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接

类的加载过程中 解析这一步 是将常量池中的符号引用变为直接引用 动态链接是将栈帧中的指向常量池的符号引用变为直接引用

在这里插入图片描述

2.2.4.3.1方法的调用_虚方法_虚方法表(方法区)

PPT162

方法重写的本质 PPT_171

虚方法表 PPT_172

  • 在面向对象的编程中,会很频繁的使用到动态分配,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率 因此为了提高性能 JVM采用在类的方法区建立一个虚方法表 (Virtual method table) 来实现 使用索引表来代替查找

  • 每个类中都有一个虚方法表 表中存放着各个方法的实际入口

何时被创建:

虚方法表会在类加载的链接阶段被创建并开始初始化 类的变量初始值准备完成之后 JVM会把该类的方法表也初始化完成

2.2.4.4栈帧组成–方法返回地址(return Address)

PPT_181

2.2.4.5虚拟机栈面试题

2.3堆Heap(线程共享)

2.3.1堆的核心概述

  • 一个JVM实例只存在一个堆内存 堆是Java内存管理的核心区域 一个进程一个堆

  • Java堆区在JVM启动的时候即被创建 其空间大小也就确定了 是JVM管理空间最大的一块内存空间

  • 堆内存可以调节 Edit Configuration :

    1.-Xms10m(调节最小内存) -X 是JVM的运行参数 ms是memory start

    2.-Xmx10m(调节最大内存

  • 所有的线程共享Java堆 在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer 即TLAB)

  • Java中的对象实例和数组都存在堆中 栈帧保存这个对象或者数组的引用

2.3.2堆内存细分

在这里插入图片描述

堆区

  • 年轻代

    1.Eden区

    2.Survivor1 幸存者1区

    3.Survivor2 幸存者2区

  • 老年代

  • 永久代或者元空间 就是现在的方法区
    在这里插入图片描述

查看参数 视频P69

2.3.3OOM(OutOfMemoryError)举例

代码

2.3.4新生代与老年代

相关参数设置视频P71

默认

  • 新生代占堆空间的1/3 老年代占堆空间的2/3

  • **新生代中的Eden区比Survivor1比Survivor2比例是 8:1:1 但是这个会有默认自使用内存分配机制 看的话一般都是6:1:1 **

  • 几乎所有的Java对象都是在Eden区(新生代)区里被new出来的

  • 绝大部分的Java对象的销毁都是在新生代进行的

  • 可以使用选项"-Xmn"设置新生代最大内存大小(一般使用默认值即可)

2.3.5对象分配的一般过程

为新对象分配内存是一件非常严谨和复杂的任务 JVM的设计者们不仅需要考虑内存如何分配 在哪里分配的问题

并且由于内存分配算法与内存回收算法密切相关 所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片

主要过程

1.new的对象放在堆中的新生代中的Eden区中 (有特殊情况 当对象过大时 直接放进老年区) 此区有大小限制

2.当Eden区的内存满了 程序又创建了对象 JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC) 将Eden区中的不再被其他对象所引用的对象进行销毁 然后将剩余的对象移动到一个空的Survivor1区中 (这时给幸存下来的对象都给一个年龄为1 如果下次再次垃圾回收 这个对象还没有被销毁被放进了另一个幸存者区 这个年龄会继续增加) 然后就把这个新创建的对象放到Eden区中

3.如果再次触发垃圾回收 此时幸存下来的放到Survivor1区的对象放到Survivor0区中

4.如果再次经历垃圾回收 此时会重新放到Survivor1区

5.当这些对象一直没有被回收 年龄等于15的时候 这些对象就会去老年区

6.老年区发生的GC的次数一般会少 当养老区内存不足的时候 就会触发Maior GC 进行老年区的内存清理

7.在老年区执行了Maior GC之后发现依然无法进行对象的放入 就会产生OOM异常

注:新生代的三个区 Eden区 S0 S1区 只有Eden区满的时候才会触发Minor GC S0和S11区满的时候不会触发Minor GC

2.3.5.1GC的简单概述

JVM在进行GC时 并非每次都对 新生区(包括Eden区 S0区 S1区) 老年区 方法区一起回收 大部分时候回收都是指新生代 针对HotSpot VM的实现 它里面的GC按照回收区域又分为两大中类型:

  • 一种是部分收集(partial GC)
  • 一种是整堆收集(Full GC)

部分收集 不是完整的收集整个Java堆的垃圾收集 其中又分为:

  • 新生代收集: (Minor GC / Young GC) :只是新生代的垃圾收集
  • 老年代收集: (Major GC / Old GC) :只是老年代的垃圾收集

注意:很多时候Major GC会和 Full GC混淆使用 需要具体分辨是老年代回收还是整堆回收

年轻代GC(Minor GC)触发机制:

当年轻代空间不足时 就会触发Minor GC 这里的年轻代满 值得是Eden区满 Survivor区满不会引发GC

每次MinorGC会清理年轻代的内存 Minor GC非常频繁 一般回收速度也比较快 但是 Minor GC会引发STM

暂停其他用户的线程 等垃圾回收结束 用户线程回复运行

在这里插入图片描述

老年代GC(Major GC)触发机制

  • 指发生在老年代的GC 对象从老年代消失 我们说Major GC 或者 Full GC 发生

  • 出现了Major GC 经常会伴随至少一次的Minor GC (非绝对的 在Parallel Scavenge的收集器的收集策略里就有直接进行Major GC的策略选择过程) 意思就是在老年代空间不足时 会先尝试触发Minor GC 如果之后空间还不足 则触发Major GC

  • Major GC的速度一般会比Minor GC慢十倍以上 STW的时间更长

  • 如果Major GC后 内存还不足 就报OOM

Full GC触发机制

调用System.gc()时 系统建议执行Full GC 但是不必然执行

老年代空间不足

方法区空间不足

通过Minor GC 后进入老年代的平均大小小于老年代的可用内存

Full GC是开发或调优尽量避免的 这样暂停时间会短一些

2.3.6为对象分配内存TLAB(Thread Local Allocation Buffer)

堆区是线程共享区域 如何线程(一个进程中的)都可以访问到堆区的共享区域

由于对象实例的创建在JVM中非常频繁 因此在并发环境下从堆区中划分内存空间是线程不安全的 这里的不安全指的是不同线程占用相同的内存地址 为避免多个线程操作同一地址 需要使用加锁等机制 进而影响分配速度

什么是TLAB?

从内存模型而不是垃圾收集的角度 对Eden区继续进行划分 JVM为每个线程在Eden中分配一个私有的缓存区域

(注意:私有的意思是在这个线程创建对象的时候这块区域这个线程专属 其他线程不能占用 但是创建好的对象其他线程可以使用)

多线程同时分配内存时 使用TLAB可以避免这一犀利的非线程问题 同时还能够提升内存分配的吞吐量 因此我们可以将这种内存分配方式称为快速分配策略

  • 尽管不是所有的对象实例都能在TLAB中成功分配内存 但JVM确实是将TLAB作为内存分配的首选

  • JVM默认是开启TLAB的

  • 默认情况下 TLAB空间的内存非常小 仅占整个Eden空间的 1% 我们可以使用选项:"-XX:TLABWasteTargetPercent" 设置TLAB空间所占用Eden空间的百分比大小

  • 一旦对象在TLAB空间分配内存空间失败 JVM就会尝试着通过 加锁 机制确保数据操作的原子性 从而直接在Eden空间中分配内存
    在这里插入图片描述

2.3.7堆空间常用的参数设置

在这里插入图片描述

在这里插入图片描述

2.3.8拓展-堆是分配对象的存储的唯一选择吗_逃逸分析

在《深入理解Java虚拟机》 中关于Java堆内存有这样一段描述:随着JIT编译器的发展与逃逸分析技术的逐渐成熟,

栈上分配 标量替换优化技术将会导致一些微妙的变化 所有的对象都分配到堆上 也渐渐变得不那么绝对了

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识,但是有一些特殊情况,—那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就有可能被优化成栈上分配,这样就无需在堆上分配内存,也无需进行垃圾回收 这也就是常见的堆外存储技术

如何将堆上的对象分配到栈 需要用到逃逸分析手段

什么是逃逸分析:

  • 当一个对象在方法中被定义后 对象只在方法内部使用 则认为没有发生逃逸
  • 当一个对象在方法中被定义后 它被外部方法所引用 则认为发生了逃逸 例如作为调用参数传递到其他地方中

在这里插入图片描述

结论: 开发中能使用局部变量的 就不要使用在方法外定义

2.3.8.1代码优化

1.栈上分配

实际上 Hotspot还没有实现栈上分配 之所以优化是因为标量替换+逃逸分析都开启了 主要原因是JVM默认开启标量替换 优化的原因也是因为标量替换

代码分析:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            create();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为: " + (end - start) + " MS");
        TimeUnit.SECONDS.sleep(1000);
    }
	//此方法不会发生逃逸
    static void create() {
        A a = new A();
    }
}
class A {
    
}

测试一: 关闭逃逸分析(JVM是默认开启的)使用 -XX:-DoEscapeAnalysis 开启GC的打印细节

经过测试发现 最后一波还没有进行GC堆中A的对象存在 1,885,671 达100多万

在这里插入图片描述

并且发生了两次GC 执行时间为70ms

过程

关闭了逃逸分析 即所有的Java对象都会在堆上创建 每个方法都是一个栈帧 在循环一千万次创建对象时 每一次都会在堆中创建一个对象 对象的引用在栈的栈帧中 方法调用完栈帧出栈 即创建的对象的引用出栈 即这个对象没有任何一个引用去指向他 当Eden区满了 就会发生Minor GC 那些在栈中没有引用的对象就被回收 只要Eden区满了就会进行GC 当方法调用完了 对象的引用也就没了 所以到最后A的对象都会被回收


测试二: 开启逃逸分析 -XX:+DoEscapeAnalysis

经过测试发现 最后一波还没有进行GC堆中A的对象存在 100,843 只有10万

在这里插入图片描述

过程中没有发生过GC

过程

由于开启了逃逸分析(JVM默认是开启的) 也就是说不是所有的对象都是在堆中分配的 有的分配在栈中 随着方法的调用完成就出栈了 那些在堆中的对象放入堆中的Eden区的时候空间是够用的 没有发生GC 但是随着线程一直在执行 最终线程产生的其他垃圾会挤满Eden区 就会发生Minor GC 而这些对象都是没有被引用的 所以到最后A的对象会被全部回收

2.同步省略

视频_P84

3.标量替换

标量(Scalar)就是指一个无法再分解成更小的数据的数据 Java的原始数据类型就是标量 相对的那些还可以分解的数据叫做聚合量(Aggregate) 例如我们写的类

在JIT阶段 如果经过逃逸分析 发现一个对象不会被外界访问的话 那么经过JIT优化 就会把这个对象拆解成若干个其中包含的若干个成员变量来替代 这个过程就是标量替换

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

标量替换是基于逃逸分析的 也就是说经过逃逸分析如果是一个为逃逸的方法 才会使用标量分析

​ 本次的实验逃逸分析一直开着作为不变 一次开启标量替换**(-XX:+EliminateAllocations)**一次关闭 发现结果也是 相差十倍 但是由上面的栈上分配不是主要开启了逃逸分析就有栈上分配吗 那么为何在我们开启逃逸关闭标量替换的情况下我们还是会相差那么多呢

原因就是JVM根本没有使用栈上分配技术 我们的栈上分配实验实际上是标量替换引起的!!!

如下图:

在这里插入图片描述

2.3.9堆总结

在这里插入图片描述

2.4方法区(Method Area)

2.4.1栈 堆 方法区的交互关系

Person person = new Person()

在这里插入图片描述

Metaspace元空间也叫方法区
在这里插入图片描述

2.4.2方法区的理解

《Java虚拟机规范》中明确说明:“尽管方法区在逻辑上是属于堆的一部分 但一些简单的实现可能不会选择去进行垃圾回收或者进行压缩” 但对于HotSpotJVM而言 方法区还有一个别名叫做 Non-Heap(非堆) 目的就是要和堆分开

所以 方法区看作是一块独立于堆的内存空间

  • 方法区(Method Area)与堆一样 是各个线程共享的内存区域
  • 方法区在JVM启动的时候被创建 并且它的实际的物理内存空间中和Java堆一样都可以是不连续的
  • 方法区的大小 跟堆一样 可以选择固定大小或者可拓展
  • 方法区的大小决定了系统可以保存多少个类 如果系统定义了太多的了类 导致方法区溢出 虚拟机同样会抛出内存溢出的错误 java.lang.OutofMemoryError: Metaspace(以前叫做永久代PermGen space)
  • 关闭JVM 就会释放这个区域的内存

HotSpot中方法区的演进

  • 在JDK7及以前 习惯上把方法区成为永久代(PermGen space ) JDK8开始 使用元空间(Metaspace)取代了永久代
  • 本质上 方法区和永久代并不等价 仅是对Hotspot而言的 方法区就像是一种规范 而不论是永久代还是元空间都是对方法区的一种实现
  • 元空间的本质和永久代类似 都是对JVM规范中方法区的实现 不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中 而是使用本地内存 永久代.元空间二者并不只是名字变了 内部结构也调整了
  • 根据《JVM虚拟机规范》的规定 如果方法区无法满足新的内存分配需求 将会抛出OOM异常

2.4.3设置方法区大小与OOM

(JPS) 查看正在运行中的Java进程号

(jinfo -flag 参数 进程号)查看想要看到进程的参数值

​ 方法区的大小不必是固定的 JVM可以根据应用的需要动态调整

 JDK7及以前

​ 通过: -XX:PermSize来设置永久代初始分配空间 默认值是20.75M

-XX:MaxPermSize来设置永久代最大可分配空间 32位机器默认是64M 64位机器默认是82M

​ 当JVM加载的类的信息容量超过了这个值 会报异常OutOfMemoryError:PermGen space

JDK8及以后
  • ​ 元空间大小可以使用-XX:MetaspaceSize 和 -XX:MaxMetaspace指定 替代上述原有的两个参数
  • ​ 默认值依赖于平台 Windows下 默认大小是21M 最大无限制
  • ​ 与永久代不同 如果不指定大小 默认情况系 虚拟机会耗尽所有的可用系统内存 如果元空间区发生移溢出 虚 拟机一样会抛出OOM异常

解决这些OOM
在这里插入图片描述

2.4.4方法区的内部结构

java代码经过编译形成class文件

方法区存储内容描述如下 它用于存储已被虚拟机加载的类型信息 常量 静态变量 即使编译器编译后的代码缓存等

对每个加载的类型(类class 接口interface 枚举enum 注解annotation) JVM必须在方法区中存储一下类型信息

类型信息

1.这个类型的完整有效名称 (全名=包名.类名)

2.这个类型直接父类的完整有效名(interface或者是Object类 都没有父类)

3.这个类型的修饰符

4.这个类型直接接口的一个有序列表

属性(Field)信息

1.JVM必须在方法区保存类型的所有属性的相关信息以及属性的声明顺序

2.属性的相关信息包括:属性名称 属性类型 属性修饰符

方法信息

  • 方法名称
  • 方法的返回类型
  • 方法参数的数量和类型
  • 方法的修饰符
  • 方法的字节码 操作数栈 局部变量表及大小(abstract和native方法除外)
  • 异常表(abstract和native方法除外)

non-final的类变量

静态变量和类关联在一起 随着类的加载而加载 他们成为类数据在逻辑上的一部分

类变量被类的所有实例共享 即使没有类实例也可以访问

全局常量:static final

被声明为final的静态变量的处理方法则不同 每个全局变量在编译的时候就会被分配

2.4.5常量池与运行时常量池

PPT_298

方法区内部包含运行时常量池

在这里插入图片描述

2.4.6方法区在JDK1.6 1.7 1.8中的演变细节

1.6之前及之前 有永久代(Permanent generation) 静态变量存放在 永久代

1.7有永久代 但已经逐步“去永久代” 字符串常量池 静态变量从永久代转为保存在堆里

1.8永久代移除 替换为 元空间(Metaspace) 类型信息 字段 方法 (static final)常量保存在本地内存的元空间

字符串常量池 静态变量 仍在堆

2.4.7永久代为什么要被元空间替换

随着Java8的到来 HotSpot VM中再也见不到永久代了 替换为元空间(Metaspace) 内存也有虚拟机内存变为了与堆不相连的本地内存 这项改动很有必要 原因是:

1.为永久代设置空间大小是很有必要的 在某些场景下 如果动态加载的类过多 容易产生永久代的OOM 比如某个实际的Web工程中 因为功能点比较多 在运行过程中 要不断动态加载很多类 经常出现致命错误 而元空间与永久代之间最大的区别在于:元空间并不在虚拟机中 而是使用的本地内存 因此默认情况下 元空间的大小仅受本地内存限制

2.对永久代进行调优是很困难的

2.4.8StringTable(字符串常量池为什么要调整)由方法区的运行时常量池直接放进了堆中

JDK7以前字符串常量池在方法区的运行时常量池中

JDK7将字符串常量池放在了堆空间中 因为永久代(以前的永久代与堆中连接 的回收率很低 在full GC 的时候才会触发 而full gc是老年代的空间不足 永久代不足时才会触发 这就导致StringTable 回收效率不高 而我们开发中会有大量的字符串被创建 回收率低 导致永久代内存不足啊 能及时回收

简单来说就是:放在方法区中 发生GC的频率低 但是Stirng字符串的回收是很频繁的 所以就直接放进了堆中

2.4.6静态变量存在于堆中及各种变量的存放位置

Object obj = new Object

对于对象本身来说只只看

public class Test {
    //这个person引用会跟着Perosn对象的实例存在堆中 因为类的成员变量都在堆中
    Person person = new Person();
    //jdk8后静态的引用也会存在于堆中 jdk6在之前在方法区 jdk7之后和其他的成员变量一样分配在堆空间   就是这个personStatic
    static Person personStatic = new Person();
    //person引用存在say方法栈帧的局部变量表中 局部变量都在栈中
    void say() {
        Person person = new Person(); 
    } 
}

2.4.7方法区的垃圾收集

方法区存在垃圾回收 但是回收效果比较令人难以满意 尤其是类型的卸载 条件相当苛刻

方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型

方法区的常量池中主要存放的两大类常量 :字面量和符号引用

字面量如文本字符串 被声明为final的常量值等

在这里插入图片描述

2.4.8面试题及总结

视频_P101
在这里插入图片描述

3.本地方法接口

本地方法栈(线程私有)

本地(Native)方法是用C语言实现的

它的具体做法是Native Method Stack(本地方法栈)中登记native方法 在Execution Engine(执行引擎)执行时加载本地方法库

4.对象的实例化内存布局与访问定位

4.1对象的实例化

在这里插入图片描述

对象的创建过程

  • 1.加载类
  • 2.在堆中为对象分配内存
  • 3.处理并发问题(内存占用)
  • 4.属性的默认初始化(这里指的是实例变量的初始化 静态变量在类加载的时候就初始化完成)
  • 5.设置对象头的信息(4.2对象的内存布局)
  • 6.调用init方法为对象显示初始化

关于第三步处理并发问题:
有两种方式

  • 1.对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
  • 2.本地线程分配缓冲TLAB(Thread Local Allocation Buffer)。
    把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为TLAB,哪个线程要分配内存,就在哪个线程的TLAB中分配,只有本地的缓冲区用完了,分配新的缓冲区时才需要同步锁定。

4.2对象的内存布局

在这里插入图片描述

内存布局:

1.对象头

​ 1.运行时数据区(一些对象本身的信息)

  • ​ 哈希值
  • ​ GC分带年龄
  • ​ 锁状态标志
  • ​ 线程持有的锁
  • ​ 偏向线程id
  • ​ 偏向时间戳

​ 2.类型指针 :指向了方法区这个对象是哪个类

2.实例数据

3.对齐填充

public class Customer {
    int id = 1001;
    String name = "xxx";
    Account acct = new Account();
   
     public static void main(String[] args){
        Customer cust = new Customer();
    }
    
}
class Account{
    
}

​ 当在main方法中new了一个Customer对象时 首先在main方法的栈帧的局部变量表中 有两个变量 一个是args 一个是cust这个引用 指向了堆空间中的对象

在堆空间中 这个对象包含了运行时元数据存储哈希值 锁等信息 类型指针指向了这个对象在方法区的类的元信息 还有这个类的实例数据 (id:1001 name:(因为字符串常量池的原因)name指向了字符串常量池中的值 acct:指向了Account的实例对象)

Account的实例对象结构都一样 它也有一个类型指针指向了方法区的Account的元信息

根据代码表示类中各个信息的占用位置

在这里插入图片描述

4.3对象的访问定位

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?

通过栈帧中的引用存的是对象的地址指向了这个对象 对象的对象头里有元数据指针指向了方法的类信息

在这里插入图片描述

访问对象的方式主要有两种:

句柄访问:

在Java堆里面维护一个句柄池 引用指向这个句柄池 句柄池中有两个指针一个指向对象 一个指向方法区中对象类型数据

在这里插入图片描述

直接指针:

Hospot虚拟机默认是使用的直接指针 栈帧中的引用直接指向对象 在对象的对象头中有一个类型指针指向了方法区的对象的类的信息
在这里插入图片描述

两种方式的好处

  1. 使用句柄来访问的最大好处就是引用中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要被修改
  2. 使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。

4.4直接内存

  1. 不是Java虚拟接运行时数据区的一部分 也不是Java虚拟机规范中定义的内存区域
  2. 直接内存是在Java堆外的 直接向系统申请的内存空间
  3. 来源于NIO(no-blocking IO) 通过存在堆中的DirectByteBuffer操作native本地内存
  4. 通常 访问直接内存的速度会快于Java堆 即读写性能好

​ - 因此出于性能考虑 读写频繁的场合可能会考虑使用直接内存

​ - Java的NIO库允许Java程序使用直接内存 用于数据缓冲区

  1. 也可能导致OOM

  2. 由于直接内存在Java堆外 因此它的直接大小不会受限于-Xmx指定的最大堆内存 但是系统内存是有限的 Java堆和直接内存的总和依然受限于操作系统给出的最大内存

  3. 缺点:

  • 分配回收成本较高
  • 不受JVM内存管理回收
  1. 直接内存大小可以通过MaxDirectMemorySize设置

  2. 如果不指定 默认与堆的最大值-Xmx参数值一致

读写文件 需要与磁盘交互 需要由用户态切换到内核态 在内核态需要内存如下的操作 使用IO 这里两份内存存储重复数据效率低
在这里插入图片描述

使用NIO时 如下图 操作系统划出的直接缓冲区可以被Java代码访问 只有一份 NIO适合对大文件操作

在这里插入图片描述

5.执行引擎

5.1执行引擎概述

  • 执行引擎是Java虚拟机核心的组成部分之一
  • Java虚拟机的主要任务是负责装载字节码到其内部 但字节码并不能够直接运行在操作系统上 因为操作系统只能识别机器指令(0 1) 字节码内部仅仅包含一些能够被Java虚拟机所识别的字节码指令 符号表 以及其他辅助信息
  • 那么 如果想要让一个Java程序运行起来 执行引擎(Execution Engine) 的任务就是将字节码指令解释/编译成本地机器指令才可以运行 简单来说 执行引擎充当了将高级语言翻译成机器语言的译者

如图

在这里插入图片描述

工作过程

在这里插入图片描述

5.2Java代码编译和执行的过程

问题:什么是解释器(Interpreter) 什么是JIT(Just In Time)编译器(即时编译器)

解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行 将每条字节码文件中的内容翻译为对应平台的本地机器指令执行

JIT编译器:就是Java虚拟机将class文件直接编译成和本地机器平台相关的机器语言

为什么说Java是半编译半解释型语言

JDK1.0时代 将Java语言定位为解释执行还是比较准确的 在后来 Java也发展出可以直接生成本地代码的编译器

现在JVM在执行Java代码的时候 通常都会讲解释执行与编译执行二者结合起来

5.3机器码 指令 汇编语言

机器码

  • 各种用二进制编码(0 1)方式表示的指令 叫做机器指令码 开始 人们就用它采编写程序 这就是机器语言
  • 机器语言虽然能够被计算机理解和接受 但和人们的语言差别太大 不易被人们理解和记忆 并且用它编程容易出差错
  • 用它编写的程序一经输入计算机 CPU直接读取运行 因此和其他语言编的程序相比 执行速度最快。
  • 机器指令与CPU紧密相关,所以不同种类的CPU所对应的机器指令也就不同。

指令

  • 由于机器码是有o和1组成的二进制序列,可读性实在太差,于是人们发明了指令
  • 指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如movinc等),可读性稍好
  • 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同。

汇编语言

  • 由于指令的可读性还是太差,于是人们又发明了汇编语言。
  • 在汇编语言中 用助记符(Mnemonics)代替机器指令的操作码 用地址符号(Symbol)或标号(Labe1)代替指令或操作数的地址。
  • 在不同的硬件平台 汇编语言对应着不同的机器语言指令集 通过汇编过程转换成机器指令。
  • 由于计算机只认识指令码 所以用汇编语言编写的程序还必须翻译成机器指令码 计算机才能识别和执行。
    在这里插入图片描述

Java文件编译后的字节码需要再次经过编译或者解释后才能变成机器指令

5.4解释器

解释器就是逐条解释字节码变为机器指令然后执行程序

解释器真正意义上所承担的角色就是一个运行时的翻译者 将字节码文件中的内容翻译为对应平台的本地机器指令执行

当下一条字节码指令被解释执行完成后 接着在根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作

由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的,比如Python、Perl、Ruby等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些C/C++程序员所调侃

为了解决这个问题,JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。

不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。

5.5JIT编译器

Java代码的执行分类

第一种是将源代码编译成字节码文件 然后在运行时通过解释器将字节码文件转换为机器码执行

第二种是编译执行(直接编译成机器码) 现代虚拟机为了提高执行效率 会采用即时编译技术 将方法编译成机器码后字后执行

简单来说就是 解释器时逐条编译逐条执行 编译器是把找到所有代码热点都编译后在执行

在这里插入图片描述

热点代码探测

PPT_398

方法调用计数器

回边计数器

6.StringTable(字符串常量池)

美团文章关于intern()JDK6 7 的不同

参考

Java8字符串常量池

参考

1.8字符串底层是char数组 1.9底层是byte数组

1.final

     /* 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
     如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。*/
     final String str = "a";
     str = "x"; //报错 因为这个str用了final修饰 就不能在去引用其他的字符串 

2.字面量创建 与 new的区别

		//使用字面量创建 会在字符串常量池中创建
        String str = "字符串";
		//使用new String("xx")来创建字符串 实际创建了两个对象 一个在堆中 一个在常量池
        String str1 = new String("字符");       

2.1String的两种构造器的不同

       //这种创建会在常量池中生成一个对象 
        String str1 = new String("字符");
         //这种不会 因为在编译期对象不确定 只会在堆空间中创建一个对象
        char[] c = new char[]{'中','国','人'};
        String str2 = new String(c);

3.连接符的使用

//4.字符串拼接 + 的底层 
        String str1 = "aa"; //常量池中存一个aa 返回引用
        String str2 = "bb"; //常量池中存一个bb 返回引用

        String str7 = "aabb"; //常量池中存一个 aabb 返回引用
        //这个在前端编译的时候会优化 直接在常量池中就是 aabb 但是常量池中没有aa 和 bb
        String str3 = "aa" + "bb"; 
        //只要带上了变量进行+的操作 底层都是使用了StringBuilder() 
       /*StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(1);
       stringBuilder.toString()*/
注意这里的toString底层是使用了new String(char[] c)的这个构造器所以说字符串拼接完之后不会放进常量池中 因为最终使用了new String(char[] c) 所以说这这种类型的String在堆中 不指向常量池 即下面三个都是false
        String str4 = "aa" + str2; 
        String str5 = str1 + "bb";
        String str6 = str1 + str2;

        System.out.println(str7 == str3); //true
        System.out.println(str7 == str4); //false
        System.out.println(str7 == str5); //false
        System.out.println(str7 == str6); //false

4.intern的使用

String

参考代码

JVM**/StringTable/**zongjie

String去重 视频P133

7.内存的分配与垃圾回收概述

面试题:视频135

垃圾是什么?

垃圾是指在运行程序中没有任何指针指向的对象 这个对象就是需要被回收的垃圾

内存泄露: 本身不用了 但是还没办法进行垃圾的回收

应该关心哪些区域的回收

运行时数据区是否有GC是否有OOM是否有栈溢出
方法区
程序计数器

​ 所以 最应该关心堆区的垃圾回收 Java堆是垃圾收集器的工作重点

从次数上讲

  • ​ 频繁收集新生代
  • ​ 较少收集老年区
  • ​ 基本不动方法区

8.垃圾回收相关算法

8.1垃圾标记阶段

8.1.1引用计数算法

什么是引用计数算法

​ 引用计数算法**(Reference Counting)**比较简单 对每个对象保存一个整型的引用计数器属性 用于记录对象被引用的情况

​ 对于一个对象A 只要有任何一个对象引用了A 则A的引用计数器及加1 当引用失效时 引用计数器就减1 只要对象的引用计数器的值为0 即表示对象A不可能再被使用 可进行回回收

优点实现简单 垃圾对象便于辨别 判定效率高 回收没有延迟性

缺点:

  • 需要单独的字段存储计数器 这样的做法增加了存储空间的开销
  • 每次赋值都需要更新计数器 伴随着加法和减法操作 这增加了时间开销
  • 引用计数器有一个严重的问题 即无法处理循环引用的问题 这是一条致命缺陷 导致在Java的垃圾回收器中没有使用这类算法

循环引用

​ 一个引用p指向了一个对象next1 next1对象里有一个引用指向了next2 next2对象里有next3的引用指向了next3

next3中的有一个引用又指向了next1 这时next1的引用计数器为2 next2和next3的引用计数器都为1

​ 这时如果栈中的引用p指向了null 这时next1的计数器减1变为1 但是这三个对象已经没有引用指向了 但是引用计数器没有变为0 这时就会出现无法被回收 导致内存泄露

在这里插入图片描述

总结

引用计数算法 是很多语言的资源回收选择 例如Python 它更是同时支持引用计数和垃圾收集机制

集体使用哪种最优要看场景 业界有大规模实践中仅保留引用计数机制 以提高吞吐量的尝试

Java并没有选择引用计数 是因为他存在一个基本的难题 就是就是难以处理循环引用的问题

Python如何解决循环引用?

  • 手动解除:很好理解就是在合适的时机 解除引用关系
  • 使用弱引用weakref weakref是python提供的标准库 旨在解决循环引用

8.1.2可达性分析算法

PPT_77 视频P_141

8.2对象的finalization机制

  • Java提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
  • 当垃圾回收器发现没有引用指向一个对象 即: 垃圾回收此对象之前 总会先调用这个方法的finalize()方法
  • finalize() 方法允许在子类中被重写 用于在对象被回收时进行资源释放

永远不要主动调用某个对象的finalize()方法 应该交给垃圾回收机制调用 理由包括下面三点

  • 在finallize()时可能导致对象复活
  • finalize()方法的执行时间是没有保障的 它完全由GC线程决定 极端情况下 若不发生GC 则finalize() 方法将没 有执行机会
  • 一个糟糕的finalize() 会严重影响GC的性能

由于finalize()方法的存在 虚拟机中的对象一般处于三种可能的状态

如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:

  • 可触及的: 从根节点开始 可以到达这个对象
  • 可复活的: 对象的所有引用都被释放 但是对象有可能在finalize()中复活 (代码参考D:\IDEAworkspace\fuxi\src\JVM\垃圾回收算法\FinalizationTest.java)
  • 不可触及的:对象的finalize() 被调用 并且没有复活 就会进入不可触及状态 不可触及的对象不可能被复活 因为finalize() 只会被调用一次

以上三种状态 是由于finalize()方法的存在进行的区分 只有在对象不可触及时 才可以被回收

在这里插入图片描述

8.3垃圾清除阶段

PPT_2.95

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。

8.3.1标记-清除(Mark-Sweep)算法

当堆中的有效空间(available memory)被耗尽的时候 就会停止整个线程(Stop The World) 然后进行两项工作 第一项是标记 第二项则是清除

  • 标记: 从引用根节点开始遍历 标记所有被引用的对象 一般是在对象的Header中记录为可达对象
  • 清除: 对堆内存从头到尾进行线性的遍历 发现如果某个对象在其Header中没有标记为可达对象 则将其回收

缺点:

  • 效率不高
  • 在进行GC时 需要停止整个应用程序
  • 这种方式清理出来的空闲内存是不连续的 产生内存碎片(就是这一点 那一点) 需要维护一个空闲列表

注意:何为清除?

这里所谓的清除并不是真的置空 而是把需要清除的对象地址保存在空闲的地址列表里 下次有新对象需要加载时判断垃圾的位置空间是否足够 如果够 就直接存放 等于是覆盖

在这里插入图片描述

8.3.2复制(copying)算法

核心思想:

将内存空间一分为二A B 每次只使用其中的一块A 在垃圾回收时将正在使用A的内存中的存活对象复制到另一块空间中到B去 之后清除A的空间 交换两个内存的角色 最后完成垃圾回收 适用于有大量的垃圾的内存空间使用 挪动的对象少 效率高

优点:

  • 没有标记和清除过程 实现简单 运行高效 就是在遍历的时候发现存活的直接移动
  • 复制过去之后保证空间的连续性 不会出现碎片问题

应用场景:

在新生代 对常规应用的垃圾回收 一次通常可以回收70%-90%的内存空间 回收性价比高 所以现在的商业虚拟机都是

用这种收集算法回收新生代

在这里插入图片描述

8.3.3标记-压缩(整理)(Mark-Compact)算法

复制算法的高效性是建立在存活对象少 垃圾对象多的前提下 这种情况在新生代经常发生 而在老年代 更常见的情况大部分对象都是存活对象 如果依然使用复制算法 由于存活对象多 复制的成本也搞 因此 基于老年代垃圾回收的特性 需要使用其他的算法

标记清除算法的确可以应用到老年代 但是该算法不仅执行效率低 而且执行完还会产生内存碎片 所以JVM的设计者在此基础上进行了改进 标记-压缩算法由此诞生

标记压缩算法的最终效果就等同于 标记清除算法执行完成后 再进行一次内存碎片整理

二者的本质差异在于标记-清除算法是一种非移动式的回收算法 标记-压缩是移动式的 是否移动回收后的存活对象是一项优缺点并存的风险决策

可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

缺点:

  • 从效率上来说 标记整理算法要低于复制算法
  • 移动对象的同时 如果对象被其他对象引用 则还需要调整引用的位置
  • 移动过程中 STW

在这里插入图片描述

8.3.4三种算法对比

在这里插入图片描述

8.3.5分代收集算法

PPT_2.114

  • 前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
  • 分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
  • 在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如: string对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

年轻代:复制算法

老年代:标记清除与标记整理混合实现

8.3.6增量收集算法 分区算法

PPT_p115

增量算法

基本思想:

如果一次性将所有的垃圾进行处理 需要造成系统长时间的停顿 那么就可以让垃圾收集线程和应用程序线程交替执行

每次 垃圾收集线程只收集一小片区域的内存空间 接着切换到应用程序线程 依次反复 直到垃圾收集完成

总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法 增量收集算法通过对线程间冲突的妥善处理 允许垃圾收集线程以分阶段的方式完成标记 清理 或复制工作

缺点:

使用这种方式 由于在垃圾回收过程中 间断性的还执行了应用程序代码 所以能减少系统的停顿时间 但是 因为线程切换和上下文转换的消耗 会使得垃圾回收的总成本上升 造成系统吞吐量的下降

分区算法

一般来说,在相同条件下,堆空间越大,一次Gc时所需要的时间就越长,有关cc产生的停顿也越长。为了更好地控制cc产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次Gc所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制
-次回收多少个小区间

9.垃圾回收相关概念

9.1System.gc的使用

在默认情况下 通过System.gc() 或者Runtime.getTuntime().gc()的调用 会显示触发Full GC 同时对老年代和新生代进行回收

然而 System.gc() 调用附带一个免责声明 就是说我调用了这个方法 但是你回不回收是你的事

只是提醒JVM的垃圾回收器执行GC 但是不确定是否马上执行

System.runFinalization()强制调用对象的finalize()的方法

手动GC理解对象不可达视频P157

PPT_2.127内存问题代码

9.2内存溢出

JavaDoc对OutOfMemoryError的解释是 没有空闲内存 并且垃圾收集器也无法提供更多的内存

在抛出OOM之前 通常会进行GC 尽其所能去清理出空间

当然 也不是任何情况下垃圾收集器都会被触发

比如 我们去分配一个超大对象 类似一个超大数组超过堆的最大值 JVM判断出 GC并不能解决 这个问题 所有直接抛出OOM

9.3内存泄露

严格来说 :只有对象不会再被程序用到 但是GC又无法回收的情况才叫内存泄露

但实际情况很多时候我们的程序会导致对象的生命周期变长 导致OOM 也可以叫做宽泛意义上的内存泄露

举例:

  • 单例模式

    单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。

  • 一些资源连接未关闭

​ 数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收 的。

9.4安全点与安全区域

视频P163 PPT_145

9.5Java各种引用

强引用(StringReference): 最传统的 “引用” 的定义 是指在代码中普遍存在的引用赋值 就相当于Object object = new Object(); 这种引用无论什么情况下只要强引用关系还存在 垃圾收集器就永远不会回收掉被引用的对象

软引用(SoftReference): 在系统将要发生内存溢出之前 将会把这些对象列入回收范围之中进行第二次回收 如果这次回收后还没有足够的内存 才会排除内存溢出异常

弱引用(WeakReference): 被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾收集器工作时 无论内存空间是否足够 都会回收掉被弱引用关联的对象

虚引用(PhantomReference): 一个对象是否有虚引用的存在 完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

10.垃圾回收器

吞吐量: 用户线程执行时间 - GC时间 / 总时间

暂停时间:每次GC的时间长度

Java常见的垃圾收集器?

串行回收器:Serial ,SerialOld

并行回收器:ParNew,Parallel Scavenge ,Parallel Old

并发回收器:CMS G1

新生代回收器:Serial ,ParNew,Parallel Scavenge(复制算法)

老年代回收器:SerialOld,Parallel Old(标记-压缩算法),CMS

整堆回收器:G1

JDK8默认使用Parallel GC

JDK9默认使用 G1

10.1Serial SerialOld串行回收器

PPT_195

Serial使用复制算法回收新生代

Serial Old 使用标记整理算法回收老年代

10.2ParNew并行回收器

并行使用复制算法回收新生代

10.3ParallelScavenge ParallelOld并行回收器(吞吐量优先)

JDK8默认使用

Paraller Scavenge使用复制算法收集新生代

  • 和ParNew收集器不同 Parallel Scavenge收集器的目标是达到一个可控制的吞吐量 它也被称为吞吐量优先的垃圾收集器
  • 自适应调节策略也是它和ParNew的一个重要区别

Paraller Old使用标记-压缩算法收集老年代

参数设置

  • -XX: +UserParallelGC

  • -XX: +UserParallelOldGc

  • -XX: +ParallerGCThreads 设置年轻代并行收集器的线程数 一般的 最好与CPU数量相等 以避免过多的线程数影响垃圾收集性能

    默认情况下 当CPU数量小于8个 ParallelGCThreads的值等于CPU数量

    当CPU数量大于8个 ParallelGCThreads 的值等于3+[5*CPU_Count/8]

  • -XX:MaxGCPauseMillis 设置垃圾收集器的最大停顿时间(即STW时间)

  • -XX:GCTimeRatio 设置垃圾收集时间总时间的比例

  • -XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器的自适应调节策略

10.4CMS(Concurrent-Mark-Sweep)并发回收器

视频_P185 PPT_P212

CMS是并发的使用标记-清除算法老年代回收器

中断时间短 低延迟

JDK14移除了CMS

产生内存碎片

10.G1并行回收器区域化分代式

1.9默认使用

9.4安全点与安全区域

视频P163 PPT_145

9.5Java各种引用

强引用(StringReference): 最传统的 “引用” 的定义 是指在代码中普遍存在的引用赋值 就相当于Object object = new Object(); 这种引用无论什么情况下只要强引用关系还存在 垃圾收集器就永远不会回收掉被引用的对象

软引用(SoftReference): 在系统将要发生内存溢出之前 将会把这些对象列入回收范围之中进行第二次回收 如果这次回收后还没有足够的内存 才会排除内存溢出异常

弱引用(WeakReference): 被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾收集器工作时 无论内存空间是否足够 都会回收掉被弱引用关联的对象

虚引用(PhantomReference): 一个对象是否有虚引用的存在 完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

10.垃圾回收器

吞吐量: 用户线程执行时间 - GC时间 / 总时间

暂停时间:每次GC的时间长度

Java常见的垃圾收集器?

串行回收器:Serial ,SerialOld

并行回收器:ParNew,Parallel Scavenge ,Parallel Old

并发回收器:CMS G1

新生代回收器:Serial ,ParNew,Parallel Scavenge(复制算法)

老年代回收器:SerialOld,Parallel Old(标记-压缩算法),CMS

整堆回收器:G1

JDK8默认使用Parallel GC

JDK9默认使用 G1

10.1Serial SerialOld串行回收器

PPT_195

Serial使用复制算法回收新生代

Serial Old 使用标记整理算法回收老年代

10.2ParNew并行回收器

并行使用复制算法回收新生代

10.3ParallelScavenge ParallelOld并行回收器(吞吐量优先)

JDK8默认使用

Paraller Scavenge使用复制算法收集新生代

  • 和ParNew收集器不同 Parallel Scavenge收集器的目标是达到一个可控制的吞吐量 它也被称为吞吐量优先的垃圾收集器
  • 自适应调节策略也是它和ParNew的一个重要区别

Paraller Old使用标记-压缩算法收集老年代

参数设置

  • -XX: +UserParallelGC

  • -XX: +UserParallelOldGc

  • -XX: +ParallerGCThreads 设置年轻代并行收集器的线程数 一般的 最好与CPU数量相等 以避免过多的线程数影响垃圾收集性能

    默认情况下 当CPU数量小于8个 ParallelGCThreads的值等于CPU数量

    当CPU数量大于8个 ParallelGCThreads 的值等于3+[5*CPU_Count/8]

  • -XX:MaxGCPauseMillis 设置垃圾收集器的最大停顿时间(即STW时间)

  • -XX:GCTimeRatio 设置垃圾收集时间总时间的比例

  • -XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器的自适应调节策略

10.4CMS(Concurrent-Mark-Sweep)并发回收器

视频_P185 PPT_P212

CMS是并发的使用标记-清除算法老年代回收器

中断时间短 低延迟

JDK14移除了CMS

产生内存碎片

10.G1并行回收器区域化分代式

1.9默认使用

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shstart7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值