JVM

一.JVM内存结构

1.程序计数器 (Program Counter Register)

  • 作用:记录下一条JVM指令的地址
  • Java源代码进行编译会被转化为二进制字节码(JVM指令)
  • 虚拟机中的解释器负责把从程序计数器中得到的JVM指令地址解释转化为机器码,将机器码交由CPU执行
  • 特点:线程安全,每个线程都有其私有的程序计数器

2.栈(Stacks)

  • 作用:提供线程运行需要的内存空间,主要用于存储局部变量和对象的引用变量
  • 栈帧:每个方法运行需要的内存空间,包含方法里的参数、局部变量和返回地址
  • 特点:方法内的局部变量是线程安全的,但是方法内传入的参数和被返回的局部变量都是非线程安全的

每进入一个方法就会将其对应的栈帧入栈

帧栈结构

  • 局部变量表:用于存放方法参数和方法内部定义的局部变量,内部分为一个个槽(Slot),0位slot存放this,接下来存放方法的接收参数,然后是方法内部定义的变量
  • 操作数栈:后进先出栈,用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间,我们所说的jvm的解释引擎是基于栈的执行引擎,这个栈就是操作数栈。jvm字节码指令表所说的入栈就是操作数栈
  • todo
  • todo

内存诊断:jstack pid

另外:在写一些递归算法题时可能会出现栈内存溢出的情况,就是方法调用次数过多内存又得不到释放,超出栈内存限制导致的。

3.本地方法栈(Native Method Stacks)

  • 作用:用来存放本地方法的栈空间

  • 本地方法:那些由非Java编写的方法(C、C++…)。因为有些功能无法用java实现,需要使用C、C++来操作底层操作系统,这些方法名前都会有个native关键字。

4.堆(Heap)

通过new关键字,由程序员自行创建的对象都会使用堆内存,主要是存放对象实例和数组,也存放类的成员变量(也可以说是全局变量)

特点:

  • 它是线程共享的,堆中的对象都要考虑线程安全的问题
  • 有垃圾回收机制

堆内存诊断:

  • 使用jps查看当前Java进程情况,查看所需进程的pid
  • 使用 jhsdb jmap –heap –pid ‘pid’ (jdk1.8以上)查看进程堆内存使用情况
  • 也可以使用jconsole命令运行jconsole.exe,通过使用用户界面直观地查看

5.方法区

这块区域被所有JVM线程共享,存储着每一个类的类结构,例如:运行过程中的常量池、字段、方法数据、字节码指令以及函数和构造器的代码。

方法区在虚拟机运行时被创建,尽管逻辑上它应该属于堆的一部分,但一些实现商可能不会选择将方法区分配到堆中。

image-20230131220626854

  • 常量池:一张常量表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量(由字母、数字等构成的字符串或者数值常量)等信息
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址
  • 串池(StringTable):每次创建一个串池中不存在的字符串常量时就会将其存入串池中。若在创建字符串常量时串池中存有,则会直接从池中获取。原先1.6之前串池是在方法区中,而1.8后串池被设置在堆中
public static void main(String[] args) {
    String s1 = "a"; // 懒惰的
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
    String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
    System.out.println(s3 == s5);//true
}

前三条定义后a、b、ab三个字符串常量就会被存入串池中,执行第四句时由于是字符串变量拼接,会先调用StringBuilder拼接后再new一个String对象接收其拼接值,所以s3和s4是两个不同的地址。

而s5虽然看起来是两个字符串常量拼接,但实际和s3一样,都是从串池中取得ab,所以s3和s5的地址都是同一个。

另外:Stringxxx.intern()会尝试把该对象的字符串尝试放入串池,

在jdk6中,如果存在则然后返回串池中的该对象;如果串池中不存在该字符串对象,则加入串池,然后返回串池中的该对象

而在jdk7中,由于串池移到堆中,如果串池中存在该对象,则返回串池的对象;如果串池中不存在该字符串对象,则加入串池,然后返回堆中的该字符串引用(毕竟串池就在堆中,就)

String s3 = new String("1") + new String("1");//堆中有"1"、"11"对象,池中只有"1"对象
String intern = s3.intern(); //池中不存在"11",所以将堆中的"11"加入池中,此时池中有"1"、"11"对象
//intern()会返回堆中的"11"对象,所以变量intern是堆中的"11"的引用,因此 intern==s3
System.out.println(s3 == intern);


String s4 = "11"; //此时因为intern()的缘故,串池中存在"11",s4是拿的堆中的“11”对象引用,因此s3==s4
System.out.println(s3 == s4);

6.直接内存

image-20230210225227846

首先要知道的是,Java本身是做不到读写操作的。JVM在继续I/O操作的流程如上图:CPU首先从用户态也就是Java程序调用一些native方法进入到内核态继续读写,然后再返回到用户态。

内存方面:从磁盘读到数据后会把数据存放在系统内存的缓冲区中,然后再把这些数据复制一份到Java堆内存中,此时是会存在两份数据的。

这种读写方法是提供Java普通的byte数组进行读写的 byte[] buf = new byte[1Mb];

image-20230223121100544

ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);

在使用ByteBuffer读写的话,会创建一块系统和jvm都能共同访问的内存空间来存储读写数据,这样就能减少一次数据的拷贝。这块共享内存就叫做直接内存。而gc操作是不会把直接内存回收掉的。

直接内存的分配和释放需要Unsafe对象的setMemoryfreeMemory方法,并且必须要手动调用freeMemory方法才能释放内存。

ByteBuffer对象内部会创建DirectByteBuffer对象,其内部使用Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被回收,则Cleaner会创建一个新的线程执行其run方法,run的就是freeMemory方法。

总结:虚引用关联的对象(ByteBuffer)被回收了,就会触发虚引用对象内部的clean方法,继而调用unSafe的freeMemory()方法

二.垃圾回收

1.如何判断对象的是否可以回收

Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

1.1.可达性分析算法:扫描堆中的对象,寻找该对象是否在沿着GC Root对象为起点的引用链上,如果找不到,表示该对象没有被引用,可以回收。

1.2.四种引用(引用本身也是一个对象)

  • 强引用:直接引用,是指创建一个对象并把这个对象赋给一个引用变量,这个变量就是对这个对象的一种引用。GC时永远不会被回收,宁愿抛出OutOfMemory错误也不会回收这种对象
  • 软引用:间接引用,被软引用的对象如果同时也被强引用着,则不会被回收;若该对象只被软引用时,以下情况会被回收:GC后内存还是不够用,才会回收被软引用的对象
  • 弱引用:间接引用,只要发生了GC,不管GC后内存是否充足,被弱引用的对象都会被回收
  • 虚引用(PhantomReference):必须配合引用队列使用,主要配合ByteBuffer使用。虚引用对象被释放时,就会被放入引用队列,从而间接的调用虚引用Cleaner,开启一个线程执行Unsafe.freeMemory回收直接内存
  • 引用队列:引用自身也是一个对象,当引用的对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。因此当引用的对象被回收后,该引用就会被放入引用队列中。
// 创建一个软引用类型的List
List<SoftReference<byte[]>> list = new ArrayList<>();

// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

for (int i = 0; i < 5; i++) {
    // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
    SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
    System.out.println(ref.get());
    list.add(ref);
    System.out.println(list.size());
}

2.垃圾回收算法

2.1标记清除

标记需要回收的内存块起末地址,进行清除

image-20230218180033562

2.2.标记整理

标记需要回收的内存块起末地址,进行清除,然后再把占用着的内存移动到一起

image-20230218180430057

2.3.复制

准备两块内存空间(FROM,TO),将FROM中的占用空间复制到TO中,把FROM清除,然后两块内存空间的身份互换

image-20230218180502833

3.分代垃圾回收

  • 新生代:用于存放需要经常回收和分配的对象

  • 老年代:一直被引用着、自身状态比较稳定的对象的存放区域

  • 新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

  • 老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

  • FROM和TO都称为幸存区

image-20230218201152400

  • 对象首先分配在伊甸园区域
  • 当新生代空间不足时,触发minor gc,伊甸园和FROM区域存活的对象会被copy复制到TO区域中,清理FROM区,然后FROM和TO交换身份,存活的对象年龄会加1
  • minor gc会引发stop the world(STW,暂停其他用户线程),等到gc完成后用户线程才恢复运行
  • 当对象年龄超过阈值(4bit,所以最大为15)时,会晋升老年代
  • 如果老年代空间不足,会先触发minor gc,如果之后还是空间还是不足,则会触发full gc,STW的时间更长

image-20230219233427654

4.垃圾回收器

4.1.串行垃圾回收器(Serial收集器)

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

image-20230219203900867

过程:进行串行垃圾回收时,所有用户线程都要阻塞等待(安全点),垃圾回收线程回收后用户线程才会继续运行

4.2.吞吐量优先垃圾回收器(Parallel收集器)

  • 多线程
  • 堆内存较大,多核CPU
  • 吞吐量:运行用户代码时间/总程序的执行时间(运行时间+STW时间) ,换句话说就是让单位时间内,STW的时间最短

image-20230219220433807

过程:安全点后所有线程都进行垃圾回收,回收完后再恢复运行。

4.3.响应时间优先垃圾回收器(CMS(Concurrent Mark Sweep)收集器)

  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让单次STW的时间最短

image-20230219215417024

过程:安全点A用户线程阻塞,垃圾回收线程进行初始标记,安全B后用户线程恢复运行,回收线程进行并发标记。由于是并发标记,可能存在一些内存更新,所以安全点C后所有线程都需要重新标记(扫描堆的所有对象),安全点D后用户线程恢复运行,垃圾回收线程并发清理后再恢复运行。

初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行

而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录

4.4.G1(Garbage First收集器)

虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:

G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以 根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式

新生代垃圾回收

image-20230223120652372

5.垃圾回收调优

java -XX:+PrintFlagsFinal -version | findstr “GC” 打印虚拟机的GC相关参数

5.1 新生代调优

5.1.1 新生代的特点:

  • 所有的new操作的内存分配非常廉价
  • 死亡对象的回收代价是零
  • 大部分对象用过即死
  • minor gc的时间远远低于full gc
  • TLAB(thread-local allocation buffer):一块线程隔离的用于分配新生代的伊甸园内存,因为内存的分配也是需要注意线程安全的,TLAB的作用就是为了减少分配内存时线程对内存的抢占

5.1.2 新生代的空间分配得越大越好吗?

  • -Xmn
  • 新生代空间太小容易触发Minor GC,太大老年代空间容易不足,从而引发Full GC导致回收时间变长
  • 推荐新生代的大小为堆内存空间的25%~50%

5.1.3 新生代的幸存区(FROM and TO )

​ 当新生代空间比较小时,JVM会动态调整晋升的阈值,提前将幸存对象晋升到老年代

5.2 老年代调优

以CMS为例

  • CMS的老年代内存越大越好,避免回收时产生的浮动垃圾导致老年代空间不足,引起并发回收失败,从而退化为串行回收,回收时间变长
  • 浮动垃圾:并发回收的时候其他用户线程也能运行,此时产生的垃圾称为浮动垃圾
  • 如果没有Full GC,先尝试调优新生代
  • 给观察发送Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3

三、类加载和字节码

1.类文件结构

image-20230225121743498

cafe babe 0000 0034 0099 0a00 2800 560a

字节码为16进制,两位为1字节

魔数:存在文件字节码中,用于识别文件的文件类型

1.在java中,Class文件的头0~3字节是魔数,cafebabe是class文件的标识

cafe babe 0000 0034 0099 0a00 2800 560a

2.之后的4~7个字节是class的版本,jdk版本从45开始,Java 8是52,十六进制为0x34

​ cafe babe 0000 0034 0099 0a00 2800 560a

8~9字节,表示常量池长度,0x99(153d)表示常量池中有#1-#152项,其中#0项不计入,因为没有值

​ cafe babe 0000 0034 0099 0a00 2800 560a

先了解到这里

2.字节码指令

2.1 javap工具

在JDK的bin目录中,Oracle公司已经为我们 准备好一个专门用于分析Class文件字节码的工具:javap

指令:javap -v XXX.class

帧栈结构

  • 局部变量表:用于存放方法参数和方法内部定义的局部变量,内部分为一个个槽(Slot),0位slot存放this,接下来存放方法的接收参数,然后是方法内部定义的变量
  • 操作数栈:后进先出栈,用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间,我们所说的jvm的解释引擎是基于栈的执行引擎,这个栈就是操作数栈。jvm字节码指令表所说的入栈就是操作数栈

2.2.1 结合帧栈结构分析字节码指令

// 以这段程序为例
public void getSum(){
    int m = 10;
    int n = 20;
    int k = m+n;
}

// 以下为通过javap反编译上面程序class文件后得到的字节码指令,编译过程如下:
 Code:
      stack=2, locals=3, args_size=0   //操作数栈stack的最大深度为2,局部变量表locals大小为3
         0: bipush        10     // 将10入操作数栈
         2: istore_0             // 10出栈,存入局部变量表
         3: bipush        20     // 20入栈
         5: istore_1			// 20出栈,存入局部变量表
         6: iload_0				// 将局部变量表中索引为0的数值10入栈
         7: iload_1				// 将局部变量表中索引为0的数值20入栈
         8: iadd				// 执行引擎执行,10和20依次出栈,相加得到30入栈
         9: istore_2			// 30存入局部变量表
        10: ireturn				// 栈帧出栈
            
 // 补充:iinc指令是直接在局部变量表slot上运算
 // 那么a++和++a是先执行iload还是先执行iinc?
 // 先使用先入栈iload   a++ :先iload再iinc   ++a: 先iinc再iload
 // 例子
public static void getSum() {
    int a = 0;
    int i = 0;
    while(i<10){
        a = a++;
        i++;
    }
    System.out.println(a);   a = 0
}

// javap反编译后的jvm指令
// 由于a++是 先iload再iinc,先读取局部变量表中的0后将0入栈,局部变量表中的a:0自增
// 可是在赋值操作时,istore_0指令又将栈中的0覆盖掉变量表中的1,最后还是为0
10: iload_0
11: iinc       0, 1
14: istore_0

2.2.2 代码块

编译器会按从上至下的顺序,收集所有的static静态代码块和静态成员赋值的代码,合并为一个特殊的方法:()V,类构造器方法,该方法会在类加载的初始化阶段被调用。()V,对象构造器方法

static int i = 10;
static {
    i = 20;
}
static {
    i = 30;
}
 
 Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return

2.3.多态原理

jhsdb hsdb   //打开java的HSDB可视化工具

当执行invokevirtual指令(调用实例方法)时,

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际Class
  3. Class结构中有Vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查vtable表得到方法的具体地址
  5. 执行方法的字节码

2.4.异常处理

2.4.1 try-catch
public static void main(String[] args) {
    int i = 0;
    try {
        i = 10;
    } catch (Exception e) {
        i = 20;
    }
}

Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1              // 10存入局部变量表
         5: goto          12
         8: astore_2              // 发生异常,将异常引用对象存入局部变量表2号slot
         9: bipush        20
        11: istore_1
        12: return
      Exception table:            // try,catch代码块会出现一个Exception table,用于检测到异常实现指令跳转
         from    to  target type  // 检测2~5行的try代码块,如果出现异常就跳转到第8行
             2     5     8   Class java/lang/Exception     

  

如果是多个catch代码块,Exception table中检测的异常数增加,由于同时刻只会发生一种异常,所以用同一个slot来存储异常

2.4.2 finally
public static void main(String[] args) {
    int i = 0;
    try {
        i = 10;
    } catch (Exception e) {
        i = 20;
    } finally {
        i = 30;
    }
}

Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10   // try
         4: istore_1
             
         5: bipush        30    // finally1
         7: istore_1
             
         8: goto          27
        11: astore_2 
        12: bipush        20   //catch
        14: istore_1
            
        15: bipush        30   // finally2
        17: istore_1
            
        18: goto          27
        21: astore_3
            
        22: bipush        30    //finally3
        24: istore_1
            
        25: aload_3
        26: athrow
        27: return
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any  //检测除Exception类型外的父级或平级异常
            11    15    21   any  // 如果发生了剩余异常保证也能执行finally3

finally代码块的指令会被复制到try块和catch块中,保证最后finally块会被执行。

需要注意的是:

1.不要在finally里出现return语句,因为return会把执行return指令,吞掉astore指令,异常无法存入局部变量表,从而导致即使出现异常也无法发现

2.在try块中return的变量不会被finally块改变,因为该变量被存储在slot1后还会被留一个备份存储在slot2,接下来执行被复制的finally块指令覆盖的是slot1的值,而reurn时是找slot2的值

2.5.加锁synchronized

public static void main(String[] args) {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println("ok");
    }
}

Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1						// 存object对象到slot1
             
         8: aload_1							// object1入栈
         9: dup								// 复制栈顶元素(object对象)并压栈
        10: astore_2						// object2出栈,存复制的object2对象到slot2
        11: monitorenter					// 获得object1对象的锁
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2							// 读取slot2对象object2,入栈
        21: monitorexit						// 释放对象object的锁
        22: goto          30
        25: astore_3						// 如果synchronized发生异常,异常引用存储到slot3
        26: aload_2							// 读取局部变量表的第二个object对象入栈
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 12
        line 9: 20
        line 10: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;

1.首先将Object引用存入slot1,然后读取slot1入栈。复制栈顶元素(Object引用)压栈,栈顶Object引用出栈存入slot2,此时局部变量表slot1和slot2都存有同个对象引用,操作数栈中有一个object引用。

2.出栈,获取object的锁,执行同步块里的代码。然后读取slot2的object引用入栈,出栈释放对象锁。

3.如果同步块发现异常:异常引用存入slot3,读取slot2引用入栈,再出栈释放对象锁。

3.编译期处理

3.1语法糖

其实是指Java编译器(如Idea)把 *.java 源码编译成 *.class字节码的过程中,自动生成和转换的一些伪代码,和java源码类似。主要是为了减轻程序员的负担

3.1.1 默认构造器

public class Candy1 {
}

// 编译成class后的伪代码
public class Candy1 {
    public Candy1() {
    }
}

3.1.2 可变参数

public class Candy4 {
    public static void foo(String... args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo("hello", "world");
    }
}

// 可变参数本质是数组,编译器在编译期间会将args转换为string[]
public class Candy4 {
    public Candy4() {
    }

    public static void foo(String... args) {
        System.out.println(args);
    }

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

3.1.3 foreach循环

public class Candy5_1 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
        for (int e : array) {
            System.out.println(e);
        }
    }
}
// foreach会被编译器转化为fori循环,而且是将array数组赋值给一个新的数组变量,
// 这就是在foreach循环里无法实质2改变被遍历数组的元素的原因,因为改变的实际上是临时变量
public static void main(String[] args) {
        int[] array = new int[]{1, 2, 3, 4, 5};
        int[] var2 = array;
        int var3 = array.length;
        for(int var4 = 0; var4 < var3; ++var4) {
            int e = var2[var4];
            System.out.println(e);
        }
 }


public class Candy5_2 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}
//foreach遍历集合,实际上编译器会转化为while循环遍历迭代器iterator
public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    Iterator var2 = list.iterator();

    while(var2.hasNext()) {
        Integer i = (Integer)var2.next();
        System.out.println(i);
    }

}

3.1.4 switch判断字符串

switch (str) {
    case "hello": {
        System.out.println("h");
        break;
    }
    case "world": {
        System.out.println("w");
        break;
    }
}

// switch实际上还是只能case整型
// switch判断字符串实际上分为两个switch步骤:
// 1.选择字符串的哈希值进行case,对于case下判断字符串值是否匹配,匹配就给var2赋值(从0开始)
// 2.选择var2的值进行case,对应case下执行源码中的case块代码
switch(str.hashCode()) {
        case 99162322:
            if (str.equals("hello")) {
                var2 = 0;
            }
            break;
        case 113318802:
            if (str.equals("world")) {
                var2 = 1;
            }
        }

        switch(var2) {
        case 0:
            System.out.println("h");
            break;
        case 1:
            System.out.println("w");
        }
}

问题1:为什么case了字符串的哈希值后还要equals判断?

回答:因为绝大部分的字符串的哈希值不一样,但还是有一些字符串哈希值是一样的。例如“C.”和”BM”的哈希值都是2123,这时候为避免哈希冲突,所以要进行equal判断。

问题2:既然始终要进行equals判断为什么不去掉哈希值case直接equals判断呢?

回答:这是笔者出现过的一个疑问,实际上很傻。为什么?因为编译器本来就只能允许case整型啊,所以才会用哈希值的方法来间接实现case字符串的功能。

3.1.4 枚举类

public enum t {
    SPRING,SUMMER;
}


// 反编译后的伪代码
// 枚举实际上是一个自身不能被继承并且自身继承了Enum的子类T
// 一个枚举属性是一个T实例:SPRING = new T("SPRING", 0);
public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }
 
    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(demo/T, s);
    }
 
    public static final T SPRING;
    public static final T SUMMER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER
        });
    }
}

3.1.5 try-with-resources

image-20230227001641419

try(InputStream is = new FileInputStream("d:\\1.txt")) {
    System.out.println(is);
} catch (IOException e) {
    e.printStackTrace();
}

image-20230227002332302

3.1.6 匿名内部类

public static void main(String[] args) {
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("running...");
        }
    };
}


// 反编译后的伪代码
public static void main(String[] args) {
    //用额外创建的类来创建匿名内部类对象
    Runnable runnable = new Demo8$1();
}

//创建了一个额外的类,实现了Runnable接口
final class Demo8$1 implements Runnable {
   public Demo8$1() {}

   @Override
   public void run() {
      System.out.println("running...");
   }
}

----------------------------------------------------------------------------------------------------
// 如果匿名内部类中引用了外界的局部变量
//final修饰的局部变量
public static void test(final int x){
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            //引用局部变量
            System.out.println(x);
        }
    };
}


final class Demo8$1 implements Runnable {
   //多创建了一个变量
   int val$x;
   
   //变为了有参构造器
   public Demo8$1(int x) {
      this.val$x = x;
   }

   @Override
   public void run() {
      System.out.println(this.val$x);
   }
}

1.对于需要使用到外部变量的内部类,在创建该内部类对象的时候会为它添加一个接收值的构造函数,这样进行new实例化时就能传入外部变量,从而让匿名内部类使用到外部变量。

2.需要注意的是:构造方法里这个传入的必须是final不可变的,毕竟要保持内外一致性。

4.类加载阶段

java虚拟机底层是用C++实现的,表层的java对象必然对应着下层的一个C++对象。

而klass是Java类在JVM的存在形式(c++代码)

img

4.1 加载

普通的Java类的字节码通过类加载器加载到方法区中后,在JVM中对应的是 instanceKlass 类(Java类,非数组)的实例,也就是说内部采用C++的instanceKlass描述表层的Java类

堆中的对象和方法区的instanceKlass能够互相通过记录的地址找到对方,

_java_mirror:Klass里 存储的java类的镜像,作用是把klass暴露给Java使用

image-20230227165855767

4.2 链接

4.2.1 验证

验证 类 是否符合JVM规范,安全性检查。

可以尝试用UE或Sublime Text支持二进制的编辑器修改一个class文件的魔数,此时运行java文件会报错

错误: 加载主类 cn.itcast.jvm.t5.HelloWorld 时出现 LinkageError
java.lang.ClassFormatError: Incompatible magic value 1667327589 in class file cn/itcast/jvm/t5/HelloWorld

4.2.2 准备

为静态变量分配空间,设置默认值

  • static 变量在jdk7之前是存在instanceKlass末尾,从jdk7开始,存储于_java_mirror末尾
  • static变量分配空间和赋值是两个不同的步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果static 变量是final的基本类型,那么编译阶段值就已经确定了,赋值在准备阶段完成
  • 如果static变量是final的引用类型,那么赋值同样是在初始化阶段完成
4.2.3 解析

符号引用转为直接引用,因为此时所有需要的类已经被加载到内存,有了具体的地址,这时就可以把符号引用变成直接引用

符号引用就是一个类中(当然不仅是类,还包括类的其他部分,比如方法,字段等),引入了其他的类,可是JVM并不知道引入的其他类在哪里,所以就用唯一符号来代替,等到类加载器去解析的时候,就把符号引用找到那个引用类的地址,这个地址也就是直接引用。

4.3 初始化

image-20230228095335789

4.4 类加载器

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[1],是虚拟机自身的一部分;

另外一种就是其他所有 的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

以jdk8为例

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

双亲委派模型:

  • 定义:各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载 器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用 组合(Composition)关系来复用父加载器的代码。
  • 工作流程:如果一个类加载器收到了类加载的请求,它首先会查看自己是否加载过这个类,即使没有加载过自己也不会去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
  • 注意:对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。(例如Object类,由于双亲委派模型,最终需要启动类加载器加载,这样就能保证所有对象的父类Object都是同一个类)

自定义类加载器:

  1. 创建一个继承ClassLoader的类MyClassLoader
  2. 重写findClass方法
  3. 创建MyClassLoader实例对象,调用对象的loadClass方法(该方法内部调用了findClass)
class MyClassLoader extends ClassLoader {
    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

5.运行期优化

字节码无法直接交给硬件执行,需要JVM将字节码转换为机器码,而转换的策略有两种:一种叫解释执行,一种叫编译执行,也称即时编译(JIT)

  • 解释执行:读一句执行一句,优点是启动快,但缺点就是整体的执行效率较低
  • 编译执行:先把读所有的语句,将其转换为机器码,再执行。优点就是执行快,有数量级的提升,缺点就是启动慢
优化一:逃逸分析

在jvm虚拟机中是两者混合出现,既有解释执行也有编译执行。首先是 解释执行,一条条执行所有字节码,如果JVM发现某个方法被频繁的调用会把该方法用编译执行的策略编译好存入Code Cache,下次执行的时候直接调用机器码,这种方法被称为 热点方法(逃逸分析)

优化二:方法内联

image-20230228161811105

优化三:反射优化

四. JVM的工作流程

一个java文件从编码到执行需要经过下面几个阶段

1、编译阶段:

首先.java经过javac编译成.class文件

2、类加载阶段:

然后.class文件经过类的加载器加载到JVM内存。

2.1 加载

2.2 链接

2.2.1 验证
2.2.2 准备
2.2.3 解析

2.3 初始化

3、解释阶段:

class字节码经过字节码解释器解释成系统可识别的指令码。

4、执行阶段:

系统再向硬件设备发送指令码执行操作。


  转载请注明: 流浪秃球计划 JVM

 上一篇
Mysql-日志篇 Mysql-日志篇
一.日志篇该篇所提到的日志用途实际上用于数据更新方面(insert、update、delete) undo log(回滚日志):Innodb存储引擎生成的日志,用于事务回滚 redo log(重做日志):Inoodb存储引擎生成的日志,用
2023-03-08
下一篇 
1.自动内存管理自动内存管理其实指的是系统的垃圾回收(Garbage Collection):由程序语言运行时系统回收动态内存。 交由系统回收的好处是: 避免手动内存管理,能够专注与实现业务逻辑 保证内存的使用的正确性和安全性,避免出现
2023-01-19 Touko
  目录