JVM-类加载器、对象创建

2023-11-05

目录

类加载的各个阶段

一、加载

 二、连接

三、初始化

类加载器

Bootstrap ClassLoader:

Extension ClassLoader 

 双亲委派

自定义类加载器

即时编译器优化:

逃逸分析

方法内联

对象创建

1.类加载检查

2.分配内存

3.初始化零值

4.设置对象头

5.执行方法


类加载的各个阶段

一、加载

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

  • _java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法

如果这个类还有父类没有加载,先加载父类

加载和链接可能是交替运行的

当类加载的时候,将类的字节码文件加载到元空间中,是一个用C++描述的InstanceKlass。同时也在堆中存储一个类对象,比如说如果类是Person类i,那么就会在堆中存储一个Person.class的类对象,不管有多少实例对象,类对象只有一个,类对象作为java虚拟机与操作系统之间的一个桥梁,在实体对象的对象头中有8个字节是用来存储person.class这个类对象的地址的,而在类对象Person.class中也持有元空间咋哄InstanceKlass的地址,而InstanceKlass中也持有类对象的地址,当调用类的某些 特定方法例如getmethod、getfield等方法的时候,就会通过类对象找到InstanceKlass,然后执行方法。此外类的静态变量也只有一份,在JDK1.6包括之前,方法区位于堆中,那么静态变量是存放在IntanceKlass后面,而在JDK1.8之后,静态变量存放在类对象Person.class后。

 二、连接

1、验证

验证类是否符合 JVM规范,安全性检查。
UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行就会出现以下报错现象

2、准备

  • static 变量分配空间,设置默认值
  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶 段完成
  • 如果 static 变量是 final的,但属于引用类型,那么赋值也会在初始化阶段完成

3、解析

将常量池中的符号引用解析为直接引用
一个未解析的类在常量池中会被标明未unresolvedClass,这种情况下是找不到他在内存中的地址的,也就是符号引用

当解析就不会被标记为unresolvedClass,并且能够找到他在内存中的地址

 

三、初始化

初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全
  • 发生的时机
  1. 概括得说,类初始化是【懒惰的】
  2. main 方法所在的类,总会被首先初始化
  3. 首次访问这个类的静态变量或静态方法时
  4. 子类初始化,如果父类还没初始化,会引发
  5. 子类访问父类的静态变量,只会触发父类的初始化
  6. Class.forName
  7. new 会导致初始化
  • 不会导致类初始化的情况
  1. 访问类的 static final静态常量(基本类型和字符串)不会触发初始化
  2. 类对象.class 不会触发初始化
  3. 创建该类的数组不会触发初始化
  4. 类加载器的 loadClass 方法
  5. Class.forName 的参数 2 false

类加载器


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

名称
加载哪的类
说明
Bootstrap ClassLoader
JAVA_HOME/jre/lib
无法直接访,c++层次的,显示为 null
Extension ClassLoader
JAVA_HOME/jre/lib/ext
上级为 Bootstrap null
Application ClassLoader
classpath
上级为 Extension
自定义类加载器
自定义
上级为 Application

Bootstrap ClassLoader:

先介绍一个jvm指令:-Xbootclasspath 表示定义启动类加载的路径

  • -Xbootclasspath:<new bootclasspath>  表示覆盖之前的路径(JAVA_HOME/jre/lib)
  • -Xbootclasspath/a:<new bootclasspath> 表示追加一个新的路路径在后面
  • -Xbootclasspath/p:<new bootclasspath> 表示在前面追加一个新路径

  • 1、先创建一个类F
public class F {
        static {
            System.out.println("我被初始化了");
        }
}
  • 2、然后在main方法中加载类
public class asdasd {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> f = Class.forName("F");
        ClassLoader classLoader = f.getClassLoader();
          System.out.println("classLoader:"+classLoader);
    }

}
  • 3、接着编译并输出查看classLoader

  • 4、 由此看到,启动类加载器并不能访问,是一个null值

Extension ClassLoader 

首先我自己创建了一个F类

 然后这时候打包这个类为jar包,放入jdk/jre/lib/ext目录下

然后开始main方法,加载F类,可以看到是由拓展类类加载器进行加载的

 双亲委派

源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 2、有上级的话,委派上级 loadClass
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                  // 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
             
            }

            if (c == null) {
                 long t1 = System.nanoTime();
            // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
              
                c = findClass(name);

                // 5、记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

 过程

1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级sun.misc.Launcher$ExtClassLoader.loadClass()
3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader
查找
5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 fifindClass 方法,是在
JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader
// 2
7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 fifindClass 方法,在
classpath 下查找,找到了
优点:

防止java核心api被串改,避免类重复创建

自定义类加载器


使用场景

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

步骤

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

自定义类加载器代码:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        //指定类加载的路径
        String path = "d:\\" + name + ".class";
        try {
            //创建一个字节输出流
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            //将类文件拷贝为字节输出流
            Files.copy(Paths.get(path), os);
            //得到一个字节数组
            byte[] bytes = os.toByteArray();
            //将字节数组转换成class文件
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }

    }
}

调用


       MyClassLoader myClassLoader = new MyClassLoader();
        Class<?> f = myClassLoader.loadClass("F");
     

即时编译器优化:

对于java中的每一条指令,都会先通过虚拟机进行解释成机器码,然后再进行执行。但是如果对一些重复了很多次很多次的热点数据指令,一直在做重复的解释操作,无疑会耗费很多时间

JIT编译的交互过程:具体请参考

当一个方法被调用时,会先检查该方法是否存在被JIT 编译过的版本:如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法计数器的阈值。若超过了,则将会向即时编译器提交一个该方法的代码编译请求;

如果不做任何设置:执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会被系统自动改写成新的,下一次调用该方法时就会使用已编译的版本,整个JIT 编译的交互过程
 

具体包含以下几种优化

逃逸分析

分析对象的动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递给其他方法,称为方法逃逸。甚至还有可能被外部线程访问到,比如赋值给类变量或可以在其他线程中访问到的实例变量,称为线程逃逸。

优化方案

如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,就可以为这个变量进行一些高效的优化:如:栈上分配、同步消除、标量替换等。

  • 栈上分配

如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将是一个不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁;

  • 同步消除

线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,那这个变量的读写肯定不会有竞争,对这个变量实施的同步措施就可以消除掉;

  • 标量替换
  1. 标量定义:它是指一个数据已经无法再分解成更小的数据来表示了,java虚拟机中的原始数据类型都不能再进一步分解,他们就可以称为标量;
  2. 聚合量定义:如果一个数据可以继续分解,它被称为聚合量;
  3. 标量替换:如果把一个java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换;那程序真正执行的时候,将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

例如:

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

private static void alloc() {
   Point point = new Point(1,2);
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
    private int x;
    private int y;
}

point方法并没有发生方法逃逸,那么就可以优化为:

private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}

方法内联

对于一个方法的执行,会在线程的栈内存中压入栈帧内存,当方法执行完毕后又会将栈帧内存弹出,如果一个方法非常的简单,但是又重复多次执行(热点数据),也就是多次对栈帧的压栈和弹出,虚拟机认为有优化的空间,那么就直接将方法的内容直接放到调用的位置上,从而避免的压栈出栈的开销

例如:

private int add4(int x1, int x2, int x3, int x4) {  
        return add2(x1, x2) + add2(x3, x4);  
    }  

    private int add2(int x1, int x2) {  
        return x1 + x2;  
    }  

优化为:

private int add4(int x1, int x2, int x3, int x4) {  
        return x1 + x2 + x3 + x4;  
    }  

当启用方法内联的时候,如果在一个方法中多次使用一个成员变量,那么方法在读取成员变量的时候,会用一个局部变量来存储成员变量,这样再以后使用这个成员变量的时候只需要再本地查找局部变量就可以

对象创建

对象创建主流程

1.类加载检查

  虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等

2.分配内存

  在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。

这个步骤有两个问题:

  • 1.如何划分内存。
  • 2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

划分内存的方法:

  • “指针碰撞”(Bump the Pointer)(默认用指针碰撞)

如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

  • “空闲列表”(Free List)

如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录

解决并发问题的方法:

  • CAS(compare and swap)

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。

3.初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.设置对象头

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

32位对象头

0

64位对象头

0

5.执行方法

  执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JVM-类加载器、对象创建 的相关文章

随机推荐

  • float数据在内存中是怎么存储的

    float类型数字在计算机中用4个字节存储 遵循IEEE 754格式标准 一个浮点数有2部分组成 底数m和指数e 底数部分 使用二进制数来表示此浮点数的实际值 指数部分 占用8bit的二进制数 可表示数值范围为0 255 但是指数可正可负
  • 雷军写的代码上热搜了!

    就在昨天 雷军写的代码 相关话题先后上了一波热搜和热榜 出于好奇 第一时间点进去围观了一波 原来雷总马上要在8月14日举办他的2023年度演讲了 并且也放出了对应的演讲海报 这个海报可以说暗藏玄机 放大后仔细一看 好家伙 密密麻麻全都是代码
  • jsp人事管理系统(源码+数据库)

    实例简介 实现普通用户以及管理员用户的登录 人员的管理 部门的管理 日志等信息的发布 文件 590m com f 25127180 488929552 972443 访问密码 551685 实例截图 以下内容无关 分割线 第一个想法 读取每
  • Android APK安装完成自动删除安装包

    Android APK安装完成自动删除安装包 需要实现此功能 一般实际开发是在自动版本更新上 当更新完开始自动安装完毕后 删除内存卡里的安装包 实现方式很简单 监听应用广播 获取内存卡下的文件 删除 1 监听广播 package com e
  • 深度学习GPU环境安装(Windows、WSL)

    目录 1 概述 2 windows环境安装 2 1 电脑配置 2 2 安装PyTorch 2 3 安装CUDA和cuDNN 2 4 环境变量配置 2 5 测试 3 WSL Linux环境安装 ubuntu 3 1 安装 ubuntu 3 2
  • Vue+Echarts实现饼图统计通过率

    Vue Echarts实现一个饼状图 1 在项目里面安装echarts 2 在需要用图表的地方引入 1 在项目里面安装echarts 在项目中安装echarts 插件 cnpm install echarts save 2 在需要用图表的地
  • 【分治算法】有重复元素的排列问题

    算法实现题 2 8 有重复元素的排列问题 问题描述 设 R r1 r2 rn 是要进行排列的 n 个元素 其中元素r1 r2 rn 可能相同 试设计 一个算法 列出 R 的所有不同排列 编程任务 给定 n 以及待排列的 n 个元素 计算出这
  • 浪潮英信NF5245M3服务器系统安装手册

    安装前 1 准备工作 准备软件 rhel server 6 2 x86 64 dvd ISO镜像 点我下载 RedHat 6 2 EM64T Raid驱动程序 点我下载 UltraISO工具 准备硬件 16GU盘一枚 2 制作U盘启动 Ul
  • os.environ[“CUDA_VISIBLE_DEVICES“] = args.device不起作用

    问题描述 在代码中这样写 if name main os environ CUDA VISIBLE DEVICES args device main 不起作用 原因是os environ CUDA VISIBLE DEVICES args
  • Android使用不可滑动RecyclerView条目显示不全问题(只显示两个item)

    文章目录 前言 二 解决步骤 1 xml布局 2 java代码示例 总结 前言 在写一个ScrollView包裹RecyclerView的功能时 要求RecyclerView包裹数据长度且不可滑动 设置不可滑动后发现item只展示了两条数据
  • svn中出现各种感叹号说明

    黄色感叹号 有冲突 这是有冲突了 冲突就是说你对某个文件进行了修改 别人也对这个文件进行了修改 别人抢在你提交之前先提交了 这时你再提交就会被提示发生冲突 而不允许你提交 防止你的提交覆盖了别人的修改 要解决冲突 如果你确认你的修改是无效的
  • 实现一个计算输入的两数的和与差的简单函数。

    实现一个计算输入的两数的和与差的简单函数 首先 我们知道一般从子函数只能向主函数中返回一个值 可是本题却要求我们返回和与差 这样我们就无法通过返回值来进行完成 于是我相信很多初学者都会犯如下错误 在子函数中通过值传递的方式计算出加减的结果
  • 算法---LeetCode 200. 岛屿数量

    1 题目 原题链接 给你一个由 1 陆地 和 0 水 组成的的二维网格 请你计算网格中岛屿的数量 岛屿总是被水包围 并且每座岛屿只能由水平方向和 或竖直方向上相邻的陆地连接形成 此外 你可以假设该网格的四条边均被水包围 示例 1 输入 gr
  • 非监督特征学习与深度学习(十五)--------长短记忆(Long Short Term Memory,LSTM)

    注 本文转载自https github com exacity simplified deeplearning blob master E5 BE AA E7 8E AF E9 80 92 E5 BD 92 E7 BD 91 E7 BB 9
  • android BitmapFactory.decodeFile返回null

    在做拍照后图片显示的时候 第一次BitmapFactory decodeFileBitmapFactory decodeFile没有返回空 能够正常显示图片 到第二次拍照查看照片时返回null 查看路径都没问题 两张都有保存下来 搜了很多类
  • 安全生产知识竞赛活动小程序界面分享

    安全生产知识竞赛活动小程序界面分享
  • 常见算法问题总结

    1 给定二维数组 其中的元素是a z的26个小写字母 例如如下 int 3 3 a b a c e f d g h i 任意给定字符串str acg 判断在该二维数组中是否存在该字符串 提示 存在是指在二维数组中上 下 左 右所有字符串时候
  • 数组详解【Java】

    目录 前言 一 数组的概念 二 如何使用数组 1 Java中数组的创建以及初始化 2 数组的使用 2 1 用for循环打印 和C语言的原理是一样的 2 2 用for each打印 2 3 用库函数打印 三 数组是引用类型 3 1 简单的认识
  • linux上运行出错libstdc++.so.6 version CXXABI_1.3.8找不到怎么办

    ImportError lib64 libstdc so 6 version CXXABI 1 3 8 not found的解决办法 在Centos7上跑python程序 发现报错找不到CXXABI 1 3 8 查找一通 发现原来是GCC的
  • JVM-类加载器、对象创建

    目录 类加载的各个阶段 一 加载 二 连接 三 初始化 类加载器 Bootstrap ClassLoader Extension ClassLoader 双亲委派 自定义类加载器 即时编译器优化 逃逸分析 方法内联 对象创建 1 类加载检查