jvm加载类的过程

image-20211009111959162

一句话解释清楚整个过程

  1. 先加载 class 进内存
  2. 对 class信息内容进行语法校验,判断是否有错误
  3. 对静态变量初始化
  4. 调用构造器 初始化实例

加载类的方式

加载类的方式有以下几种:

1)从本地系统直接加载

2)通过网络下载.class文件

3)从zip,jar等归档文件中加载.class文件

4)从专有数据库中提取.class文件

5)将Java源文件动态编译为.class文件(服务器)

1、invokeinterface:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的那个对象的特定方法。4
2、invokestatic : 调用静态方法
3、invokespecial: 调用自己的私有方法,构造方法()以及父类的方法。
4、invokevirtual: 调用虚方法,运行期动态查找的过程。
5、invokedynamic: 动态调用方法。
————————————————
版权声明:本文为CSDN博主「魔鬼_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wzq6578702/article/details/82711236

初始化的过程

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

Java语言中类的加载、连接和初始化过程都是在程序运行期间完成的,领Java具备高度的灵活性。

类加载的过程:加载、连接(验证、准备、解析)、初始化。

– 加载:通过一个类的名字获取此类的二进制字节流(PS:不限于从文件中读取);将这个字节流代表的静态存储结构转换为方法区的运行时结构(由具体的虚拟机自己定义);在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口。

– 验证:文件格式验证、元数据验证(语义分析,类与类的继承关系等)、字节码验证(数据流和控制流分析)、符号引用验证(对类自身以外的信息进行匹配校验)

– 准备:正式为类变量分配内存并设置初始值,这里类变量指的是被static修饰的变量。例外:如果类字段是常量,则在这里会被初始化为表达式指定的值。

– 解析:将常量池内的符号引用替换为直接引用。符号引用:类似于OS中的逻辑地址;直接引用:类似于OS中的物理地址,直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。

– 初始化:真正开始执行类中定义的Java程序代码;初始化用于执行Java类的构造方法。类初始化的过程是不可逆的,如果中间一步出错,则无法执行下一步。

双亲委派机制

image-20210916160946361

  • bootstrap class loader 是 加载 c++ 编写的java核心库
  • ext 加载 jre, javax 等类
  • app 加载 user.dir, class 等
  • custom: 用户自己定义加载方式

JAVA 语言怎么实现跨平台

  1. jvm 将java代码编译为.class 文件的中间代码文件, 通过 jvm 来解释执行 这个 字节码, 将字节码翻译为适配操作系统底层的机器语言, jvm 是不跨平台的, 每个操作系统要安装对应版本的jvm 进行语言解释。

什么是 OOM , 哪里会出现 OOM

out of memory

  • 堆内存异常 , java.lang.OutOfMemoryError, java heap space
  • 栈内存溢出, stackOverflowError
  • 永久代溢出, java.lang.OutOfMemoryError: Permgen space

内存分布原理

打印 classLayout

1
2
3
4
5
<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.10</version>
</dependency>

参考博客

参考博客2

  • 查看对象内部信息: ClassLayout.parseInstance(obj).toPrintable()
  • 查看对象外部信息:包括引用的对象:GraphLayout.parseInstance(obj).toPrintable()
  • 查看对象占用空间总大小:GraphLayout.parseInstance(obj).totalSize()

打印测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.openjdk.jol.info.ClassLayout;

/**
 * @Author lyr
 * @create 2021/9/15 19:20
 */
public class Main {
    static class User {
        int p1, p2, p3;
        long l1, l2, l3;
    }

    public static void main(String[] args) throws InterruptedException {
        User user = new User();
        System.out.println(ClassLayout.parseInstance(user));
    }
    
}


/*

 
Main.User.p1 @12 (int, 4b)
Main.User.l1 @16 (long, 8b)
Main.User.l2 @24 (long, 8b)
Main.User.l3 @32 (long, 8b)
Main.User.p2 @40 (int, 4b)
Main.User.p3 @44 (int, 4b)
size = 48



*/
disruptor 缓存行伪共享原理解释

参考博客

缓存中的数据并不是独立的进行存储的,它的最小存储单位是缓存行,缓存行的大小是2的整数幂个字节,最常见的缓存行大小是 64 字节。CPU 为了执行的高效,会在读取某个对象时,从内存上加载 64 的整数倍的长度,来补齐缓存行。

以 Java 的 long 类型为例,它是 8 个字节,假设我们存在一个长度为 8 的 long 数组 arr,那么CPU 在读取 arr[0] 时,首先查询缓存,缓存没有命中,缓存就会去内存中加载。由于缓存的最小存储单位是缓存行,64 字节,且数组的内存地址是连续的,则将 arr[0] 到 arr[7] 加载到缓存中。后续 CPU 查询 arr[6] 时候也可以直接命中缓存。

注意: 数组 0-7 有8个 long 的 数被存到同一个缓存行里面

Cache Line

这就是缓存行共享,听起来不错,但是一旦牵扯到了写入操作就不妙了。

假设 Core-1 想要更新 arr[7] 的值,根据 CPU 的 MESI 协议,那么它所属的缓存行就会被标记为失效。因为它需要告诉其他的 Core,这个 arr[7] 的值已经被更新了,缓存已经不再准确了,你必须得重新去内存拉取。但是由于缓存的最小单元是缓存行,因此只能把 arr[7] 所在的一整行给标识为失效。

此时 Core-2 就会很郁闷了,刚刚还能够从缓存中读取到对象 bar,现在再读取却被告知缓存行失效,必须得去内存重新拉取,延缓了 Core-2 的执行效率。

这就是缓存伪共享问题,两个毫无关联的线程执行,一个线程却因为另一个线程的操作,导致缓存失效。这两个线程其实就是对同一缓存行产生了竞争,降低了并发性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import org.openjdk.jol.info.ClassLayout;

/**
 * @Author lyr
 * @create 2021/9/15 19:20
 */
public class Main {
    // 前:填充 56 字节
    static class RhsPadding extends Value {
        protected long p9, p10, p11, p12, p13, p14, p15;
    }

    static class Value extends LhsPadding {
        protected volatile long value;
    }

   static class LhsPadding {
        protected long p1, p2, p3, p4, p5, p6, p7;
    }


    public static void main(String[] args) throws InterruptedException {
        Value user = new Value();
        System.out.println(ClassLayout.parseInstance(user));
    }

}

/*
Main.LhsPadding.p1 @16 (long, 8b)
Main.LhsPadding.p2 @24 (long, 8b)
Main.LhsPadding.p3 @32 (long, 8b)
Main.LhsPadding.p4 @40 (long, 8b)
Main.LhsPadding.p5 @48 (long, 8b)
Main.LhsPadding.p6 @56 (long, 8b)
Main.LhsPadding.p7 @64 (long, 8b)
Main.Value.value @72 (long, 8b)
size = 80

这里 我解释一下
LhsPadding 这个对象头部 (object header ) 占用 8字节
p1到  p7 7个元素 各占用 8字节
也就是 padding 一共 64字节
---------------
继承类的那个 value  就 占用8字节,就存到了下一个缓存行里面

*/

img

一个对象16个字节

普通对象

对象头:markword 8 (锁的标识位:标识对象的状态,GC标记:对象被回收了多少次 分代年龄)
ClassPointer指针:-XX:+UseCompressedClassPointers 为4字节(默认开启) 不开启为8字节 (对象属于哪个Class)
实例数据 引用类型:-XX:+UseCompressedOops 为4字节(默认开启) 不开启为8字节 Oops Ordinary Object Pointers(成员变量的引用 比如下面的Object o)
Padding对齐,8的倍数 (64位的机器 按块来读,一下子读16个字节)

观察虚拟机配置命令 java -XX:+PrintCommandLineFlags -version

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;

/**
 * @Author lyr
 * @create 2021/9/15 19:20
 */
public class Main {
    // 前:填充 56 字节
    // static class RhsPadding extends Value {
    //     protected long p9, p10, p11, p12, p13, p14, p15;
    // }

    static class Value/* extends LhsPadding */{
        // protected volatile long value;
    }

   // static class LhsPadding {
   //      protected long p1, p2, p3, p4, p5, p6, p7;
   //  }


    public static void main(String[] args) throws InterruptedException {
        Value user = new Value();
        System.out.println(ClassLayout.parseInstance(user));
        System.out.println( GraphLayout.parseInstance(user).toPrintable()
                );
        System.out.println(  GraphLayout.parseInstance(user).totalSize()
                 );
    }

}

/*

size = 16

Main$Value@f2a0b8ed object externals:
          ADDRESS       SIZE TYPE       PATH                           VALUE
        7404f0d08         16 Main$Value                                (object)


16

*/

/*
Main$Value object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
--------------------------
前2行是 markword 占用8字节
打印结果:

16 = 对象头8个字节 + ClassPointer指针 4个字节 +padding对齐 4个字节

*/

对象是怎么定位的

T t = new T()

t是怎么找到堆内存中的T对象

  1. 句柄池:

  2. 直接指针(HotSpot的实现):

img