IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 2021-11-13 java内存分析 -> 正文阅读

[Java知识库]2021-11-13 java内存分析

java内存分析

在这里插入图片描述

简述类的加载过程

Java中的字节码(后续补充字节码相关内容):英文名为 bytecode , 是Java代码编译后的中间代码格式。JVM需要读取并解析字节码才能执行相应的任务。

Java字节码是JVM的指令集。JVM加载字节码格式的class文件,校验之后通过JIT编译器转换为本地机为代码执行。

运行java程序的第一步就是加载class文件/或输入流里面包含的字节码。我们用"类加载 (Class Loading)"来表 示:将class/interface名称映射为Class对象的一整个过程。这个过程还可以划分为更 具体的阶段: 加载,链接和初始化(loading, linking and initializing)。

类的生命周期

在这里插入图片描述
其中前五个部分(加载,验证,准备,解析,初始化)统称为类加载:

  1. 加载:

    • 加载阶段也可以称为**“装载”**阶段。这个阶段主要的操作是:根据明确知道的class完全限定名,来获取二进制classfile格式的字节流,简单点说就是找到文件系统中/jar包中/或存在于任何地方的"class文件"。如果找不到二进制表示形式,则会抛出NoClassDefFound错误。装载阶段并不会检查 classfile 的语法和格式。类加载的整个过程主要由JVM和Java 的类加载系统共同完成,当然具体到loading阶段则是由JVM与具体的某一个类加载器(java.lang.classLoader)协作完成的。
  2. 校验:

    • 链接过程的第一个阶段是 校验,确保class文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。
    • 校验过程检查classfile的语义,判断常量池中的符号,并执行类型检查,主要目的是判断字节码的合法性,比如magic number(字节码中会提到),对版本号进行验证。这些检查过程中可能会抛出VerifyError,ClassFormatError或UnsupportedClassVersionError。
    • 因为classfile的验证属是链接阶段的一部分,所以这个过程中可能需要加载其他类,在某个类的加载过程中,JVM必须加载其所有的超类和接口。如果类层次结构有问题(例如,该类是自己的超类或接口,死循环了),则JVM将抛出ClassCircularityError。而如果实现的接口并不是一个interface,或者声明的超类是一个interface也会抛出IncompatibleClassChangeError。
  3. 准备:

    • 然后进入准备阶段,这个阶段将会创建静态字段,并将其初始化为标准默认值(比如null或者0值),并分配方法表,即在方法区中分配这些变量所使用的内存空间。请注意,准备阶段并未执行任何Java代码
    • 例如:public static int i = 1;在准备阶段 i 的值会被初始化为0,后面在类初始化阶段才会执行赋值为1,但是下面如果使用final作为静态常量,某些JVM的行为就不一样了:public static final int i = 1;对应常量i,在准备阶段就会被赋值1。
  4. 解析:

    • 然后进入可选的解析符号引用阶段,也就是解析常量池,主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析。简单的来说就是我们编写的代码中,当一个变量引用某个对象的时候,这个引用在.class文件中是以符号引用来存储的(相当于做了一个索引记录)。在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。加载一个class时, 需要加载所有的super类和super接口。
  5. 初始化:

    • JVM规范明确规定,必须在类的"首次主动使用"时才能执行类初始化
    • 初始化的过程包括执行:类构造器方法、static静态变量赋值语句、static静态代码块
    • 如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化。所以其实在java中初始化一个类,那么必然先初始化过java.lang.Object类,因为所有的java类都继承自java.lang.Object。
    • ps: 为了提高性能,HotSpot JVM通常要等到类初始化时才去装载和链接类。因此,如果A类引用了B类,那么加载A类并不一定会去加载B类(除非需要进行验证)。主动对B类执行第一条指令时才会导致B类的初始化,这就需要先完成对B类的装载和链接

类的加载时机

了解了类的加载过程,我们再看看类的初始化何时会被触发呢?JVM规范枚举了下述多种触发情况:

显示触发:

  • 当虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类;
  • 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类,就是new一个类的时候要初始化;
  • 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  • 当遇到访问静态字段的指令时,初始化该静态字段所在的类;

隐式触发:

  • 子类的初始化会触发父类的初始化;
  • 如果一个接口定义了default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  • 使用反射API对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
  • 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。

同时以下几种情况不会执行类初始化

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化,Hello.class不会让Hello类初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName(“jvm.Hello”)默认会加载Hello类。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是不初始化)

诸如 Class.forName(), ClassLoader.loadClass() 等Java API, 反射API, 以及 JNI_FindClass 都可以启动类加载。JVM本身也会进行类加载。比如在JVM启动时加载核心类,java.lang.Object, java.lang.Thread 等等。

类加载器机制

类加载过程可以描述为"通过一个类的全限定名a.b.c.XXClass来获取描述此类的Class 对象",这个过程由"类加载器(ClassLoader)"来完成。这样的好处在于,子类加载器可以复用父加载器加载的类。系统自带的类加载器分为三种:

  • 启动类加载器(BootstrapClassLoader): 它用来加载Java的核心类,是用原生C++代码来实现的,并不继承自java.lang.ClassLoader(负责加载JDK中jre/lib/rt.jar里所有的class)。它可以看做是JVM自带的,我们再代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它,如果打印出来就是个null。举例来说,java.lang.String是由启动类加载器加载的,所以String.class.getClassLoader()就会返回null。但是后面可以看到可以通过命令行参数影响它加载什么。
  • 扩展类加载器(ExtClassLoader): 它负责加载JRE的扩展目录,lib/ext或者启动java程序时在java启动命令中添加参数: -Djava.ext.dirs额外的添加指定JDK使用的类或jar包的扩展路径,代码里直接获取它的父类加载器为null(因为无法拿到启动类加载器。ps:这里的’父类加载器’并不是java类之间的继承关系,而是’双亲委托’机制中的层级关系,扩展类加载器和应用类加载器都继承自URLClassLoader类,而扩展类加载器的父类加载器是BootstrapClassLoader,应用类加载器的父类加载器是扩展类加载器);多个应用程序共性依赖的jar就可以放在扩展类加载器中,扩展类加载器是对启动类加载器和应用类加载器的一个补充。
  • 应用类加载器(AppClassLoader): 它负责在JVM启动时加载来自Java命令的java --classpath或者-- cp选项、java.class.path系统属性指定的jar包和类路径。在应用程序代码里可以通过ClassLoader的静态方法getSystemClassLoader()来获取应用类加载器。如果没有特别指定,即在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。

一般启动类加载器是由JVM内部实现的,在Java的API里无法拿到,但是我们可以侧面看到和影响它(后面的内容会演示)。后2种类加载器在Oracle Hotspot JVM里,都是在中sun.misc.Launcher定义的,扩展类加载器和应用类加载器一般都继承自URLClassLoader类,这个类也默认实现了从各种不同来源加载class字节码转换成Class的方法。

加载器类的继承关系:

在这里插入图片描述
此外还可以自定义类加载器。如果用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器,但是直接从ExClassLoader里拿不到它的引用,同样会返回null。

双亲委托层级关系:

在这里插入图片描述

添加加载路径的几种方式,总结

1、放到JDK的lib/ext下,或者启动时指定-Djava.ext.dirs参数

2、java -cp/classpath或者class文件放到当前路径

3、自定义ClassLoader加载

4、拿到当前类的ClassLoader,反射调用addUrl方法添加Jar或路径(JDK9无效)

需要特别注意的是:JDK9以后,应用类加载器,扩展类加载器,跟URL类加载器是平级关系了,没有继承关系了,所以就没办法把应用类加载器和扩展类加载器转换成URL类加载器;在JDK9之后,我们可以直接在class.forNmae后面的参数上加一个new URLClassLoader,把我们需要添加的jar包或者文件夹作为URLClassLoader的参数指定进来

实例1:打印三种加载器加载的所有路径:

package com.river.classloader;

import sun.misc.Launcher;

import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;


/**
 * 测试类加载器:
 */
public class PrintClassLoaderTest {
    public static void main(String[] args) {
        /*
            查看启动类加载器加载的路径:
            启动类加载器:启动类加载器虽然我们在代码的层面上没办法直接拿到,
            但是像Open JDK或者Oracle JDK,我们可以通过一个静态方法去拿到它的ClassPath,
            从而拿到它内部所有的加载的路径
         */
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(" =====> " + url.toExternalForm());
        }


        // 扩展类加载器
        ClassLoader extClassLoader = PrintClassLoaderTest.class.getClassLoader().getParent();
        printClassLoader("扩展类加载器", extClassLoader);
        // 应用类加载器:方式1
        ClassLoader appClassLoader = PrintClassLoaderTest.class.getClassLoader();
        // 应用类加载器:方式2
//        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();  // 也可以通过这个方式获得应用类加载器
        printClassLoader("应用类加载器", appClassLoader);
        // 查看应用类加载器加载路径 方式2:
        System.out.println("应用类加载器: " + System.getProperty("java.class.path"));
    }

    /**
     * 打印输出classLoader的名字和其加载的所有路径
     * @param name
     * @param classLoader
     */
    private static void printClassLoader(String name, ClassLoader classLoader) {
        System.out.println();
        if (null != classLoader) {
            System.out.println(name + " ClassLoader -> " + classLoader.toString());
            printClassLoaderUrl(classLoader);
        } else {
            System.out.println(name + " ClassLoder -> null");
        }
    }

    /**
     * 打印classLoader加载的所有路径
     * @param classLoader
     */
    private static void printClassLoaderUrl(ClassLoader classLoader) {
        Object ucp = insightField(classLoader, "ucp");// 获取对应classLoader中的"ucp"字段,"ucp"的字段类型为:URLClassPath
        Object path = insightField(ucp, "path");// 获取URLClassPath中隐藏了所有加载路径的列表
        List paths = (List) path;
        for (Object p : paths) {
            System.out.println("======> " + p.toString());
        }
    }

    /**
     * 通过fName,获取obj中对应的字段
     * @param obj
     * @param fName
     * @return
     */
    private static Object insightField(Object obj, String fName) {
        Field f = null;
        try {
            if(obj instanceof  URLClassLoader) {
                /*
                    1.getDeclaredFiled 获取类本身的属性成员(包括私有、共有、保护)
                    2.getField 获取类(及其父类可以自己测试) public属性成员
                    因为扩展类加载器和应用类加载器都继承自URLClassLoader,
                    所以可以把他们当作URLClassLoader,获取URLClassLoader中的'URLClassPath ucp'字段。
                    需要特别注意的是:JDK9以后,应用类加载器,扩展类加载器,跟URL类加载器是平级关系了,
                    没有继承关系了,所以就没办法把应用类加载器和扩展类加载器转换成URL类加载器
                 */
                f = URLClassLoader.class.getDeclaredField(fName);
            } else {
                f = obj.getClass().getDeclaredField(fName);
            }
            f.setAccessible(true); // 设置允许外部访问私有属性
            return f.get(obj);  // 方法:get(Object obj) 返回指定对象obj上此 Field表示的字段的值
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

实例2: 自定义类加载器

package com.river.classloader;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
 * 自定义类加载器
 * 可以用来做类的动态加载,class加密
 */
public class CustomizeClassLoader extends ClassLoader {

    public CustomizeClassLoader() {
    }

    public static void main(String[] args) throws Exception {
        Class helloClass = (new CustomizeClassLoader()).findClass("Hello"); // 装载class
        Object o = helloClass.newInstance(); // 初始化
        Method hello = helloClass.getMethod("hello");
        hello.invoke(o);
    }

    // 重写加载方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 将一个.class文件用BASE64加密成加密串
        String helloBase64 = "NQFFQf///8v/4/X/+f/x9v/w/+/3/+71/+3/7Pj/6/j/6v7/+cOWkZaLwf7//NfWqf7/+7yQm5r+//CzlpGasYqSnZqNq56dk5r+//qXmpOTkP7/9ayQio2cmrmWk5r+//W3mpOTkNGVnome8//4//f4/+nz/+j/5/7/7Leak5OQ09+ck56MjLOQnpuajd74/+bz/+X/5P7/+reak5OQ/v/vlZ6JntCTnpGY0LCdlZqci/7/75WeiZ7Qk56RmNCshoyLmpL+//yQiov+/+qzlZ6JntCWkNCvjZaRi6yLjZqeksT+/+yVnome0JaQ0K+NlpGLrIuNmp6S/v/4j42WkYuTkf7/6tezlZ6JntCTnpGY0KyLjZaRmMTWqf/e//r/+f///f/+//j/9//+//bi//7//vrVSP/+Tv7/9fn//v7//v/0//f//v/22v/9//72Tf/97fxJ//tO/v/19f/9+//3//r//v/z/f/y";
        // 读取后进行解密
        byte[] bytes = this.decode(helloBase64);

        for(int i = 0; i < bytes.length; ++i) {
            byte temp = (byte)(~bytes[i]);
            bytes[i] = temp;
        }

        Object var8 = null;

        try {
            new String(bytes, "UTF-8");
        } catch (UnsupportedEncodingException var7) {
            var7.printStackTrace();
        }

        return this.defineClass(name, bytes, 0, bytes.length); // 装载class
    }

    public byte[] decode(String base64) {
        return Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
    }
}
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-14 21:29:54  更:2021-11-14 21:30:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 2:18:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码