Skip to main content

JVM 学习

PPLongAbout 71 min

JVM 学习日志

[toc]


简介

JVM : Java HotSoit Virtual Machine

Write once, run anywhere

Java 程序 ----->编译为字节码文件(本身具有跨平台性) ----> 不同操作系统上JVM

JRE 和 JDK的关系

JDK:它是Java开发运行环境,在程序员的电脑上当然要安装JDK;
JRE:Java Runtime Environment它是Java运行环境,如果你不需要开发只需要运行Java程序,那么你可以安装JRE。例如程序员开发出的程序最终卖给了用户,用户不用开发,只需要运行程序,所以用户在电脑上安装JRE即可。
JDK包含了JRE,JRE中包含虚拟机JVM

JVM跨语言的平台

JVM起始 从字节码开始

字节码文件可以由不同的编程语言提供 在java平台解释运行

由不同的编程语言提供不同的编译器

image-20210224091832738
image-20210224091832738

JVM的语言无关性

Java虚拟机不关心在内部运行的程序是用何种编程语言写的

只关心字节码文件

只要其他编程语言的编译结果满足并包含Java虚拟机的内部指令集,符号表以及请他信息,他就可以被装载和运行

(JVM) 字节码

通过编译器编译能在JVM上运行的二进制文件格式

不同的编译器可以编译出相同的字节码文件 字节码文件可以在不同的JVM上运行

多语言混合编程

JDK 1.5 ---> Java SE 5.0

虚拟机

软件:执行一系列虚拟计算机指令

分类

  • 系统虚拟机: 提供可运行完整操作系统的软件平台,是对物理计算机的仿真
  • 程序虚拟机:专门为执行单个计算机程序设计的 Java虚拟机中执行的指令 --- 字节码指令

特点:

  1. 一次编译到处运行
  2. 自动内存管理
  3. 自动垃圾回收机制

虚拟机在计算机中的位置

image-20210224094303154
image-20210224094303154
image-20210224094516957
image-20210224094516957

其他

OpenJDK 半年更新一次 维护半年 之后不再维护 免费开源

OracleJDK 维护期三年 付费 内容稍微少一些

两者在代码的实质上基本一致

正式进入

Java代码的执行流程

image-20210224095237677
image-20210224095237677

JVM的架构

Java编译器输入的指令流是 一种基于栈的指令集架构 不是基于寄存器的指令集架构

image-20210224095537650
image-20210224095537650

JVM的生命周期

  1. 启动: bootstrapclass loader引导类加载器创建的初始类 initial class 完成的 (加载父类??)
  2. 执行:一个虚拟机对应着执行一个Java程序
  3. 退出:异常 、 正常执行完毕、操作系统错误、exit方法

类加载

ClassLoader

只负责class 文件的加载,至于是否能运行,由Execution Engine决定

image-20210224152835665
image-20210224152835665
image-20210224153411382
image-20210224153411382
image-20210224163801377
image-20210224163801377

加载Loading

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将该字节流代表的静态存储结构转化为方法的运行时数据
  3. 内存中生成一个代表此类的 java.lang.Class对象,作为方法区这个类的各个数据的访问入口

链接Linking阶段

验证 Verify

确保Class文件的字节流一中包含信息符号当前的虚拟机要求,保证加载类的正确性,不危害虚拟机自身安全。

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

准备 Prepare

为类变量分配内存并且设置该内变量的默认初始值,哪怕是a = 1 最开始也是 a = 0

不包含用final修饰的static final在编译阶段就会分配了

解析 Resolve

将常量池内的符号引用转换为直接引用的过程

image-20210228145418046
image-20210228145418046

类的初始化

执行类构造器方法 clinit 的过程

javac 编译器自动收集类中的所有类变量赋值动作静态代码块中语句合并而来

有静态语句 或者 静态变量 就会有 clinit 方法 不包含主方法

clinit 方法一定是先执行父类的 clinit 在进行子类的clinit。

虚拟机必须保证一个类的clinit 方法在多线程下被同步加锁

类的构造器方法为 init,且可能有多个,static代码块以及静态变量的初始赋值合并在 clinit 中,且只能用有一个。static 代码块 和 static变量的赋值 有顺序的关系,但是可以在static语句块中赋值再定义 ------有一个注意点 非法的前向引用(可以赋值但不能调用

举个例子:

static {
    ABC = 4;
}
public static int ABC = 3;
// ABC = 3

public static int ABC = 3;
static {
    ABC = 4;
}
// ABC = 4

static {
    System.out.println(ABC);
}
public static int ABC = 3;
// Error

变量初始化过程

默认初始化--显示初始化- -构造器初始化--对象.属性的初始化

加载器的分类

BootStrapClassLoader

  • 用c/c++实现 嵌套在JVM内部
  • 不继承ClassLoader

系统的核心类库都是用引导类加载器加载的

不能直接通过getClassLoader获取

AppClassLoader

加载环境变量classpath和系统属性 java.class.path 是程序中的默认类加载器

ExtensionLoader

从java.ext.dirs系统属性指定的目录中加载类库

自定义类加载器

目的:

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄露

双亲委派机制

问题引入:

自己创建 的 java.lang.String 和原生的冲突 会先加载原来的

image-20210224173500988
image-20210224173500988
  1. 一个类加载器收到了类加载请求,他并不会自己先加载,而是把这个请求委托给父类的加载器执行。
  2. 一次递归请求最终到达顶层的启动类加载器
  3. 如果父类的加载器无法加载(引导类加载器和拓展类加载器只加载指定目录的class),才会委派给子类的加载器加载

优点

  1. 保护程序安全,避免核心API被更改
  2. 避免类的重复加载。当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类

沙箱安全机制

沙箱安全机制是由基于双亲委派机制上 采取的一种JVM的自我保护机制,假设你要写一个java.lang.String 的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.

虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)

其他

JVM中判断两个class对象是否是同一个类存在的两个条件

  1. 类的完整名称必须一致
  2. 加载这个类的加载器必须相同

类的主动使用和被动使用

image-20210224184726345
image-20210224184726345

运行时数据区

RunTime Data Area 桥梁: CPU --- 内存 ---- 硬盘

不同的JVM对内存的划分方式和管理机制存在差异

image-20210225201740198
image-20210225201740198
image-20210225201756298
image-20210225201756298

红色 一个进程一份

灰色 一个线程一份

一个JVM实例对应一个RunTime实例

HotSpot JVM每个线程对应操作的本地线程直接映射

虚拟机栈

概念

内部保存一个个的栈帧,对应着一次次的java方法调用(线程私有)

  • 生命周期和线程一致

  • 栈是运行时单位,堆是存储单位

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

优点: 访问速度快 不存在垃圾回收问题 GC (只有进栈出栈操作) 但可能存在OOM

设置栈大小 VMOptions: -Xss256k

异常

  • JVM允许java栈大小是动态或者固定的
  • 栈溢出异常: 线程的请求分配的栈容量超过JAVA虚拟机允许的最大容量,将抛出Stack Overflow异常
  • OOM: 动态扩展时没有足够的内存去申请或者创建,则跑出OutofMemoryError异常

栈帧

栈中的数据都是以栈帧方式存在的,每个方法都对应一个栈帧

当前栈帧:当前执行方法。

不同线程的栈帧是不允许相互引用的,不能在一个线程的方法里去调用另一个线程中的方法

包含:

局部变量表

(基本决定栈帧的大小)

image-20210228084320963
image-20210228084320963
  • 数字数组,存储方法参数和定义在方法体内的局部变量。包含基本数据类型,对象引用byte short boolean char 被转换为int),返回值类型

  • 垃圾回收的重点内容

  • 所需大小是编译器确定下来的。未被赋值的声明是不会列入局部变量表的slot,但会列入locals(maximum local variables)中

  • image-20210228090243915
    image-20210228090243915
  • Slot 槽:局部变量表的基本单位

    • 32位以内的类型只占用一个slot 64位占用两个slot,在locals中看得出来
      • 在外部方法中引用该类中对象或者方法时(不引用也会有) slot index0 位置会保存类的对象引用this
      • slot中的变量存在重复利用,内部域变量过期后的slot位置将由外部遍历替代
      • 局部变量初始化后再使用时必须赋值,否则编译不通过
    • slot中先保存this(即该类),然后保存该方法参数,然后再是局部变量
操作数栈

数组、链表实现 FILO

作用:根据字节码指令,在栈中写入数据,入栈和出栈,保存计算过程的中间结果

  • a + b = c实例
代码解析

bipush: 这里的 bi 代表byte 的整型 -128~127 时,与代码中定义的int byte类型无关,他只与结果的具体数值有关,转换成最接近的类型

sipush:整型 -32768~32767 时

istore_x: 弹出栈的值并保存到slot索引为x的地方

注意这里istore_0和istore_1的区别,如果是主方法或者普通方法时,要么保存的是对象的引用this 要么就是args参数在index 为0位置。如果是类的静态方法中时,则就不存在其他的数据,一个数据都保存在index0处

iconst:当int取值**-1~5时,JVM采用iconst**指令将常量压入栈中。

  		0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: return
  • 每个操作数栈都拥有一个明确的栈深度存储数值,编译器就定义好

Code --- max_stack中

  • 栈占用单位同slot
  • 操作数栈中存在复用
  • 如果被调用的方法中带有返回值的话,其返回值将会被压入当前栈帧操作数栈中,并更新pc寄存器中的指令
栈顶缓存技术

出发点:栈结构,频繁执行内存读写(出栈入栈)

栈顶元素全部缓存在cpu寄存器中,提升执行引擎的效率

动态链接

------运行时常量池的方法引用

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用

目的:支持当前方法的代码能够实现动态链接,将保存变量和方法引用的符号引用转换为调用方法的直接引用

方法的调用

静态链接和动态链接

image-20210228141739834
image-20210228141739834

静态链接:目标方法在编译期间可知且运行期保持不变,这种情况下降调用方法的符号应用转换为直接引用的过程称为静态链接

动态链接:编译时无法确定

方法的绑定机制:早期绑定和晚期绑定

image-20210228142033732
image-20210228142033732

晚期绑定:

多态、继承 无法确定 ----invokeVirtual

invokeSpecial

虚方法:除非虚方法外的所有方法

非虚方法:

  • 编译期间确定版本,运行时不可变

  • 静态方法、私有方法final方法(static修饰的除外)、示例构造器、父类方法

image-20210228143115307
image-20210228143115307

在子类中未显示的调用父类方法是虚方法(可能子类进行了重写)

一个问题:为什么调用non-private的final方法是invokevirtual类型的?

考虑以下一个场景:

class Base {
  void foo() { System.out.println("Base"); }
}

class Derived extends Base {
  @Override
  final void foo() { System.out.println("Derived"); }
}

public class Test {
  public static void main(String[] args) {
    Derived d = new Derived();
    d.foo(); 
    Base b = d; //  Base类型引用指向Derived类型实例
    b.foo();    //  通过invokevirtual调用到final Derived.foo()
  }
}

// 作者:RednaxelaFX
// 链接:https://www.zhihu.com/question/45131640/answer/98820081

也就是子类重写了父类的虚方法并声明为final不希望被自己的子类继承,这个时候则可以通过多态以Base的类型调用到Derived的final方法,所以也是不确定的

动态类型和静态类型语言的区别

对类型的检查是在编译期间还是在运行期间

  • 前者判断变量自身的类型信息

  • 后者判断变量值的类型信息,变量是没有类型信息的(JavaScript)

方法分派

静态分派

首先需要理解,在Java中 Animal animal = new Dog(); 其中Animal表示的是静态类型, 而随后的Dog()则表示实际类型

public class DispatchTest {
    //方法重载,是一种静态行为,编译期就可以确定
    public void test(Animal animal){
        System.out.println("Animal");
    }
    public void test(Dog dog){
        System.out.println("Dog");
    }
    public void test(Cat cat){
        System.out.println("Cat");
    }

    public static void main(String[] args) {
        DispatchTest staticDispatch = new DispatchTest();
        Animal animalDog = new Dog();//变量声明为Animal,引用指向Dog实例
        Animal animalCat = new Cat();//变量声明为Animal,引用指向Cat实例
        staticDispatch.test(animalDog);//调用重载的方法
        staticDispatch.test(animalCat);//调用重载的方法
    }
}
class Animal{}
class Dog extends Animal{}
class Cat extends Animal{}

// result:
// Animal Animal

考虑以上情况,结果却不为Dog和Cat,这是因为对方法的重载,方法的参数以变量的静态类型为准,而不管引用指向的实际类型。变量的静态类型不会发生变化,而实际类型可能在运行时发生改变。静态分派发生在编译阶段,所以确定静态分派的动作实际上不是由JVM完成的,而应该是Javac编译器

动态分派

public class DynamicTest {
    public static void main(String[] args) {
        Fruit apple = new Apple();//声明为Fruit,引用指向子类
        Fruit orange = new Orange();
        apple.test();
        orange.test();
        apple = new Orange();//修改引用
        apple.test();
    }
}
class Fruit{
    public void test(){
        System.out.println("Fruit");
    }
}
class Apple extends Fruit{
    @Override//重写
    public void test(){
        System.out.println("Apple");
    }
}
class Orange extends Fruit{
    @Override//重写
    public void test() {
        System.out.println("Orange");
    }
}
// Result:
// Apple Orange Orange


// 调用的都是Fruit的test方法
// 17: invokevirtual #6                  // Method stream/Fruit.test:()V
// 20: aload_2
// 21: invokevirtual #6                  // Method stream/Fruit.test:()V

动态分派和方法的重写有关,涉及到一个概念:方法接受者

调用invokevirtual字节码指令的多态查找流程:

  • 在操作数栈顶找到第一个元素所指向的对象的实际类型C(这是造成多态查找的根本原因)
  • 如果在类型C中找到与敞亮中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回该方法的直接引用。
  • 否则,按照继承关系从下往上对C的各个父类进行搜索验证
  • 没有找到合适的方法,抛出AbstractMethodError;

字段是否有多样性

参考以下代码, 深刻理解字段不具有多样性

class Father {
    public int money = 1;
    public Father() {
        money = 2;
        showMeTheMoney();
    }
    public void showMeTheMoney() {
        System.out.println("I am father I got " + money + " money");
    }
}

class Son extends Father {
    public int money = 3;
    public Son() {
        money = 4;
        showMeTheMoney();
    }
    public void showMeTheMoney() {
        System.out.println("I am son I got " + money + " money");
    }
}
public static void main(String[] args) throws InterruptedException {
    Father son = new Son();
    System.out.println(son.money);
}
/** I am son I got 0 money
 *  I am son I got 4 money
 *  2
*/
  • 调用new Son()时,肯定先要加载父类,执行父类的init方法,此时父类的money字段为2 ,随后invokevirtual调用虚方法showMeTheMoney()
  • 但这里是方法的重写,所以有动态分派,实际类型是Son,所以调用Son的showMeTheMoney()
  • 但这个时候Son对象还没有被初始化啊,所以Son的money字段为0
  • 然后进行Son对象的初始化,此时money为4且调用son类的showMeTheMoney方法
  • 最终由于对象的静态类型是Father,所以son.money其实是Father类中的money对象,结果为2

当子类声明了与父类同名的字段时,虽然在自类的内存中两个字段都会存在,但自类的字段会则遮蔽父类的同名字段,但由于这里通过静态类型访问变量,因此能访问到父类的money变量。

当子类声明了与父类同名的字段时,在自类的内存中两个字段都会存在。当指定静态类型Father获取时,获取的为属于Father的静态变量,当指定静态类型为Son时获取的是Son的静态变量。已在VisualVM中得到验证

除去重载和重写之外 针对以上代码,考虑这种情况:

// Son
public void sayOfSon() {
  ...
}
// Main
Father son = new Son();
son.sayOfSon();

这时候是否会报错呢?答案是会的,这里也是多态的一种,因为静态类型是Father, 在编译时只会考虑静态类型所拥有的方法。

Java是一门静态多分派而动态单分派的语言

静态分派时考虑静态类型和方法参数等,动态分派只考虑其方法的接收者的实际类型即可。

JVM如何做到分派呢:建立虚方法表

虚方法表

目的: 避免自下而上频繁的使用动态分配

创建阶段: linking

每个类中都有一个虚方法表,虚方法表中存放着每个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表的地址入口和父类相同方法的入口是一样的,如果重写了该方法,则子类虚方法表中的地址会被替换。在父类、子类的虚方法表中都应当有一样的索引序号,当类型变换时,只需要变更查找的虚方法表,就可以找出正确的方法的执行地址

方法返回地址

方法正常退出的定义(异常退出时由异常表决定)

存放调用该方法的pc寄存器的值 类似ret???

异常处理表

class文件中 from to target

异常完成出口不会给上层调用者任何返回的信息

附加信息

本地方法栈

  • 线程私有
  • 内存 固定或者动态扩展
  • 本地方法用C语言实现
  • 实现:虚拟机栈中登记本地方法

当线程调用本地方法时,就进入了一个全新的不受虚拟机限制的 世界,和虚拟机有相同的权限

本地方法通过本地方法接口访问虚拟机内部运行时数据区

本地方法接口和库

Native Method : Java 调用非Java代码

程序计数器

用来存储指向下一条指令的的字节码地址( ProgramCounter Register)

  • 很小的内存空间,运行速度最快

  • 线程私有,生命周期和线程的生命周期保持一致

  • 存储当前线程正在执行方法的JVM指令地址 如果执行native方法则是未指定值

  • 唯一一个在JVM规范中没有规定任何OOM情况的区域

  • 线程分支循环跳转异常处理、恢复等基础功能都需要依赖他

PC寄存器的好处

CPU会不停切换线程,切换回来以后不知道从那开始执行

指明当前执行代码的字节码指令

CPU时间片

  • CPU分配给各个程序的时间

  • CPU主频高,不断的切换,感觉像是在并行(不是并发)的执行-------这就叫并发

  • 一个内核一时刻只能执行一条指令

一个JVM只存在一个堆内存 一个进程对应一个堆,JVM启动时 堆被创建 其空间大小就确定了 ,但堆内存的大小是可以调节的

堆可以处于物理上不连续的空间,但逻辑上是连续的

所有的线程共享JAVA堆,还可以划分线程私有的华冲去 TLAB THREAD LOCAL ALLOCATION BUFFER

所有的对象实例及数组都应在运行时分配到堆上

  • 方法结束后,堆的对象不会马上被移除,仅仅在垃圾收集时才会被移除(判断是否有引用)
  • 堆是 GC 执行垃圾回收的重点区域

堆空间细分(逻辑上)

  • Java 7 新生区 养老区 永久区

  • Java 8 包括以后 新生区 养老区 元空间

设置堆空间大小

-Xms 堆区(新生代+老年代)的起始内存

-Xmx 堆区最大内存

-X JVM运行参数 -ms memory start

JVM 参数地址

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.htmlopen in new window

初始大小 电脑内存 / 64 默认大小 电脑内存 / 4

Runtime.getRuntime().totalMemory()

问题: 设置Xms为10m  为什么得到的内存<10

  • Eden 区 只能和Survivor 1 或 2区 进行存储 而不是两个Survivor区都可以存储

堆空间的OOM

image-20210302163155472
image-20210302163155472

年轻代与老年代

YoungGen

占比1 / 3

  • Survivor 0 : from区
  • Survivor 1 :to区

OldGen

占比2 / 3

image-20210302163141130
image-20210302163141130

默认比例

image-20210302163437852
image-20210302163437852
image-20210302163456336
image-20210302163456336

问题: Eden和Survivor比例是 8:1  但实际是 6:1?

  • 自适应机制 -XX:-UseAdaptiveSizePolicy 关闭自适应的内存分配
  • 可通过 -XX:SurvivorRatio 调整

几乎所有的Java对象都是Eden区 中new出来的

对象分配过程

YoungGC Minor GC

  1. new的对象先到Eden区
  2. Eden区被填满后 JVm GC器对 Eden区进行垃圾回收 MinorGC 销毁没有引用的对象
  3. 将Eden的剩余对象移动到Survivor 0 区 并加上age 1
  4. 再次触发GC时 上次放到Seurvivor 0 区的就会 进入 1 区 age ++
  5. 阈值 age >15 时 到 Old Gen 区 也有可能破格晋升

注意点

  • Eden区满 会将 Eden 和Survivor 区一起进行回收

  • Survivor区满不会进行回收!Survivor放不下,直接晋升老年代

  • 0区 和1区复制之后有交换,谁空谁是to区

  • 动态对象年龄判断:如果Survivor 中相同年龄的所有对象大小的总合大于 该区空间的一半,则年龄大于或等于该年龄的对象直接进入老年代(优化)

  • 大对象直接放入老年代 (优化)

Minor GC、Major GC 、Full GC

STW : stop the world

  • 部分收集 Partial GC:

    • Minor GC :

      • 执行频率高,速度快触发 STW 暂停用户线程
    • Major GC:

      • 执行速度比minor GC慢10倍以上 STW时间更长
      • 老年代空间不足,先尝试minor GC如果还不足,就触发Major GC
    • Mixed GC: 收集整个新生代和部分老年代的垃圾

  • 整堆收集 Full GC: 收集整个java堆和方法区的垃圾

    • 调优中尽量避免
    • 触发条件:
      1. 老年代空间不足
      2. 方法区空间不足
      3. System.gc()但不一定执行
      4. MinorGC进入老年代的平均大小大于老年代的可用内存
      5. Survivor区转存到老年代时可用内存小于该对象大小
image-20210303160142447
image-20210303160142447

为什么要分代

  • 优化GC性能 缩小对堆的扫描范围

TLAB

问题:堆区是线程共享区域 并发环境下从堆区划分内存空间是线程不安全的

定义: 每个线程中独有一份TLAB 即私有缓存区域

目的:

  • 多线程分配内存,能避免一系列的非线程安全问题
  • 提升内存分配的吞吐量(快速分配策略)

JVM将TLAB作为内存分配的首选

TLAB空间小 仅仅占整个Eden的1% , 也可以设置

一旦对象在TLAB空间分配内存失败,JVM尝试使用加锁机制确保数据操作的原子性,进而在Eden空间分配内存

逃逸分析

将堆上的对象分配到栈

  • 对象在方法内部中定义后 只 在内部使用, 则没有逃逸 ------>使用栈上分配
  • 对象在方法内部定义后 被外部方法所引用 。则发生了逃逸

如何判断是否发生逃逸

image-20210303161641422
image-20210303161641422

优化 前提 未发生逃逸

栈上分配

原理 :将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配

实例n个对象时,开启栈上分配, 速度大幅提高,实例数降低。

同步省略
image-20210303162647795
image-20210303162647795

去除了同步操作

代码优化

分离对象或者标量替换 肢解

image-20210303163208358
image-20210303163208358

方法区

永久代 ----->元空间(都是对JVM方法区的一种实现策略)

固定大小或者动态变化 自动收缩 逻辑上连续 物理上不连续

逻辑上属于堆的一部分

方法区用于存储已被虚拟机加载的类型信息、常量、静态常量、即时编译器(JIT)编译后的代码缓存....

方法区看做是一块独立于Java堆的内存空间

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,方法区会溢出 抛出OOM

元空间和永久代最大的区别

元空间不在虚拟机的内存中,而是在本地内存 ,内部结构也发生改变。JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存

方法区 堆 栈的交互关系

image-20210303164007274
image-20210303164007274
image-20210303164100411
image-20210303164100411

设置元空间大小

-XX: MataSpaceSize= 设置初始分配空间 默认20.75M

-XX:MaxMataSpaceSize 设定最大可分配控件 32位默认64M 64位默认82M 设置为-1则没有限制

JDK1.7以前 -PermSize

常用指令 jps jinfo -flag xxxxx aaaa

触及21M FULL GC 释放没用的类,水位线重置,如果释放空间过多,则降低该值

内存泄露和内存溢出

内存泄露 病人康复 但是占着床位

内存溢出 病人太多,床位不够

image-20210303171142462
image-20210303171142462

内部结构

类型信息

完整有效名称 直接父类的完整有效名称 修饰符 直接接口的有序列表

方法信息
域信息

域名称、域类型、域修饰符

注意:如果是private类型的话,是看不到域信息的

non-final类变量

静态变量 在实例对象为null的情况下依然可以访问对象中的静态方法或者字段

全局常量

编译时分配 (准备阶段)

与静态变量比较

静态变量在准备阶段初始化 在initial阶段赋值 也就是在静态语句中 clinit类构造器方法中赋值。而全局变量则不会再这个阶段赋值

运行时常量池

字节码的常量池

存储各种字面量和对类型、方法和域的符号引用

类引用、字段引用、方法引用、数量值、字符串值

将在类加载后存放到方法区的运行时常量池中,这时符号地址就转换为了真实地址

动态性

与常量池的差别

JDK1.6--1.7--1.8的调整

为什么要用元空间替代永久代?

  1. 为永久代设置空间大小是很难确定的
  2. 对永久代的调优很困难

静态变量的位置

1.6--1.7 对象的引用名放在Old Gen中 1.8 在堆中 ?

image-20210304150244295
image-20210304150244295
image-20210304150705423
image-20210304150705423

只要是实例对象 必然会在Java堆中分配

垃圾回收

回收内容:

常量池中废弃的常量和不再使用的类型

苛刻条件: 判断一个类已经被废弃

总结

image-20210304145524447
image-20210304145524447

对象的实例化(面试)

对象的实例化

  1. new
  2. class.newInstance / constructor.new Instance 反射的方式
    • 只能调用空参的构造器,权限必须是public
    • constructor的newInstance 调用空参或者带参数的构造器,权限没有要求
  3. clone()
    • 需要实现Cloneable接口
  4. 反序列化
    • 网络中获取对象的二进制流
  5. 第三方库Objenesis

对象创建步骤

字节码中

new #x : 分配内存、初始化值

dup : Duplicate the top value on the operand stack and push the duplicated value onto the operand stack. 复制操作数堆栈上的顶值,并将复制的值推送到操作数堆栈上。

invokespecial #a : 调用init构造器

流程

  1. 判断对象对应的类是否加载、连接、初始化,(是否有符号引用)
image-20210307084406633
image-20210307084406633
  1. 为对象分配内存
image-20210307084458998
image-20210307084458998
  • 如果内存规整 ------> 指针碰撞

    image-20210307084643494
    image-20210307084643494
  • 内存不规整 ----->虚拟机需要维护一个列表 空闲列表分配

    image-20210307084721903
    image-20210307084721903
  1. 处理并发问题

    CAS 失败重试 区域加锁保证更新的原子性

    每个线程预先分配一个TLAB

  2. 初始化分配空间

    所有属性值默认值,保证对象实例字段在不赋值时可以直接使用

  3. 设置对象的对象头

  4. init方法进行初始化 ---->赋值

对象的内存布局

大厂面试

image-20210307090815341
image-20210307090815341
image-20210307090828271
image-20210307090828271

对象头

  1. 运行时源数据 Mark Work
    1. 哈希值
    2. GC分代年龄
    3. 锁状态标志
    4. 线程持有的锁
    5. 偏向线程ID
    6. 偏向时间戳
  2. 类型指针 指向类元数据,确定该对象的所属属性

实例数据

Instance Data

对象真正存储的有效信息,包括程序代码定义的各种类型的字段

对齐填充

占位符作用

64位 JVM 实际对象必须是8的整数位 只有1byte属性也要给你整到 8 bytes

一个空的对象

image-20210405171441583
image-20210405171441583

两个String属性的对象

image-20210405171535161
image-20210405171535161

网上说的 32bit 4 byte

故 对象头字节固定 为 12 字节(扩充到16字节)

对象头由什么组成

  1. MarkWord 64 bit

    存储对象的hashCode、锁信息或分代年龄或GC标志等信息

    image-20210405190427725
    image-20210405190427725
  2. Class Meta Address 32/64 bit(开启指针压缩

    类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例

小端存储 / 大端存储

image-20210405184732813
image-20210405184732813

对象状态

  1. 无状态 new时
  2. 偏向锁
  3. 轻量
  4. 重量锁
  5. gc标记(无引用)

总结图示

image-20210307091320160
image-20210307091320160

对象访问定位

JVM通过栈帧中的对象引用访问到内部的对象实例

访问方式

  1. 句柄访问

    image-20210307091748884
    image-20210307091748884
    • 空间浪费
    • 稳定 发生移动时外部指向不需要修改
  2. 直接指针 HotSpot使用

    image-20210307091828272
    image-20210307091828272
    • 发生移动时 外部reference需要修改指向的地址

直接内存

基于元空间

Java堆外,向系统申请的内存空间

来源于NIO,使用DirectByteBuffer

NIO :Non-Blocking IO 免去中间商

访问直接内存的速度,优于Java堆

缺点:

  • 分配回收成本高
  • 不受JVM内存回收管理

OOM 设置: MaxDirectMemorySize

不知道,则与堆的最大值 -Xmx参数值一致

执行引擎

概述

物理机的执行引擎:

直接建立在处理器、缓存、指令集和操作系统层面上

JVM的执行引擎:

虚拟机的执行引擎,由软件自行实现,不受物理条件制约定值指令集和执行引擎的结构体系,能执行不受硬件直接支持的指令集格式

JVM主要任务

字节码文件加载到内存中并解释执行, 解释/编译为对应平台上的本地机器指令

  • 执行引擎的执行的字节码指令完全依赖于PC寄存器

Java代码编译、执行的过程

image-20210307094756146
image-20210307094756146

橙色与java虚拟机无关

解释器 Interpreter

根据预定义的规范对字节码采用逐行解释的方式执行

编译器 Just In Time Compiler编译器

JVM将原码编译成和本地机器平台相关的机器指令

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

JVM执行代码时将解释执行和编译执行两者结合起来进行

字节码是一种中间状态的二进制代码,比机器码抽象,需要直译器转译后才能成为机器码

为了实现特定软件运行和软件环境,和硬件环境无关

编译器将源码编译成字节码, 虚拟机器将字节码转译可以直接执行的指令

解释器的使用

为什么要加入字节码?

为什么不直接搞成JVM? 分割工作

解释器发展历史:

字节码解释器 效率低下,逐行翻译

模板解释器 每条字节码和模板函数相关,模板函数直接产生这条字节码执行的机器码 提高性能

基于解释器执行: 低效

即时编译 JIT编译器

避免数被解释执行,将整个函数体编译成机器码 速度快

HotSpot采用的是解释器与即时编译器并存的架构,互相协作取长补短,选择最合适的方式去权衡编译本地代码

JVM启动时,解释器首先发挥作用,随着程序时间推移,JIT编译器逐渐发挥作用,热点探测,将有价值的字节码编译为本地机器指令,以换取更高的执行效率

image-20210307215507412
image-20210307215507412
image-20210307220023296
image-20210307220023296

热点代码和探测方式

何时使用JIT 编译器?

针对运行时频繁调用的热点代码进行深度优化,直接编译为对应平台的本地机器指令 -----栈上替换 OSR

热点探测功能

基于计数器

JVM为每个方法建立2个不同的计数器

  • 方法调用计数器

    统计方法的调用次数

    阈值 1500 / 10000 次

    -XX: CompileThreshold

    对于这张图的理解,我认为是解释方式执行后是不存在Code Cache的,只有JIT编译后才存在缓存。

    热度衰减

    方法调用计数器统计的部署绝对次数,而是相对的执行效率,超过一定时间线度,如果调用次数仍然不足则会减少一半 ,半衰周期

    -XX: UseCounterDecay 关闭热度衰减

    -XX: CounterHalfLifeTime 设置半衰期时间 s

  • 回边计数器 统计循环体执行的循环次数

    image-20210307220949310
    image-20210307220949310

修改HotSpot的编译模式,会发生实质的时间变化

long s = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    System.out.println(i);
}
long e = System.currentTimeMillis();
System.out.println("==========" + (e-s));

-Xcomp 即时编译 3759

-Xint 解释编译 9802

-Xmixed 3847

可以看出,使用混合编译 和 即时编译模式下,相差不大,但解释编译时很慢且相差很大

HotSpot有 两个Jit 编译器 C1 和 C2

  • 64位版本 JVM 默认 是-server模式 使用C2 耗时较长的优化,激进优化,优化的代码执行效率更高

  • -client 模式,C1编译器, 对字节码进行简单和可靠的优化,耗时短, 更快的编译速度

image-20210307222415561
image-20210307222415561

分层编译策略

Graal编译器和AOT编译器

AOT Ahead Of Time Compiler 程序运行之前,将字节码转换为机器吗 与JIT 编译器对比

StringTable

基本特性

不可变性 实现Serializebale接口 之处序列化 实现Comparable接口 可比较大小

JDK 8 后用的是char[]数组存储, JDK9之后使用byte[] + 编码标识存储

image-20210308080434960
image-20210308080434960
  • 在方法内部通过形参传递String对象, 内部改变,外部不会改变
  • 底层是固定大小的HashTable 默认长度是1009,String 多时,Hash冲突,链表很长,影响String.intern
  • -XX:tringTableSize 调整长度
  • jdk 7 中默认长度是 60013
  • jdk8 长度要求最小值是1009
  • 提高map长度,能够在一定程度提高String存储的效率

String的内存分配

image-20210308082215487
image-20210308082215487

为什么Stringtable要调整

permsize默认比较小

永久代垃圾回收频率低

编译期优化

如果拼接的符号前后出现了变量(不是常量),则相当于在对空间中new String 地址就不一样了

String s2= s1+"123" != String s3 = s1 +s4;

String s3 = s1 +s2 字节码原理

new StringBuilder 调用的是append方法 append(s1)---> append(s2) --->StringBuilder. toStrihg方法

例如:

在循环体中 使用 string s = s +"123" 是很耗时的,因为每次拼接都new 一个StringBuilder 以及String 调用append方法,十分耗时!

StringBuilder 扩容优化 直接指定长度 个人测试后发现这里并不明显??

intern()

确保字符串在内存中只有一份

面试题

  1. new String("ab") 会构建几个对象?

    两个

    一个对象是new关键字在堆空间中创建的

    一个对象是字符串常量池中的对象, ldc

     0 new #4 <java/lang/String>
     3 dup
     4 ldc #5 <123>
     6 invokespecial #6 <java/lang/String.<init>>
     9 astore_1
    10 return
    

    而String a = “ab”;仅仅是在常量池中创建ab并把让那个a指向常量池的ab

    只要出现字面量的地方,都默认调用了intern方法将其保存到常量池中

  2. new Sting("a") + new String("b")呢?

    new String 创建在堆控件中

    String a = "a"如果已经有a ,则是常量池中的对象

    6个(包括StringBuilder) 注意 + 的拼接操作是Stringbuilder StringBuilder的toString 会再new 一个String

    注意:new Sting("a") + new String("b")不会生成在字符串常量中生成“ab”,但会在堆中创建该对象(只有有一个被加因素为变量,则就不会在字符串中生成相加后的结果)

    而String a = “abc” + “123”则会在常量池中生成“abc” “123” “abc123”

    toString方法在字符串常量池中没有该String!验证后确实没有

  3. 在jdk 6 和 jdk7/8的区别

    image-20210308162129529
    image-20210308162129529
    image-20210308165709630
    image-20210308165709630
    image-20210308161926144
    image-20210308161926144

    如果变换string s4="11"和s3.intern的位置后结果如何?

​ jdk6 中 s3.intern 创建了新的对象“11”,也就是有新的地址 常量池在永久代中‘

如果常量池中没有,则复制一份,放入常量池中

​ jdk7 中 由于常量池放入堆空间中,他就直接拿堆空间的地址。 常量池记录的是堆空间“11”地址 只占四个字节

​ JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。

String s1 = new String("1")+new String("2");
s1.intern();
String s2 ="12";
System.out.println(s1==s2);

疑问:为什么intern后再string s = “123”相同的字符串,这时java堆既有123在堆中引用,又有在常量池的对象,为什么比较还是一样的?

解答:创建s会直接去常量池中创建,但发现有这个对象了,创建的是指向原来堆中对象的一个引用 没看native层源码,不是非常清楚

易错例题

Q:下列程序的输出结果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因为s2+s3实际上是使用StringBuilder.append来完成,并且还会调用toString,会生成不同的对象。

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

而这里的new String(char[], int, int)与new String(String)不同,前者不会在常量池中生成对应的字符串。经过测试后发现声明char[]类型字符数组不会在常量池中生成字符串,而前者其实最终是调用了Arrays.copyOfRange方法,将String类的char[]数组value进行改变,只针对char[],所以不涉及到常量池。这里可专门出一篇文章细细研究

Q:下列程序的输出结果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因为final变量在编译后会直接替换成对应的值,所以实际上等于s4=”a”+”bc”,而这种情况下,编译器会直接合并为s4=”abc”,所以最终s1==s4。

实际操作中 添加多个相同的new String对象, intern成倍的影响速度

垃圾回收

image-20210309154809269
image-20210309154809269
image-20210309155016450
image-20210309155016450
image-20210309155037374
image-20210309155037374
  • 垃圾: 运行程序中没有任何指针指向的对象

GC的主要作用域 : 方法区 和 堆

垃圾标记算法

对象存活判断

1. 引用计数法

​ 每个对象保留整型的引用计数器属性 记录对象被引用的情况

​ 引用时 +1 引用失效 -1

​ 引用为 0 进行回收

​ 优点: 简单,便于标识,判断效率高,回收无延迟

​ 缺点: 单独的空间存储开销;每次加减 ,时间开销;无法处理循环引用的情况***(致命)**{相互引用,永不为0, 1-->

​ 2--->3--->2 放弃1时, 还存在 2-->3-->2的循环引用} 内存泄露

image-20210309161215885

2. 可达性分析算法

​ JVM 选用

​ 有效解决了循环引用问题,防止内存泄漏发生、简单高效

  • GC Roots :一组必须活跃的引用

    1. 按照GC Roots 为起始点,从上至下 搜索被根对象集合所连接的目标对象是否可达】

    2. 使用可达性分析算法后,内存中存活的对象都会被根对象集合 直接或间接连接,搜索所走过的路径称为引用链

    3. 如果目标对象没有和任何引用链相连,则不可达,标记为垃圾对象

  • GC Roots 可以是什么:

    • JVM栈中对象 方法中的参数、局部变量……
    • JNI引用的对象
    • 类静态属性引用的对象
    • 常量引用的对象 (StringTable 中)
    • 被synch持有的对象
    • JVM内部的引用 Class对象,系统内加载器
    • 一个指针,保存了root堆内存的对象,但他自己又没有在堆里边,他就是一个root

对象的finaliztion机制

对象销毁之前可以自定义处理逻辑

对象回收之前会执行 finalize()方法 ,可以在子类重写该方法

永远不要主动的调用对象的finalize方法

  • finalize可能导致对象复活
  • 执行时间没有保障,由GC线程决定
  • 可能影响GC性能

虚拟机中对象的三种状态:

  1. 可触及的
  2. 可复活的 所有引用被释放,但可能在finalize中复活
  3. 不可触及的 调用finalize 且没有复活

判断一个对象是否可回收 至少两次标记

理解finalize方法 --- 一生只能使用一次的免死金牌

  1. 到GC Roots没有引用连
  2. 筛选,判断是否有必要执行finalize
    1. 没有重写 finalize方法 ----没必要 finalize方法被调用 ----没必要执行
    2. 重写了且未被执行 被插入F-Queue队列中 由优先级较低的Finalizer线程执行
    3. finalize调用是对象唯一能复活的机会,稍后GC会对F-Queue的对象进行二次标记,如果在finalize中该对象和引用链任何一个对象建立了联系,则第二次标记,该对象会被移除“即将回收”集合, 如果伺候,对象再次出现没有引用存在的情况,这个时候由于一个对象的finalize方法只会执行一次,对象直接变成不可触及状态

finalize用法的最好实践

    public static Obj obj;

    public static void main(String[] args) throws InterruptedException {
        obj = new Obj();
        if(obj != null){
            System.out.println("obj is alive");
        }
        obj = null;
        System.gc();	//一定要调用gc 
        Thread.sleep(2000);//注意点
        System.out.println(obj == null?"null":"alive");

    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("xdm我要被回收了 救我");
        obj = this;
    }
}
/*
        obj is alive
        xdm我要被回收了 救我
        -----2s
        alive
	*/

注意点,这里为什么要休眠2s?

Finalizer是低优先级的线程, 并发时 可能会有耗时 ,直接不休眠的话,会显示为null。

Finalizer并不一定会保证finalize被完全执行,如果finalize中有sleep等操作,就不一定能保证语句被执行(因为这时主线程可能已经执行结束)。

使用mat 和 JVisualVM 查看dump堆

JProfiler 进行GC溯源

垃圾清除算法

成功判断死活对象后,就是执行垃圾回收操作了,释放无用对象所占的空间

JVM常见的三种垃圾收集算法

  • 标记-清除算法
  • 复制算法
  • 标记-压缩算法

标记 - 清除 算法

Mark - Sweep

堆中有效内存耗尽 --- 停止整个程序 STW ---标记 / 清除

  • 标记:Collector 从引用根节点开始遍历,标记所有被引用的对象 ,在对象头中记录为可达对象
  • 清除:Collector对堆内存从头到尾遍历(所有),如果发现对象的对象头中没有标记为可达,则回收

缺点:

  1. 效率不高 两次遍历
  2. GC时会STW
  3. 清理出的内存空间是不连续的,需要维护一个列表

清除?

清除不是置空,是把要清除的对象地址,保存在空间的地址列表中,下次有新对象要加载时,判断垃圾的位置空间是不是够,如果够就覆盖

复制算法

解决 标记清除算法在垃圾收集效率的缺陷

将活着的内存分为两块,每次只使用一块,垃圾回收时,将使用的内存的存活对象复制(复制后的就是连续的空间了)到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换角色 (类比Survivor区)

image-20210309181613653
image-20210309181613653

优点:

  • 没有清除标记的过程,实现简单
  • 复制后的空间是连续的,不需要空闲列表 ,使用指针碰撞

缺点:

  • 始终可用的内存空间为1/2
  • G1 分拆成大量region的GC,复制需要维护region之间对象的引用关系,也有时间开销 (Java对象的访问方式)
  • 系统中垃圾很多 ---导致复制算法很慢 适用于Survivor区 (回收性价比高)

标记 - 压缩算法

Mark - Compat

标记: 与 标记清除算法相同

压缩:所有存活的对象压缩到内存的一段,按照顺序排放,之后清理边界外的所有空间

差异: 移动式的

image-20210309183437564
image-20210309183437564

压缩 后内存规整 可以用指针碰撞

优点:

  • 消除了标记清除算法中内存区域分散的缺点、

  • 消除了赋值算法中内存减半的高额代价

缺点:

  • 效率 低于复制算法
  • 移动对象时,如果对象被其他对象引用,还需要调整引用的地址
  • 移动过程中 ,触发STW

对比

image-20210309184113952
image-20210309184113952

分代收集算法

不同生命周期的对象采用不同的收集方式,提高回收效率

几乎所有的GC都采用

  1. Young Gen 回收频繁、生命周期短 Coping

  2. Old Gen 生命周期长、存活率高 Sweep 和 Compat 整合

    Mark 阶段开销与存活对象正比

    Sweep 与管理区域的大小成正相关(堆的遍历)

    Compat 与存活对象的数据正比

增量收集算法

基本思想:GC线程和主线程 交替进行并发清理垃圾,让用户感受不到STW

基础: Sweep 和 Compat

缺点:

线程切换和上下文转换有消耗,使得垃圾回收总成本上升,系统吞吐量下降

(同时做两件事,结果都做的不是很好)

分区算法

堆空间越大 GC时间越长

分割内存区域为多个小块 region ,减少目标停顿时间,每次合理地回收若干个小空间 而不是整个堆空间

概念

System,gy()

Runtijme.getRunTime().gc() 显示触发 Full GC

但是附带免责声明,无法保证对垃圾收集器的调用

调用System.runFinalization 强制调用使用引用对象的finalize方法

  • 主动gc的几种情况

内存溢出

定义: 没有空闲内存且垃圾收集器也无法提供更多内存(OOM前会进行一次Full GC)

内存泄露

没有引用 , 但GC不能进行回收

例如: 单例模式、未调用close方法

STW

可达性分析算法中枚举根节点 GC Roots导致所有的Java线程停顿

  • 确保分析工作在一个一致性的快照中进行,例如JVisualVm生成 堆dump 不同时间是不同的
  • 一致性指整个分析期间 整个执行系统 像被冻结在某个时间点上
  • 如果分析过程中对象的引用关系还在不断变化,则分析结果的准确性也无法保证

JVM后台自动发起STW 开发中不要用STW

理解代码:


垃圾回收的并行和并发

并发

几个程序处于启动状态且都在同一个处理器上完成,时间片

(在同一时间段发生)

垃圾回收的并行: 多条垃圾收集线程并行工作,仍在 STW

多核中并发是怎样的

并行

系统多个CPU或者多核时,一个CPU一个核执行一个进程与另一个CPU或者另一个核同时执行一个线程,就是并行

(在同一时间点发生)

垃圾回收的并发 :用户线程和垃圾收集线程同时进行(交替),不会引起STW

垃圾收集程序在一个核 用户程序在另一个核

安全点和安全区域

安全点

并不是所有的地方都可以立马停下来 GC 必须在特定的地方停下来 即 Safe Point

安全点太少 ---- GC等待时间过长

太多 ---- 性能问题

  • 抢先式中断(未采用)

    暂停所有线程,如果有线程不在安全点上,就回复线程,让线程跑到安全点上

  • 主动式中断

    设置中断标志,各个线程跑到Safe point去看这个 标志,如果是真的,则挂起

安全区

程序block时无法响应 JVM中断 --- Safe region 解决

一段代码片段, 对象的引用关系不变, 任何位置开始GC都是安全的(二维的safe point)

实际执行

  1. 线程跑到safe region时,标识进入了safe region 如果发生GC, 则JVM忽略标识为Safe Region的线程
  2. 准备离开Safe region时,会检查JVM是否完成 GC,如果完成了则继续运行,如果没完成GC,则线程必须等待,知道收到了GC完成的命令 才会离开Safe region

引用

希望有这么一群对象,内存空间够,就在这,不够,就把他抛弃

强引用、软引用、弱引用、虚引用有什么区别,具体使用场景是什么?

强引用

new Object 无论任何情况,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象

Student s1 = new Student();
Student s2 = s1;
s1 = null;
System.gc();
//sleep
syso s2.name;

上述结果能输出s2的名字,因为由于是强引用,s1虽然没有引用了,但s2仍然持有堆空间中对象的引用,所以full gc时没有被清理

软引用Soft

常规gc时不会被回收,只有OOM前或者内存快要满了,将会把这些对象放入回收范围中进行二次回收,如果回收内存不够,才会报OOM

注意软引用的使用方式 -- 使用后要把强引用给置空,使得强引用无效

    Teacher1 teacher = new Teacher1();
    SoftReference<Teacher1> softReference = new SoftReference<Teacher1>(teacher);
    teacher = null;
    softReference.get().setName("123");
    //System.out.println(teacher.getName()); Null pointer
	//-Xms10m -Xmx10m
    try {
        byte[] bytes = new byte[1024 * 1024 * 10];
    }catch (Exception e){
    }finally {
        System.out.println(softReference.get().getName());
        // nullpointer
    }

弱引用Weak

只能生存到下一次垃圾收集之前

WeakHashMap

存储图片信息,内存不足,自动清理 内部Entry继承了WeakReference

虚引用PhantomReference

唯一目的,对象被收集器回收前收到系统的通知,记录回收时间

需要指定引用队列

get拿不到对象

垃圾回收期

image-20210322141038714
image-20210322141038714
image-20210322141151403
image-20210322141151403

-XX PrintCommandlineflags 查看默认垃圾收集器

指标

吞吐量: 运行用户代码时间占总运行时间的比例

垃圾收集开销: 吞吐量的补数,垃圾收集所用时间与总运行时间比例

暂停时间:一个时间段内应用程序线程暂停让GC线程执行

内存占用:Java堆区所占的内寸大小

吞吐量优先 与 注重暂停时间:

image-20210322150414860
image-20210322150414860
  • 高吞吐量:用户感觉只应用线程在做工作,运行快
  • 低暂停时间:交互更好
  • 两者相互矛盾

现在的标准: 在最大吞吐量优先情况下,降低暂停时间

Serial

串行收集器: 单线程 只使用一个CPU或一条收集线程完成垃圾收集工作 还会触发 STW

年代久远Client 模式下默认 、 新生代复制算法、老年代标记整理 、STW、串行回收

Old

标记-整理

Client模式默认

Server模式 与新生代ParallelScavenge配合 、 CMS后备垃圾收集方案

串行收集器采用单线程stop-the-world的方式进行收集。当内存不足时,串行GC设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行GC开始工作,采用单线程方式回收空间并整理内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能有效利用多核优势。事实上,串行收集器特别适合堆内存不高、单核甚至双核CPU的场合。

优点

简单高效 没有线程交互的开销

适用单核 、 嵌入式

-XX :+UserSerialGC

ParNew

Parllel New 对新生代并行回收

新生代回收次数频繁,用并行高效

复制算法 -- STW

是否ParNew的并行收集就一定比Serial串行快?

​ 单CPU环境下,ParNew就没有Serial收集器高,因为这时CPU不需要频繁地做任务切换,因此可以有效避免线程交互过程中不必要开销

-XXL ParallelGCThreadss num 限制线程数量 default 和CPU数据相同的线程

Parallel

复制、并行、STW

年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩

与ParNew不同点:

  • 吞吐量优先,目的为到达可控制的吞吐量

  • 自适应调节策略

高吞吐量 高效利用CPU时间 适合在后台运算不需要太多交互、服务器中

JDK1.6 用Parallel Old 替代了 老年代的Serial Old

-XX +UseParallel(Old)GC 互相激活

-XX:ParallelGCThreads num 设置年轻代并行收集器线程数

image-20210322145201261
image-20210322145201261
image-20210322145313728
image-20210322145313728
image-20210322145728931
image-20210322145728931

CMS

Concurrent Mark Sweep 针对老年代 ---ParNew

低延迟,第一次实现了让垃圾收集线程和用户线程同时工作

尽可能缩短垃圾收集时用户线程的停顿时间

G1 整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。

垃圾收集算法采用标记-清除算法

image-20210324080947878
image-20210324080947878
  • 初始标记 :STW 标记GC Roots 直接关联到的对象
  • 并发标记:从直接关联对象开始遍历整个对象图的过程,释放内存空间,与用户线程同时运行
  • 重新标记:STW 修整并发标记期间,标记产生变动的对象。时间稍短
  • 并发清除:清理删掉标记阶段判断的已经死亡的对象,释放内存空间、

并发时期,要确保程序用户线程有足够的内存可以用。因此CMS收集器不能像其他收集器那样等到老虎年代几乎被填满再回收。当堆内存使用率达到一定阈值,便开始回收,确保应用程序在CMS工作过程中,依然有足够的控件支持应用程序运行。如果CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,JVM启动后备方案,用Serial Old收集器重新进行老年代的垃圾收集,停顿时间长

缺点:

  • 标记清除算法,产生碎片,只能选择空闲列表执行内存分配,不能放入大对象
  • 对CPU资源敏感,并发时占用一部分线程,总吞吐量降低
  • 无法处理浮动垃圾:用户线程产生新的垃圾,并发标记时产生的垃圾无法被标记(注意 重新标记是修成并发标记已标记的对象,即已经是垃圾的可能又不是垃圾了[保险操作])

为什么不换成Mark Compact?

压缩时会对内存进行整理,但是清理的时候是并发执行的,不能改变使用对象的地址

JDK9 废弃 JDK14 已经把CMS移除了

-XX:+UseConcMarkSweepGC 手动指定使用CMS

-XX: CMSlinitialtingOccupanyFraction 设置堆内存使用率阈值 默认 92

G1

区域化分代式

JDK 9 开始采用,面向服务端应用

适应不断扩大的内存和不断增加的处理器数量 降低暂停时间且兼顾良好的吞吐量

为什么 叫G1?

​ 并行收集器,划分堆内存 ,使用不同Region表示Eden Survivorws Old Gen等

垃圾优先:

​ G1 GC 有计 划避免了这个堆中进行全区域的垃圾回收,跟踪不同的Region垃圾堆积的价值大小,回收所获得的空间大小与回收所需时间的经验值。 维护一个优先列表,根据允许的收集时间,优先回收价值最大的Region

特点:

  1. 并行并发兼具:

    有多个GC线程工作,利用多核 STW

    能够与应用程序交替执行,不会STW

  2. 分代收集:

    image-20210324084815558
    image-20210324084815558

    堆空间分成若干Region,可以不连续而且每个小区域的角色是可以变换的????

    G1同时兼顾老年代和年轻代。

  3. 空间整合:

    内存回收以region为基本单位,region之间是复制算法,但整体可以看成是标记整理算法(因为分region)。有利于程序长时间运行

  4. 可预测的停顿时间模型(软实时 Soft Real-Time)

    让使用者明确指定在长度为M ms时间段内,消耗在垃圾收集上的时间不得超过N ms -- 尽可能

    根据允许的收集时间,优先回收价值高的region 收集效率高

JVM GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程

适用场景:

服务端,大内存,多处理器

50%Java堆活动数据占用、对象分配频率或年代提升频率变化大、GC停顿时间长

分区Region

image-20210324091225275
image-20210324091225275

划分整个堆为2048个大小相同的独立Region 每个region控制在1 - 32 M 之间,每个region大小相同,在JVM生命周期内不会被改变。新生代和老年代通过region的动态分配实现逻辑上的连续

G1还增加了新的内存区域 Humonogous 存储大对象。因为将一个短期存在的大对象放入老年代,会对垃圾收集器造成影响。如果单个H区装不下一个大对象,会找一个连续的H区存储,如果还找不到就Full GC了 ,可将H区看做老年代但不完全相等

对单个Region 分配规则类似指针碰撞,也可以在region中给单个线程分配TLAB

内存占用、额外执行负载 比CMS高 (记忆集)

小内存应用上 CMS优于 G1 大内存G1更优

参数:

-XX:+UseG1GC

-XX:G1HeapRegionSize region num 大小,2的 num 次方,范围是1m --- 32m

--XX:MaxGCPauseMills 设置期望的最大GC停顿时间 默认 200ms

-XX ParallelGCThread STW工作线程数,最多 8

-XX ConcGCThreads 设置并发标记的线程数 n设置为并行垃圾收集线程数的1/4左右

-XX InitiaingHeapOccupancyPercent 触发并发GC周期的Java堆占用阈值,超过此值 触发 GC,默认45

垃圾回收过程

  1. YoungGC

  2. 老年代并发标记过程concurrent Marking

  3. Mixed GC

  4. 如果需要,Full GC还是存在的,提供一种失败保护机制,强力回收 --- 单线程、独占式、高强度

    image-20210326155145913
    image-20210326155145913

RemeberedSet 记忆集

  • 一个对象可能被不同区域的对象引用 Old区 引用了 Eden区 。判断对象是否存活,是否要扫喵整个堆?
  • 回收新生代,不得不扫描老年代 , 降低Minor GC效率

解决

  • 使用RemberedSet进行全局扫描
  • 每个Region都有一个Remebered Set
  • 每次引用类型数据进行写操作时,都会有一个Write Barrier (写屏障) 中断 , 检查要写入的引用指向的对象是否和该引用类型数据在不同的region
  • 如果不同,通过CardTable把相关引用信息记录到引用指向对象所在region对应的RS 中(本体)
  • 进行垃圾收集时,在GC根节点的枚举范围加入RS,保证不进行全局扫描也不会有遗漏

回收具体过程

YoungGC

Eden空间,G1触发YoungGC 只回收Eden Survivor区

YoungGC 首先G1停止STW ,G1创建回收集 CS ,即需要背回收的内存分段集合,YoungGC 的 CS包含Eden区和Survivor、区的所有内存分段

image-20210326161642195
image-20210326161642195
image-20210326162531425
image-20210326162531425
image-20210326162602055
image-20210326162602055
image-20210326164133102
image-20210326164133102
image-20210326164237342
image-20210326164237342

https://blog.csdn.net/coderlius/article/details/79272773open in new window

深入篇

字节码

编译

javac编译器

将java源码编译成字节码的前端编译器 IDEA 默认

类似的 ECJ (Eclipse Compiler for Java) 编译效率高,编译时并非全量编译,只编译为编译部分的源码进行编译

Eclipse 和 Tomcat默认使用

Integer的自动装箱

内置的IntegerCache

Integer a = 127;
Integer b = 127;
System.out.println(a == b);
//false 
//字节码 比较时涉及到intValue方法
//new时涉及valueOf方法 
public int intValue() {
    return value;
}
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        //可以自行设置max的最大值
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

涉及到的静态内部类的知识

静态内部类只会在使用到该内部类时才会加载,否则是不会加载的,也就是只有使用到new Integer的valueOf方法时,才会加载该内部类

概念

字节码指令是一个一个字节长度,代表某种特定含义的操作码,以及跟随其后的参数---操作数构成的。

Class文件

不一定以磁盘文件形式存在,是一组以8字节为基础单位的二进制流

存储格式 无符号数 + 表

无法包含注释信息

  • 无符号数: 基本数据类型 用u1、u2、u4.、u8.. 表示多少个字节的无符号数

  • 表: 由多个无符号数或者其他表作为数据项构成的复合数据类型

    表又_info 结尾 整个class文件就是一张表

Class文件结构

image-20210313171015628
image-20210313171015628

魔数

cafebabe 识别字节码文件 4个字节的无符号整数

Class文件版本

主版本、副版本 --- 依据JDK

向下兼容: 高版本JVM可运行低版本JDK编译器生成的class文件

UnsupportedClassVersionError

常量池

字面量从1开始而不是从0 开始 但是计数器从0开始计数

存放字面量和符号引用 --- 类加载时放在方法区的运行时常量池中

image-20210315163802015
image-20210315163802015
image-20210315162456278
image-20210315162456278
image-20210315162921696
image-20210315162921696

解析

第一个字节指出是什么类型(Constant-utf8_info之类的),然后在看对应类型 有什么样的字节,具体会不一样

访问标志

类索引、父类索引、接口索引集合

字段表集合

方法表集合

属性表集合

JVM内存模型JMM

并发编程引入的问题:线程之间如何通信、线程如恶化同步。

线程通信机制:共享内存(Java采用)、消息传递

Java的线程通信由**Java内存模型(JMM)**控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。

重排序

在执行程序时为提高性能,编译器和处理器会对指令进行重排序,可分为三种类型:

  • 编译器优化:在不改变单线程程序语义的前提下,重新安排语句的执行顺序
  • 指令级并行的重排序:处理器采用指令级并行技术将多条指令重叠执行(JVM在此会插入特定的内存屏障)
  • 内存系统的重排序:处理器使用缓存和读/写缓冲区,让加载和存储操作看起来在乱序执行

源代码->编译器重排序->指令级重排序->内存系统重排序->最终执行的指令序列

那重排序有什么坏处呢?

每个处理器上的写缓冲区仅仅对其所在的处理器可见,在多核多线程的条件下就可能出现问题。即处理器对内存的读写操作的执行顺序不一定与内存实际发生的读写顺序一致。考虑以下代码:

//Thread A
a = 1;
x = b;
//Thread B
b = 2;
y = a;

最终缺可能得到x = y = 0的结果,为什么?经过重排序可能会,在a = 1 b = 2执行一半后,也就是两个线程将共享变量写入自己的写缓冲区,这时候从内存中读取另一个共享变量 a, b ,这个时候ab都为0,最后再把自己的写缓存区中保存的脏数据再flush到内存中,这时候结果就是x = y = 0了

private static int x = 0;
private static int y = 0;
private static int a = 0;
private static int b = 0;

public static void main(String[] args) throws InterruptedException {
    int i = 0;
    for (; ; ) {
        i++;
        a = 0;
        b = 0;
        x = 0;
        y = 0;

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;  //指令重排 先执行这个代码,导致x = 0 y = 0
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;   //指令重排 先执行这个代码,导致x = 0 y = 0
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        /**
         * 我们在正常情况下 x y的值组合
         * x = 0  y = 1
         * x = 1  y = 0
         * x = 1  y = 1
         * 不可能出现 x = 0 y = 0的情况 除非发生指令重排
         */
        String result = "第" + i + "次 (" + x + "," + y + ")";
        if (x == 0 && y == 0){
            System.err.println(result);
            break;
        }else {
        }
    }
}

// 第2705893次 (0,0)
// 代码引用: https://blog.51cto.com/u_15127595/4294208

自己的MBP上大概跑了7-8分钟左右,终于跑出来这种情况了!!说明指令重排是存在的,只是上题这种情况发生的条件比较苛刻。

所以,这也就引申出了内存屏障这个概念

为了保证内存可见性,java编译器在生成指令序列的适当位置插入内存屏障指令来禁止特定类型的处理器重排序

内存屏障可分为四类(需要注意的是,一些架构不支持对某些内存屏障的实现)

TypeExampleDesc
LoadLoadLoad1;
LoadLoad;
Load2;
确保Load1数据的装载,之前于Load2以及所有后续装载指令的装载
StoreStoreStore1;
StoreStore;
Store2;
确保Store1数据对其他处理器可见(刷新到内存),之前于Store2以及后续素有存储指令
LoadStoreLoad1;
LoadStore;
Store2;
确保Load1数据装载,之前于Store2以及后续所有存储之类
StoreLoadStore1;
StoreLoad;
Load2;
确保Store1数据对其他处理器可见(刷新到内存),之前于Load2以及后续所有装载指令到装载

StoreLoad Barriers 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。它是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。

happens-before

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before的关系(两个操作即可以属于一个线程又可以属于不同线程

)。需要注意的是:两个操作之间有hb关系,不代表前一个操作必须在后一个操作之前执行,只需要保证前一个操作的执行结果对后一个操作可见(不太懂)

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

  • 监视器锁规则:对一个监视器锁的解锁,happens-before 于随后对这个监视器锁的加锁。

  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  • 传递性:如果 A happens- before B,且 B happens-before C,那么 A happens- before C。

image-20210313233650045
image-20210313233650045

数据依赖性

  • 读后写
  • 写后读
  • 写后写

如果两个操作访问同一个变量,且这两个操作中只要有一个涉及读操作,那这两个操作之间就存在数据依赖性。这三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。

编译器、处理器在重排序时,会遵循数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。但是这仅仅是针对单个处理器、单线程的执行的指令序列,不同线程之间的数据依赖性编译器和处理器是不会考虑的。

as-if-serial

as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守 as-if-serial 语义。为了遵守as-if-serial语义,编译器和CPU不会对存在数据依赖关系的操作做重排序。这就给编写单线程程序的程序员创造了一个幻觉,他们呢就会认为程序是按照顺序来的。

之前验证重排序这一特性的代码中,之所以会出现(0,0)的情况,就是因为两个线程中,每个线程自己的操作是不涉及数据依赖的(虽然看作一个整体的时候会有)

顺序一致性内存模型

特点

  • 一个线程的所有操作必须按照程序的顺序执行
  • 不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行并且立刻对所有线程可见。概念上,该模型有一个单一的全局内存,任意时间点至可能有一个线程连接到内存,当多个线程并发执行时,所有线程的所有对全局内存的读写要进行串行化操作

对于同步(但顺序一致)的程序,执行顺序是全局单一的(A1->A2->A3->B1->B2->B3)

未同步程序在顺序一致性内存模型中,在整体执行顺序是无序的,但所有线程对它自己而言,只能看到一个单一的执行顺序(B1->A1->B2->A2->A3->B3)。但实际上在JMM中,未同步程序不但整体执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致(写缓存区的缘故)。例如正确同步的多线程程序synchronized块中不存在数据依赖的、在临界区内的读写操作就可能进行重排序。对于未同步程序的执行特性,JMM仅提供最小安全性:线程执行时读取的值,要么时之前某个线程写入的值,要么时默认值(0, null, false),JMM保证了线程读操作读到的值不会无中生有(JVM在堆上分配对象时首先会清零内存空间,然后才会分配对象);

查漏补缺篇

担保机制

survivor区装不下了怎么办

晋升原理

大对象是如何分配的

String

String 作为参数在方法内部改变为什么不会改变实际的值

类加载机制

类加载

将Java类的字节码文件加载到内存中 并在内存中构建出java类的原型,类模板对象

  • 通过类的全名,获取类的二进制数据流

    获取方式:

    • class文件
    • jar zip文件
    • 数据库中的二进制
    • http网络协议
    • 运行时动态生成Class的二进制信息
  • 解析类的二进制数据流为方法区的数据结构 Java类模型

  • 创建java.lang.Class 类的实例,表示该类型,作为方法区这个类的各种数据的访问入口

    数组类型的加载,数组类本身并不由类加载器负责创建。由JVM运行时根据需要直接创建的,数组的元素类型仍然需要依靠类加载器创建 ,如果数组的元素类型是引用类型,则数组类的可访问性就由元素类型的可访问性决定,缺省定义为public

链接

验证

保证加载的字节码合法 规范

image-20210320151441865
image-20210320151441865

格式验证

和加载过程一起进行,验证通过后,类加载器才会成功将该类的二进制数据信息加载到方法区中,格式验证之外的操作在方法区中运行

语义验证

是否素有的类都有父类的存在

是否一些定义为final的方法或者类被重写或者继承了

非抽象类是否实现了所有抽象方法 或者接口方法

是否存在不兼容的方法 方法的签名除了返回值都一样的情况

字节码验证

较为复杂的过程

字节码执行过程中,是否会跳转到一条不存在的指令、

函数调用是否传递了正确类型的参数

变量

赋值是不是给了正确的数据类型

栈映射帧 -- StackMapTable用于检测在特定的字节码出,局部变量表和操作数栈是否有正确的数据信息

符号引用

检查 自己要使用其他类 或者方法 是否存在 (在字符串常量池中)

准备

为类的静态变量分配内存

注意点:java并不支持boolean 对于boolean 内部实现是int,默认值是0,故boolean的默认值就是false

  • 不包含基本数据类型用static final修饰的情况,因为final在编译时分配,且在准备阶段会显示赋值
  • 非final修饰的变量,解析阶段进行默认初始化赋值,final修饰后,在解析环节直接显示赋值
  • final类变量不能声明 但不复制
  • 如果以非自变量的形式 初始化static final String ,不会像有具体的代码执行
解析

将常量池中类、接口、字段、方法的 符号引用转换为直接引用 (真实内存中的地址)

例如 JVM为每个类准备了方法表, 调用一个类的方法时,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就转变为目标方法在类中方法表的位置

初始化

显示赋值 装载的最后一个阶段

方法的调用 、 构造器的支持 则在clinit中调用

重要工作 : 执行类的初始化方法 clinit

  • 该方法只能由Java编译器 生成并由JVM调用

  • 由类静态成员的赋值语句以及static语句块合并产生的

  • 静态的类变量,如果没有显示赋值,则不会调用clinit方法

  • 显示赋值的static final 常量 -- 在准备阶段已经显示赋值了,不会调用clinit方法

  • 引用数据类型显示赋值都是在初始化阶段,基本类型 通过库方法赋值 也会在初始化阶段

    public static final int num = new Random().nextInt(199);
    

clinit的线程安全性

多线程环境被正确的加锁、同步 。多线程中尝试加载一个类,没抢到锁的会阻塞,若clnit中有耗时很长的操作,可能引发死锁

clnit中方法 Access flags访问标识 无synchronized 隐式锁

类的主动使用和被动使用

区别: 是否会调用clinit方法

注意点: 没有初始化的类 不一定没有加载

主动使用
  1. new 、反射、克隆、反序列化
  2. 调用类的静态方法 inovokestatic
  3. 使用类、接口的静态字段时
    • 如果不是final,则就是主动使用
    • 如果是final 类型, 则如果是字面量形式,则不是主动使用,如果是函数形式 比如 a = new Random().nextInt类型,当尝试调用a时,则是主动使用(因为函数形式的赋值都在clinit中执行)
  4. Class.forName()
  5. 子类构造时会先进行父类的初始化 -XX traceClassLoad 输出所有类加载顺序
    • 初始化类时,不会先初始化他锁实现的接口,但会加载这个接口Load
    • 初始化接口时,不会初始化他的父接口,但是也会加载Load
  6. 接口定义了default方法,直接、间接实现该方法接口的类初始化(只要使用到了这个接口的字段或者方法的) 都会导致这个接口在这之前初始化
  7. 初始化main所在类
  8. MethodHandle
被动使用
  1. 访问静态字段,只有真正声明这个字段的类才会被初始化
    • 通过子类引用父类的静态变量,不会导致子类初始化,但子类会加载
  2. 声明对象数组,不复制,不会引起初始化
  3. 引用常量不会的触发类或者接口的初始化 (链接阶段已经赋值)
  4. ClassLoader的loadClass方法加载一个类,不是对类的主动使用,注意与forName区分开

类卸载 Unloading

image-20210322092551016
image-20210322092551016
image-20210322092757484
image-20210322092757484

一个类结束生命周期,关键在于其Class对象 结束生命周期

image-20210322093200611
image-20210322093200611

不太可能

动态绑定与静态绑定

Java对对象属性 是静态绑定 ,方法是动态绑定

根据father在堆中实际创建的对象类型Son来确定f1方法所在的位置

调用虚方法时,Java采用的是延迟绑定 / 动态分派的语义,根据被调用对象(receiver)的实际类型来决定选择哪个版本的虚方法。

重载的方法在编译时根据参数的声明类型静态绑定到具体方法上,与运行时该参数的实际类型无关

方法的重载是静态编译

   public static void main(String[] args) {
        Father f = new Son();

        new Test1().get(f);
    }

    public  void get(Father father){
        System.out.println("fatherfather");
    }
    public void get (Son son){
        System.out.println("sonso3n");
    }

双亲委派机制

JDK 1.2

优点

  1. 避免类重复加载,确保类的全局唯一性 --- GC 有关?
  2. 安全,避免构造同包的恶意代码

沙箱安全机制

image-20210404170719177
image-20210404170719177