首页 > 其他分享 >【实战JVM】-01-JVM通识-字节码详解-类的声明周期-加载器

【实战JVM】-01-JVM通识-字节码详解-类的声明周期-加载器

时间:2024-05-25 08:56:33浏览次数:20  
标签:01 java 字节 通识 jar String JVM 2.2 加载

【实战JVM】-01-JVM通识-字节码详解-类的声明周期-加载器


1 初识JVM

1.1 什么是JVM

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

1.2 JVM的功能

在这里插入图片描述

1.2.1 即时编译

即时编译Just-In-Time 简称JIT进行性能的优化,最终到达接近C、C++的性能

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

将热点代码转换为机器码后保存至RAM,下次执行时直接从RAM中调用。

在这里插入图片描述

1.3 常见JVM

在这里插入图片描述

java -version

在这里插入图片描述

2 字节码文件详解

2.1 Java虚拟机的组成

在这里插入图片描述

2.2 字节码文件的组成

2.2.1 正确打开字节码文件

安装jclasslib

在这里插入图片描述
打开任意一个class文件
在这里插入图片描述

2.2.2 字节码组成

  • 基础信息(一般信息+接口):

    • 魔数、字节码对应的java版本号,访问标识(public、final等),以及这个类父类是哪个,以及实现了哪些接口
  • 常量池:

    • 保存了字符串常量、类、接口名、字段名。主要在字节码指令中使用。
    • 在这里插入图片描述
  • 字段:

    • 当前类或接口的字段信息,包括名字,描述符,访问标识。

    • private final static int a1=0
      
    • 在这里插入图片描述

  • 方法:

    • 当前类或接口的声明的方法信息字节码指令
    • 在这里插入图片描述
  • 属性:

    • 类的属性,比如源码的名字、内部类的列表等
    • 在这里插入图片描述

2.2.3 基础信息

在这里插入图片描述

在这里插入图片描述

2.2.3.1 魔数

在这里插入图片描述

在这里插入图片描述

打开二进制的png文件,就是以89504E47开始的

在这里插入图片描述

jpg则以FFD8FF开始

在这里插入图片描述

java字节码则是以CAFEBABE开始

在这里插入图片描述

2.2.3.1 主副版本号

在这里插入图片描述

52对应jdk1.8 61则对应jdk17

2.2.4 常量池

public class HelloWorld{
    public static final String a1= "a1a1a1";
    public static final String a2= "a1a1a1";
    public static void main(String[] args){

       System.out.println("Hello world!");
    }
}

查看编译后的class文件

在这里插入图片描述

两个都是常量,且指向同一块常量值索引,CONSTANT_String_info存放着cp_info #33,依旧是个索引

在这里插入图片描述

查看cp_info #33,这时候字面量才是a1a1a1

在这里插入图片描述

为什么要两级索引呢?这是因为在jvm中的运行时数据区域中有这方法区,方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

在这里插入图片描述

public class HelloWorld{
    public static final String a1= "abc";
    public static final String a2= "abc";
    public static final String abc= "abc";
    public static void main(String[] args){

       System.out.println("Hello world!");
    }
}

比如字段名和常量名都叫abc,但常量名是abc是String类型,而字段名是无类型的,但是都指向utf8格式的abc

在这里插入图片描述

2.2.5 方法

public static void main(String[] args){
    int i=0;
    i=i++;
    System.out.println(i);
}

对应字节码:

 0 iconst_0   			//操作数栈: [] -> [0],将常量值0压入操作数栈。
 1 istore_1   			//操作数栈: [0] -> [],将操作数栈顶的整数值(0)存入本地变量1。
 2 iload_1    			//操作数栈: [] -> [0],将本地变量1中的整数值(0)加载到操作数栈。
 3 iinc 1 by 1 			//本地变量1的值增加1。原值是0,现在变为1。
 6 istore_1				//操作数栈: [0] -> [],将操作数栈顶的整数值存入本地变量1。本地变量: [1]-> [0],
 7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>  //获取System.out的值并压入操作数栈。
10 iload_1				//操作数栈: [Ljava/io/PrintStream;] -> [Ljava/io/PrintStream;, 0],将本地变量1中的整数值加载到操作数栈。
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
14 return

i处于局部变量表的1号位

在这里插入图片描述

如果换成++i

public static void main(String[] args){
    int i=0;
    i=++i;
    System.out.println(i);
}
 0 iconst_0
 1 istore_1				//0放到本地变量表
 2 iinc 1 by 1			//本地变量表先自增,0->1
 5 iload_1				//将本地变量1中的整数值(1)加载到操作数栈。
 6 istore_1				//操作数栈: [1] -> [],将操作数栈顶的整数值存入本地变量1。本地变量: [1]-> [1],
 7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
14 return

这样就不会发生i=i++这种覆盖赋值的情况了。

作业:

int i=0,j=0,k=0;
i++;
j=j+1;
k+=1;

i和k一样快,都是把0从操作数栈中放入本地变量中直接操作本地变量自增。

j最慢,从本地变量表中加载到操作数栈中,再加载1,再相加,再放入本地变量表。

2.3 linux中打开字节码文件

在这里插入图片描述

javap -v 字节码文件名称

2.4 字节码常用工具 Arthas

2.4.1 安装Arthas

在这里插入图片描述

启动arthas

在这里插入图片描述

先启动项目再分析

public class ArthasDemo {
    public static void main(String[] args) {
        while (true) {
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在arthas工作目录中启动

java -Dfile.encoding=UTF-8 -jar arthas-boot.jar

-Dfile.encoding=UTF-8是让arthas以utf8格式启动,这样不会乱码
在这里插入图片描述

成功捕获到ArthasDemo,选择3回车进入arthas内部,他还自动下载了arthas3.7.2版本

3

在这里插入图片描述

2.4.2 Arthas功能

在这里插入图片描述

2.4.2.1 获取系统实时面板-dashboard

在这里插入图片描述

我们设置每隔两秒刷新一次,刷新3次

dashboard -i 2000 -n 3

在这里插入图片描述

只显示1次

在这里插入图片描述

2.4.2.2 加载特定类的字节码-dump

在这里插入图片描述

dump -d D:/File/StudyJavaFile/JavaStudy/JVM/low/day01/resource/ com.sjb.arthas.ArthasDemo

在这里插入图片描述

在这里插入图片描述

这样就获取了运行时的java文件的字节码信息

2.4.2.3 反编译已加载类的源码-jad
jad com.sjb.arthas.ArthasDemo

在这里插入图片描述

和我们的源码几乎一致

案例

在这里插入图片描述

启动springboot-classfile后

在这里插入图片描述

public UserVO user(@PathVariable("type") Integer type,@PathVariable("id") Integer id){
    //前边有一大堆逻辑,巴拉巴拉
    if(type==UserType.REGULAR.getType()){
        return new UserVO(id,"普通用户无权限查看");
    }
    return new UserVO(id,"这是尊贵的收费用户才能看的秘密!");
}

不能用==来判断类型,需要equals

在这里插入图片描述

在这里插入图片描述

即使是普通用户,但是因为用的==判断的类型,也能进入vip用户

使用jad查看

jad com.itheima.springbootclassfile.controller.UserController

在这里插入图片描述

定位到问题信息,以供以后热更新

2.4.2.4 查看JVM已加载的类信息-sc

在这里插入图片描述

sc -d 类名(java.lang.String)

查看当前类的类加载器,如果为空,则为启动类加载器。

3 类的生命周期

3.1 生命周期的概述

在这里插入图片描述

3.2 加载阶段

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.2.1 查看内存中的对象

推荐使用JDK自带的hsdb工具查看Java虚拟机内部的内存信息。工具位于JDK安装目录下的lib文件夹的sa-jdi.jar中。

启动jvm项目的HsdbDemo

在这里插入图片描述

使用jps展示当前所有的java进程及id

jps

在这里插入图片描述

启动命令

D:\Software\software_with_code\idea\jdk\jdk1.8.0_381\lib>java -cp sa-jdi.jar sun.jvm.hotspot.HSDB

在这里插入图片描述

输入我们要找的类的编号HsdbDemo:18648

在这里插入图片描述

直接搜索HsdbDemo,因为我们只new了一次,自然只有一个对象。

在这里插入图片描述

在这里插入图片描述

一句话概括:类加载器将类的信息加载到内存中,java虚拟机在方法区和堆区中各分配一个对象去保存这个信息,而我们需要操作的则是堆区中的对象,jdk8之后静态字段也是存在堆中的

3.3 连接阶段

在这里插入图片描述

3.3.1 验证阶段

3.3.1.1 验证是否符合jvm规范

在这里插入图片描述

3.3.1.2 元信息验证

在这里插入图片描述

3.3.1.3 验证语义

在这里插入图片描述

3.3.1.4 符号引用验证

判断是否访问了其他类的private方法。

3.3.2 准备阶段

在这里插入图片描述
value在准备阶段分配的值是默认值0,而赋值为1是初始化阶段做的事

在这里插入图片描述

但是也有例外,如果是final修饰的基本数据类型,会在准备阶段直接将代码中的值进行赋值。

public static final int value=1;

3.3.3 解析阶段

在这里插入图片描述

直接引用不在使用编号,而是直接使用内存中的地址进行访问具体的数据。

3.4 初始化阶段

  • 初始化阶段会执行静态代码中的代码,并为静态变量赋值
  • 初始化阶段会执行字节码文件中的clinit部分的字节码指令。

在这里插入图片描述

如果颠倒一下顺序,那么输出则是1

在这里插入图片描述

因为静态变量是在连接阶段准备阶段完成默认初始化。然后再赋为2,再赋为1。

在这里插入图片描述

3.4.1 笔试题

(1)

在这里插入图片描述

构造代码块先于构造方法前执行

3.4.2 特殊情况

在这里插入图片描述

  • 直接访问父类的静态变量,不会触发子类的初始化。

  • 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。
    在这里插入图片描述

  • 数组的创建不会导致数组中元素的类进行初始化。

    • 是因为创建数组时是创建的数组的对象,而不是数组中元素的对象。所以数组中元素的类不会进行初始化。
  • 如果一个变量用final修饰,并且其中的内容要执行指令才能得出结果,那么会在clinit方法中进行初始化。

3.5 总结

在这里插入图片描述

4 类的加载器

类加载器是jvm提供给应用程序去实现获取类和接口字节码数据的技术。

负责在类加载过程中的字节码获取并且加载到内存这一部分。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层的方法将byte[]转换成方法区和堆中的数据。

4.1 类加载器的分类

在这里插入图片描述

俩下Shift是搜索
Ctrl+Alt+类是找当前类的所有实现

4.1.1 JDK8之前的分类

在这里插入图片描述

  • 引导类加载器 Bootstrap,加载属于JVM的一部分,由C++代码实现,负责加载<JAVA_HOME\>\jre\lib路径下的核心类库
  • 扩展类加载器 ExtClassLoader,扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。
  • 应用程序类加载器 AppClassLoader,应用程序类加载器负责加载 classpath环境变量所指定的类库,是用户自定义类的默认类加载器。

4.1.2 使用Arthas查看类加载器-classloader

在这里插入图片描述

启动Hsdbdemo后打开arthas,在arthas工作目录中启动

java -jar arthas-boot.jar

进入Hsdbdemo

classloader

在这里插入图片描述

4.1.3 C++启动类加载器BootstrapClassLoader

负责加载<JAVA_HOME\>\jre\lib路径下的核心类库

通过类名.class.getClassLoader来获取当前类的类加载器。

在这里插入图片描述

添加java虚拟机参数D:/jvm/jar/classloader-test.jar是jar包地址

-Xbootclasspath/a:D:/jvm/jar/classloader-test.jar

4.1.4 Java中默认类加载器

在这里插入图片描述

4.1.4.1 扩展类加载器ExtClassLoader

扩展类加载器 ExtClassLoader,扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

在这里插入图片描述

添加java虚拟机参数D:/jvm/jar/classloader-test.jar是jar包地址。

不仅需要jar包的地址,还需要原来ext的地址D:\Software\software_with_code\idea\jdk\jdk1.8.0_381\jre\lib\ext

-Djava.ext.dirs="D:\Software\software_with_code\idea\jdk\jdk1.8.0_381\jre\lib\ext;D:/jvm/jar/classloader-test.jar"

在windows中;是追加,linux和mac中:是追加,尽量用双引号引起俩,以免因为特殊字符报错。

4.1.4.2 应用程序类加载器 AppClassLoader

应用程序类加载器 AppClassLoader,应用程序类加载器负责加载 classpath环境变量所指定的类库,是用户自定义类的默认类加载器。既可以加载当前项目中创建的类,也可以加载maven依赖中包含的类。

4.1.4.3 Arthas-classloader高级用法
classloader -l

在这里插入图片描述

查看当前所有的类加载器以及其哈希值

classloader -c hash值

在这里插入图片描述

查看当前查询的类加载器加载的所有jar包

在这里插入图片描述

4.2 类加载器的双亲委派机制

jvm中有多个类加载器,双亲委派机制的核心就是解决一个类到底由谁加载的问题。

双亲委派机制的作用

  1. 保证类加载的安全性

    通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如Java.lang.String,确保核心类库的完整性和安全性。

  2. 避免重复加载

    双亲委派机制可以避免同一个类被多次加载

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.2.1 Arthas查看类加载器父子关系

classloader -t

在这里插入图片描述

4.2.2 面试

在这里插入图片描述

4.3 打破双亲委派机制

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.3.1 自定义类加载器

在这里插入图片描述

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    if(name.startsWith("java.")){
        return super.loadClass(name);
    }
    byte[] data = loadClassData(name);
    return defineClass(name, data, 0, data.length);
}

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
    BreakClassLoader1 classLoader1 = new BreakClassLoader1();
    classLoader1.setBasePath("D:\\lib\\");
    Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");
    BreakClassLoader1 classLoader2 = new BreakClassLoader1();
    classLoader2.setBasePath("D:\\lib\\");
    Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");
    System.out.println(clazz1 == clazz2);
    Thread.currentThread().setContextClassLoader(classLoader1);
    System.out.println(Thread.currentThread().getContextClassLoader());
    System.in.read();
 }

重写loadClass方法,删除双亲委派机制,如果是java开头的jar包,就交给原先父类的loadClass,如果是自定义的,就自己直接加载。

自定义类加载器没指定双亲的话,默认双亲为应用程序类加载器

在这里插入图片描述

4.3.1.1 Arthas展示类的详细信息
sc -d com.itheima.my.A

在这里插入图片描述

因为刚刚用两个不同的类加载器加载com.itheima.my.A,自然得到两个不同的对象

4.3.1.2 正确的自定义类加载器

在这里插入图片描述

4.3.2 线程上下文类加载器

在这里插入图片描述

DriverManager类位于rt.jar包中,由启动类加载器加载。

在这里插入图片描述

4.3.2.1 SPI机制

SPI全程Service Provider Interface,是JDK内置的一种服务提供发现的机制

需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件java.sql.Driver的接口,在这个文件中写入接口的实现类的全限定名com.mysql.cj.jdbc.Driver。

在这里插入图片描述

ServiceLoader这个类的源码如下:

public final class ServiceLoader<S> implements Iterable<S> {
    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";
    // 被加载的类或接口
    private final Class<S> service;
    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;
    // 上下文对象
    private final AccessControlContext acc;
    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;
    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;
        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }
        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射加载类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //实例化
                S p = service.cast(c.newInstance());
                //放进缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在线程中使用

Thread.currentThread().getContextClassLoader()

默认获得的是应用程序类加载器

4.3.2.2 总结

在这里插入图片描述

在这里插入图片描述

所以说,打破双亲委派机制的唯一方法就是重写loadClass或者findClass方法

4.3.3 热部署

在这里插入图片描述

  1. 启动SpringbootClassfileApplication后,使用arthas进入,反编译出UserController

    java -Dfile.encoding=UTF-8 -jar arthas-boot.jar
    
    jad --source-only com.itheima.springbootclassfile.controller.UserController > "D:\Code\JavaCode\JVM\hot-replace\UserController.java"
    

在这里插入图片描述

修改为

if (type.equals(UserType.REGULAR.getType())) {
  1. 编译成字节码文件,如果直接编译,则会因为找不到类加载器而报错,所以需要先找到UserController.java的类加载器,获取其哈希值。

    sc -d com.itheima.springbootclassfile.controller.UserController
    

    在这里插入图片描述

  2. mc -c指定类加载器的哈希码 -d指定输出目录

    mc -c 18b4aac2 "D:\Code\JavaCode\JVM\hot-replace\UserController.java" -d "D:\Code\JavaCode\JVM\hot-replace"
    

    在这里插入图片描述

  3. 通过retransform

    retransform "D:\Code\JavaCode\JVM\hot-replace\com\itheima\springbootclassfile\controller\UserController.class"
    

    在这里插入图片描述

  4. 用jad查看热部署是否完成

    在这里插入图片描述

  5. 发送http请求

    在这里插入图片描述

    说明热部署已经完成

4.3.3.1 热更新注意事项

在这里插入图片描述

4.4 JDK8之后的类加载器

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

应用类加载器和扩展类加载器都是继承关系从URLClassLoader变为BuiltinClassLoader,没有太大的区别。

标签:01,java,字节,通识,jar,String,JVM,2.2,加载
From: https://blog.csdn.net/qq_45400167/article/details/139162540

相关文章

  • P8655 [蓝桥杯 2017 国 B] 发现环
    原题链接题解有点像拓扑排序拓扑排序怎么做来着?首先找老祖节点对不对?老祖节点有什么特性?入度为零而在无向图中,我们把叶子节点看成老祖节点,它们有什么特性?连接的边只有一条code#include<bits/stdc++.h>usingnamespacestd;vector<int>G[100005];intcon[100005]={......
  • P5662 [CSP-J2019] 纪念品
    原题链接题解定义\(dp[i]\)为今天有\(i\)元钱花时,明天卖能纯赚多少钱(这里有一个递归的思想,不需要考虑\(dp[k-a[i][j]]\)能否买得起今天的产品)如果\(dp[i-1]=k\)那么\(dp[i]\geqk\),所以存在一个\(i\)使得钱全部花完然后赚\(k\)元code#include<bits/stdc++.h>u......
  • JVM面试指南:掌握这10个问题,大厂Offer轻松拿
    引言:    随着科技巨头不断寻求具备深厚技术功底的开发人才,准备好应对他们的面试变得非常关键。Java虚拟机(JVM)是Java平台的核心组成之一,深入理解JVM的工作原理、性能优化和垃圾回收机制,对于每一个希望在顶尖科技公司站稳脚跟的Java开发者来说,是必不可少的。今......
  • 两个人不约而同的说相同的话做相同的事,是什么情况 ... 2个回答 - 回答时间: 2018年1
    两个人不约而同的说相同的话做相同的事,是什么情况...2个回答-回答时间:2018年1月25日最佳答案: 心有灵犀一点通。......
  • P5531 [CCO2019] Human Error 题解
    可能是一个比较劣的做法。但复杂度是对的。思路我们容易发现状态数非常的稀少。一个比较宽松的上限时\(3^{13}\)种状态由于每个点每走一步会吃掉一个棋子。所以实际的状态是远远达不到这个上限。那么我们可以直接设\(dp_{i,0/1,0/1}\)为在\(i\)状态下,目前是Justin......
  • 代码随想录算法训练营第十五天 | 层序遍历 、226.翻转二叉树、101.对称二叉树
    层序遍历题目链接:学会二叉树的层序遍历,可以一口气打完以下十题:102.二叉树的层序遍历107.二叉树的层次遍历II199.二叉树的右视图637.二叉树的层平均值429.N叉树的层序遍历515.在每个树行中找最大值116.填充每个节点的下一个右侧节点指针117.填充每个节点的下一个右侧节点......
  • 前端学习-Dart官方文档学习-001-基础表达式
    学习链接变量创建并初始化变量//不指定变量类型,自动推断为String;可通过指定类型改变varname='Bob';//一个对象不受限于单一类型时,使用Object或dynamic(谨慎使用)Objectname='Bob';dynamicname='Bob';//直接指定类型Stringname='Bob';空安全空安全能够......
  • SCAU 18705 01背包问题
    18705 01背包问题时间限制:1000MS 代码长度限制:10KB题型:编程题   语言:不限定Description有一个容积为M的背包和N件物品。第i件物品的体积W[i],价值是C[i]。求解将哪些物品装入背包可使价值总和最大。每种物品只有一件,可以选择放或者不放入背包。输入格式第一......
  • jvm调优的案例-自编
    一、优化目标通常来说,我们的JVM参数配置大多还是会遵循JVM官方的建议,例如:-XX:NewRatio=2,年轻代:老年代=1:2-XX:SurvivorRatio=8,eden:survivor=8:1堆内存设置为物理内存的3/4左右JVM有哪些核心指标?合理范围应该是多少?jvm.gc.time:每分钟的GC耗时在1s以内,500ms以内尤......
  • 01-Python 图片转字符画
    fromPILimportImage"""将图片转换为字符画图片转字符画是将一张图片转换成由字符组成的图像,通常用于在命令行界面或者文本编辑器中显示。这个过程主要包括以下几个步骤:-读取图片文件-将图片转换为灰度图像-调整图片的尺寸以适应字符画的宽度......