JVM

1️⃣前言

🐋什么是 JVM ?

Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。

🐬java代码的执行流程

图片

🦀JVM的整体结构

图片

ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。

Method Area:类是放在方法区中。
Heap:类的实例对象。

当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。

方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口

2️⃣JVM 内存结构

🌿程序计数器

Program Counter Register 程序计数器(寄存器)
图片

1.作用:是记录下一条 jvm 指令的执行地址行号。

2.特点

  1. 是线程私有的
  2. 不会存在内存溢出

解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。

多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

🍃虚拟机栈

1.定义

  • 每个线程运行需要的内存空间,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

问题辨析:

  1. 垃圾回收是否涉及栈内存?

    不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

  2. 栈内存分配越大越好吗?

    不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

  3. 方法呢的局部变量是否线程安全?

    如果方法内部的变量没有逃离方法的作用访问,它是线程安全的

    如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

2.栈内存溢出

栈帧过多、第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError

🌾本地方法栈

一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。

🌴堆

1.定义
Heap 堆:通过new关键字创建的对象都会被放在堆内存

2.特点

  1. 它是线程共享,堆内存中的对象都需要考虑线程安全问题
  2. 有垃圾回收机制

3.堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出

🌲方法区

1.定义

java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!

2.组成

Hotspot 虚拟机 jdk1.6 1.8 内存结构图

图片

3.方法区内存溢出

  • jdk1.8以前:java.lang.OutOfMemoryError: PermGen space
  • jdk1.8以后:java.lang.OutOfMemoryError: Metaspace

4.运行时常量池

二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)

常量池:
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息

运行时常量池:
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

比如在下面这个helloword例子中

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

经过编译后的二进制字节码反编译(javap -v Test.class)后得到的类方法定义为:

图片

其中#2#3#4就是二进制字节码要执行的指令,这些#2#3#4的指令要到常量值当中去找,其对应的常量池如下图所示:

图片

比如其中#2就是要找#16,#17指令,#16要找#23为System,#17要找#24 out #25PrintStrem,连在一起#2的指令就是System.out.println

5.StringTable

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

intern方法 1.8

调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功
  • 如果有该字符串对象,则放入失败无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

例1:

public class Main {
    public static void main(String[] args) {
        // "a" "b" 被放入串池中,str 则存在于堆内存之中
        String str = new String("a") + new String("b");
        // 调用 str 的 intern 方法,这时串池中没有 "ab" ,则会将该字符串对象放入到串池中,此时堆内存与串池中的 "ab" 是同一个对象
        String st2 = str.intern();
        // 给 str3 赋值,因为此时串池中已有 "ab" ,则直接将串池中的内容返回
        String str3 = "ab";
        // 因为堆内存与串池中的 "ab" 是同一个对象,所以以下两条语句打印的都为 true
        System.out.println(str == st2);
        System.out.println(str == str3);
    }
}

例2:

public class Main {
    public static void main(String[] args) {
        // 此处创建字符串对象 "ab" ,因为串池中还没有 "ab" ,所以将其放入串池中
        String str3 = "ab";
        // "a" "b" 被放入串池中,str 则存在于堆内存之中
        String str = new String("a") + new String("b");
        // 此时因为在创建 str3 时,"ab" 已存在与串池中,所以放入失败,但是会返回串池中的 "ab" 
        String str2 = str.intern();
        // false
        System.out.println(str == str2);
        // false
        System.out.println(str == str3);
        // true
        System.out.println(str2 == str3);
    }
}

6.StringTable 的位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

7.StringTable 垃圾回收

🥝直接内存

1.定义

直接内存(Direct Memory):是操作系统的内存

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

2.使用直接内存的好处

文件读写流程:

图片
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

使用了 DirectBuffer 文件读取流程
图片
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

3.直接内存回收原理

直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。

  • 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
  • ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存

3️⃣垃圾回收

🥑判断对象是否可以回收

1.引用计数法

当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。

2.可达性分析算法

  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
  • 可以作为 GC Root 的对象
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中 JNI(即一般说的Native方法)引用的对象

3.四种引用

图片

图片

  1. 强引用

    只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

  2. 软引用(SoftReference)

    仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
    可以配合引用队列来释放软引用自身

  3. 弱引用(WeakReference)

    仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
    可以配合引用队列来释放弱引用自身

  4. 虚引用(PhantomReference)

    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,
    由 Reference Handler 线程调用虚引用相关方法释放直接内存

  5. 终结器引用(FinalReference)

    无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。

🍇垃圾回收算法

1.标记清除

定义:Mark Sweep

  • 优点:速度较快
  • 缺点:会产生内存碎片

2.标记整理

定义:Mark Compact

  • 优点:没有内存碎片
  • 缺点:速度慢

图片

3.复制

定义:Copy

  • 优点:不会有内存碎片
  • 缺点:需要占用两倍内存空间

图片

🌵分代垃圾回收

上述三种垃圾回收算法都会在JVM垃圾回收机制中根据不同的情况来采用,不会只用一种算法,而是结合多种算法来进行垃圾回收,所以需要分代垃圾回收让它们协同工作。

分代垃圾回收将堆内存划分为两块,分别为:新生代,老年代

java中长时间使用的对象放在老年代中,用完了可以丢弃的对象放在新生代当中,新生代有伊甸园,幸村区from,幸村区to,新生代的垃圾回收一般叫做minor gc

图片

  • 新创建的对象首先分配在伊甸园区域
  • 伊甸园区域空间不足时,触发 minor gc :伊甸园和 from 区存活的对象使用 - copy 复制到 to 中,存活的对象年龄加一,然后交换 from to
  • minor gc 会引发 stop the world,暂停其他线程,等垃圾回收结束后,恢复用户线程运行
  • 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)
  • 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc(将新生代和老年代都做垃圾回收处理) ,停止的时间更长!

1.相关VM参数

图片

🎄垃圾回收器

我的理解是:当通过可达性分析算法找到需要回收的垃圾对象时,如何去回收垃圾对象就是垃圾回收器的内容,比如常见的垃圾回收器有三种:串行,吞吐量优先,响应时间优先,G1

1. 串行

  • 单线程
  • 堆内存较小,适合个人电脑

-XX:+UseSerialGC = Serial + SerialOld,Serial是新生代中的复制算法,SerialOld是老年代中标记+整理算法
图片
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象

因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

2. 吞吐量优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短 0.2+0.2 = 0.4,垃圾回收间占比最低,这样就称吞吐量高

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC,UseParallelGC是新生代中的复制算法,UseParallelOldGC是老年代中标记+整理算法

图片

3. 响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短 0.1+0.1+0.1+0.1+0.1 = 0.5

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld,
UseConcMarkSweepGC(CMS)老年代的并发的标记清除算法,在垃圾回收的其中几个阶段,其他线程也能工作,进一步减少了stop the world的时间
图片

4.G1

定义:Garbage First

  • 2004 论文发布
  • 2009 JDK 6u14 体验
  • 2012 JDK 7u4 官方支持
  • 2017 JDK 9 默认

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法

G1 垃圾回收阶段

  • Young Collection:对新生代垃圾收集
  • Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
  • Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。

Young Collection

新生代存在 STW:
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!

E:伊甸园,S:幸存区,O:老年代
新生代收集会产生 STW !

伊甸园空间满了
用复制算法放入S
幸存区放入老年代

Young Collection + CM

  • 在 Young GC 时会进行 GC Root 的初始化标记
  • 老年代占用堆空间比例达到阈值(默认45%)时,进行并发标记(不会STW

Mixed Collection

会对 E S O 进行全面的回收(会 STW)

  • 最终标记(Remark)会 STW(在前面并发标记的时候,其他工作线程可能会产生新的垃圾,所以需要最终标记找到所有需要回收的垃圾,和CMS挺类似的)
  • 拷贝存活(Evacuation)会STW

问:为什么有的老年代被拷贝了,有的没拷贝?

因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

Full GC

G1 在老年代内存不足时(老年代所占内存超过阈值)

  • 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
  • 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。

Young Collection 跨代引用

  • Young Collection 跨代引用是新生代回收的跨代引用(老年代引用新生代)问题

  • 为什么会有这个问题?因为新生代要从GC Root 对象通过可达性分析算法找到垃圾,但是有些GC Root 对象放在老年代中,老年代很大,如果遍历老年代去找GC Root 对象效率比较低,所以将老年代的区域再次细分,老年代的对象引用了新生代的对象,那么称其为脏卡

  • Remembered Set 存在于E中,用于保存新生代对象对应的脏卡,这样子做E的垃圾回收时,可以直接找到老年代中GC Root 对象进行遍历

  • Young Collection 跨代引用加速了新生代的垃圾回收效率

🍂垃圾回收调优

我个人的观点是:不同的项目需要不同的垃圾回收策略,通过观察项目运行各种参数和硬件条件,选择合适的垃圾回收策略就是垃圾回收调优

调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io
  • gc

确定目标
选择是低延迟还是高吞吐量? 低延迟就是响应时间优先。比如你做科学计算就可以考虑高吞吐量的方案,如果做电商网页服务的就要考虑低延迟的方案

  • CMS G1 ZGC(低延迟)
  • ParallelGC(高吞吐量)
  • Zing(低延迟,几乎零停顿)

最快的垃圾回收
答案是不发生垃圾回收!如果发生了垃圾回收,可以查看整个前后垃圾回收的内存占用,考虑下面几个问题

  • 数据是不是太多?
    • resultSet = statement.executeQuery("select * from 大表 limit n")
  • 数据表示是否太臃肿?
    • 对象图冗余过多
    • 对象大小,比如Integer为24字节 ,int则为4字节
  • 是否存在内存泄漏?
    • static Map map = 一直加数据
    • 考虑软弱引用
    • 考虑第三方缓存实现

案例
案例1:Full GC 和 Minor GC 频繁

  • 解决方案:增加新生代的内存空间

案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)

  • 解决方案:这里单次暂停时间不是Full GC,是重新标记线程多时间耗费长,所以在重新标记前先进行垃圾回收

案例3:老年代充裕情况下,发生 Full GC(jdk1.7)

  • 解决方案:PermGen 元空间不足,加大PermGen空间大小

4️⃣类加载与字节码技术

java 代码的执行流程
图片

🌞类文件结构

类文件结构就是上图中 字节码(xxx.class)文件,我们来观察它的结构。比如这边HelloWorld的代码

package cn.itcast.jvm.t5; 
// HelloWorld 示例 
public class HelloWorld { 
    public static void main(String[] args) { 
        System.out.println("hello world"); 
    } 
}

经过javac HellowWorld.java编译为 HelloWorld.class 后是这个样子的:

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13 
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
0001120 00 00 02 00 14

上述文件就是我们的字节码文件,只要直接在cmd命令中java 字节码文件就能直接运行java代码

根据 JVM 规范,类文件结构如下:

u4 			   magic
u2             minor_version;    
u2             major_version;    
u2             constant_pool_count;    
cp_info        constant_pool[constant_pool_count-1];    
u2             access_flags;    
u2             this_class;    
u2             super_class;   
u2             interfaces_count;    
u2             interfaces[interfaces_count];   
u2             fields_count;    
field_info     fields[fields_count];   
u2             methods_count;    
method_info    methods[methods_count];    
u2             attributes_count;    
attribute_info attributes[attributes_count];

魔数
0~3 字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
ca fe ba be :意思是 .class 文件,不同的东西有不同的魔数,比如 jpg、png 图片等!

版本
4~7 字节,表示类的版本 00 34(52) 表示是 Java 8
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
00 00 00 34:34H(16进制) = 52(10进制),代表JDK8

🦀字节码指令

上面我们看了字节码(xxx.class)的类文件结构,现在我们看下这个字节码(xxx.class)是如何在JVM内存结构中运行的

图解方法执行流程

一开始常量池载入运行时常量池,方法字节码载入方法区
图片

执行引擎开始执行字节码 : bipush 10
将一个 byte 压入操作数栈
图片

istore 1
将操作数栈栈顶元素弹出,放入局部变量表的 slot 1 中
对应代码中的 a = 10
图片

🐶编译期处理

在编译期间对字节码的优化和处理叫做语法糖
所谓的 语法糖 ,其实就是指 java编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

1.默认构造器

public class Candy1 {

}

经过编译期优化后

public class Candy1 {
   // 这个无参构造器是java编译器帮我们加上的
   public Candy1() {
      // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
      super();
   }
}

2.默认构造器

基本类型和其包装类型的相互转换过程,称为拆装箱
在 JDK 5 以后,它们的转换可以在编译期自动完成

public class Candy2 {
   public static void main(String[] args) {
      Integer x = 1;
      int y = x;
   }
}

转换过程如下

public class Candy2 {
   public static void main(String[] args) {
      // 基本类型赋值给包装类型,称为装箱
      Integer x = Integer.valueOf(1);
      // 包装类型赋值给基本类型,称谓拆箱
      int y = x.intValue();
   }
}

3.泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

public class Candy3 {
   public static void main(String[] args) {
      List<Integer> list = new ArrayList<>();
      list.add(10); //实际调用的是 List.add(Object e)
      Integer x = list.get(0);//需要将 Object 转Integer Integer x = (Integer)list.get(0);
   }
}

4.可变参数
可变参数也是 JDK 5 开始加入的新特性: 例如:

public class Candy4 {
   public static void foo(String... args) {
      // 将 args 赋值给 arr ,可以看出 String... 实际就是 String[]  
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo("hello", "world");
   }
}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:

public class Candy4 {
   public Candy4 {}
   public static void foo(String[] args) {
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo(new String[]);
   }
}

注意,如果调用的是 foo() ,即未传递参数时,等价代码为 foo(new String[]{}) ,创建了一个空数组,而不是直接传递的 null .

5.foreach 循环
仍是 JDK 5 开始引入的语法糖,数组的循环:

public class Candy5 {
	public static void main(String[] args) {
        // 数组赋初值的简化写法也是一种语法糖。
		int[] arr = {1, 2, 3, 4, 5};
		for(int x : arr) {
			System.out.println(x);
		}
	}
}

编译器会帮我们转换为

public class Candy5 {
    public Candy5() {}

	public static void main(String[] args) {
		int[] arr = new int[]{1, 2, 3, 4, 5};
		for(int i = 0; i < arr.length; ++i) {
			int x = arr[i];
			System.out.println(x);
		}
	}
}

6.switch 字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

public class Cnady6 {
   public static void main(String[] args) {
      String str = "hello";
      switch (str) {
         case "hello" :
            System.out.println("h");
            break;
         case "world" :
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

在编译器中执行的操作

public class Candy6 {
   public Candy6() {
      
   }
   public static void main(String[] args) {
      String str = "hello";
      int x = -1;
      // 通过字符串的 hashCode + value 来判断是否匹配
      switch (str.hashCode()) {
         // hello 的 hashCode
         case 99162322 :
            // 再次比较,因为字符串的 hashCode 有可能相等
            if(str.equals("hello")) {
               x = 0;
            }
            break;
         // world 的 hashCode
         case 11331880 :
            if(str.equals("world")) {
               x = 1;
            }
            break;
         default:
            break;
      }

      // 用第二个 switch 在进行输出判断
      switch (x) {
         case 0:
            System.out.println("h");
            break;
         case 1:
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

在编译期间,单个的 switch 被分为了两个,那为什么要这样子呢?使用 hashCode 是为了提高比较效率,使用 equals 是防止有 hashCode 冲突(如 BM 和 C .)

🥭类加载阶段

1.加载

  • 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类

图片

  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

2.链接(验证,准备,解析)

验证

  • 验证类是否符合 JVM规范,安全性检查
  • 用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

准备

  • 为 static 变量分配空间,设置默认值

解析
将常量池中的符号引用解析为直接引用

3.初始化

\(<cinit>()v\) 方法
初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机
概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

🥰类加载阶段

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等!

名称 加载的类 说明
Bootstrap ClassLoader(启动类加载器) JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader(拓展类加载器) JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application ClassLoader(应用程序类加载器) classpath 上级为Extension
自定义类加载器 自定义 上级为Application

1.启动类的加载器
可通过在控制台输入指令,使得类被启动类加器加载

2.拓展类加载器
如果 classpath 和 JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载。

3.双亲委派模式
双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则。

4.自定义类加载器

使用场景

  • 想加载非 classpath 随意路径中的类文件
  • 通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤

  • 继承 ClassLoader 父类
  • 要遵从双亲委派机制,重写 findClass 方法
  • 不是重写 loadClass 方法,否则不会走双亲委派机制
  • 读取类文件的字节码
  • 调用父类的 defineClass 方法来加载类
  • 使用者调用该类加载器的 loadClass 方法

🌸运行期优化

5️⃣内存模型

posted @ 2023-08-03 09:25  傻傻的小小豪  阅读(28)  评论(0编辑  收藏  举报