Skip to content

了解了 JVM 的运行时数据区之后,大概明白虚拟机的内存模型。以及每个地方的特点和作用。 进一步的去了解内存中数据的其他细节,创建、布局、访问等

模拟计算机, 遵循的冯诺依曼计算机体系.对Class文件和语法进行了安全性相关的约束

反馈和验证

  • 对原理的掌握程度
  • 线上遇到了OOM应该怎么办, fullGC频繁怎么办
  • G1的常规参数 4h8g 的初始线程数量应该是多少 maxModel 应该是多少
  • 怎么把class文件交给JVM?

问题

什么是类加载机制?

其实就是读取文件, 最终的目的就是为了访问文件里的内容

操作

  1. 拿到当前文件地址和名称
  2. IO读取到内存中

字节码文件 怎么加载到内存中? 1. 本地系统 2. 网络系统 3. 压缩文件 zip 后续演进成了 jar war包 4. 专有数据库的方式去提取class文件, 5. 将java文件动态的去编译成class文件 6. 加密文件中去读取class

JVM 是怎么分配内存的

我们可以思考一下, 如果自己实现这个功能的话, 有什么方案

  1. 如果内存是完整的、连续的,那么可以通过一个坐标,记录当前这个内存的总大小,已经使用的大小,加载这个对象需要多大,就把已经使用的大小加上当前加载对象的大小 (指针碰撞)
  2. 这个时候如果并发创建的话,可能会有对象覆盖的风险,JVM 通过 CAS + 失败重试的方式进行创建。
  3. 当然也可以通过本地线程的内存分配缓冲区(给每一个线程都预先分配一份自己的内存 ThreadLocalAllotion Buffer,TLAB)
  4. 如果内存不是完整的,那么可以通过维护一个列表,记录哪些内存是可用的,大小是多少。 (空间列表)

对象是如何创建的

在 Java 语言层面,仅仅是通过一个 new 或者复制,反射等方式。就可以创建出一个 Java 对象。

1. 装载、链接、初始化 针对 JVM 的对象创建过程, JVM 当遇到一个 new 关键字时,首先会去常量池查找一番,看看有没有这个对象的符号引用。 如果有则检查当前这个符号引用代表的类,是否已经经过了 加载链接初始化 的过程,有则直接使用,没有则继续创建。

2. 对新对象进行内存分配 在加载完成之后,就可以得知当前这个对象需要多大的内存。然后去堆内存中划分出当前对象所需要的内存空间。

3. 对象赋值零值 虚拟机必须将分配到的内存空间,都设置成零值,(如果有开启 TALB,则在 TALB 时进行处理) 作用: 保证了对象的实例字段可以不赋值初始值,就可以直接使用。这个时候程序访问到的值其实都是零值 (也就是相对应数据类型的默认值)

4. 头信息设置 设置当前这个实例属于那个类,找到元数据信息, hashcode 值,放到对象的头信息上。

前人总结

  1. 检查类是否被加载,如果没有则进行类加载机制
  2. 分配内存(类加载的时候就知道对象需要多少内存了)
    1. 是否有设置 TALB ,有则有限去 TABL 上进行分配,等 TALB 内存满了之后再去堆内存分配(同步的 CAS+失败重试机制)
  3. 给字段设置零值
    1. 如果是在 TALB 上进行分配的,那么分配完之后在 TALB 上就会直接设置零值
  4. 设置对象的头信息
  5. 调用 init 构造方法执行初始化

什么是类加载机制?

Loading,Linking and initializing 加载 链接 初始化 (取自JDK官方文档) (深入理解 Java 虚拟机) 一个类型从被加载到虚拟机内存中开始, 到卸载出内存为止。 完整的生命周期将经历 加载(Loading)、验证(Verification)、准备(Perparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading) 七个阶段

一个类的完整生命周期

JVM是如何实现 装载的

JVM为什么是虚拟机, 它既然叫虚拟机 就说明 它也是 "机" 那么 JVM 就一定也会有 输入(读取class文件) 输出(交给JVM进行run) 计算 存储 这些过程 以及功能或者叫能力.
这就是运行时 数据区

  1. 毫无疑问, 肯定是通过IO流 ,那么到底是字符流还是字节流呢. 答案是后者
  2. ClassLoader : 通过类的全限定类名 获取这个类的 字节流, 类加载器 不属于 JVM的内部模块, 这个是外部模块
  3. 通过引用去操作对象, 通过对象去操作相应的数据 (典型的面向对象)
  4. 在Java 堆 内存中, 去生成一个 对应的对象, 作为方法区的数据访问入口 (这样子 才算加载完成)

  • 运行时数据区

JVM 是如何实现链接的

JVM是如何实现验证的

文件格式验证 字节流是否符合Class的规范, 要可以被当前版本虚拟机处理并解析 在字节流转换成方法区运行时数据 之前的操作 Java文件的魔术 是否 是kafebabe 开头, 以及Java编译版本 能否和JVM版本对应的上 元数据验证 对于Java 语法的验证, 保证安全性和健壮性 字节码验证 数据流以及控制流的分析,比较复杂, 运行时检查, 栈内存的数据类型和操作码 是否与操作参数符合 符号引用验证 将符号引用转换成直接引用 常量池 访问方法和类是否有权限,

JVM是如何实现准备的

  1. 为类的静态变量分配内存,并且设置默认值 (非原子的)
  2. 准备阶段 是给 默认值的 int = 1; 在准备阶段 他是0 赋值的时候是在 class init时候执行的
  3. final 修饰的 不在这里, 是在编译的时候就已经分配好了.
  4. 实例变量也不在这个地方, 是在对象分配的时候, 一起分配到堆内存当中的

JVM是如何实现 解析的

符号引用转换成直接引用

字面上的引用关系 转换成 直接指向 对象的内存

同一个符号引用 可能会有多个解析的需求, 所以 JVM 会有一个缓存

虚拟机 可以对 任意一个指令的第一次执行 进行缓存

虚拟机将常量池内的符号引用替换为直接引用的过程

符号引用: 是一组符号来描述所引用的目标,可以是任何形式的字面量。 直接引用: 是指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄

JVM 针对同一个符号引用进行多次解析请求的情况会进行缓存,在运行时直接引用常量池的记录,并吧常量标记位已解析的状态,来规避解析动作的重复执行。

JVM 是如何实现初始化

如何初始化的?

执行 类的 构造方法. 准备阶段 设置的默认值, 在此时 构造方法的阶段, 才会吧 你代码中 给的值 替换掉默认值 声明类变量 为指定的初始值 静态代码块为类变量去赋值

编译成字节码之后就会调用 init 方法来进行初始化,这里其实对应的就是代码中写的构造方法。

这里就会有一个问题, 你的 初始化静态代码块 和 类变量, 有先后顺序的执行问题, 顺序搞错了 业务赋值会有问题

所以 静态变量一定要写在静态代码块前面 不然 可能会有问题

JVM 是如何实现卸载

JVM 针对以下两种情况, 会触发卸载动作

  • 当这个类没有任何实例的时候, 就会被卸载
  • 加载这个类的ClassLoader 已经被回收

类加载器

为了安全起见, 防止篡改

  1. Bootstrap ClassLoader 负责加载 Java_HOME 中 jre/lib.rt.jar 下的所有 class 或 XbootClassoath 选项指定的jar包. 是C++ 实现的 不是ClassLoder的子类 是 C ++ 实现的
  2. Extension ClassLoder 负责加载 Java平台中拓展功能的一些Jar包, 包括但不限于 Java_home中 jre/lib/*.jar 或者 -Djava.ext.dirs指定目录下的jar包
  3. App ClassLoder 负责加载 classPath中指定的jar包及-Djava.class.path 所指定目录下的类和jar包.
  4. Custom ClassLoder 通过Java.lang.ClassLoder的子类自定义加载class, 属于应用程序根据自身需要自定义的ClassLoder,如Tomcat,Jboos,都会根据j2ee规范自行实现ClassLoder

当我们get BootstrapClassLoder时是get不到的, 因为他是c++实现的, 所以打印的是个null

三大特性

全盘委托

当加载一个Class 的时候, 这个class 所依赖的其他class 也由当前的ClassLoader进行加载, 除非进行了特别的指定说明 某一个class需要用xxxClassLoder进行加载 类加载的入口 就是当前类 然后向上找 走双亲委派

双亲委派(不是强制模型,是可以进行打破的)

举个例子, 子类 优先向上找父类进行加载, 父类在向父类找, 如果都没有 依次返回 进行加载 当前ClassLoder 判断这个class 是否已经加载, 如果已经加载过, 就返回,如果没有就委托父类进行加载

缓存机制

所有已经加载过的class 都会进行缓存到 直接内存

不同的类加载器中, 是允许 同全限定类名 的多个类 存在的 这个是怎么做到的?

通过类加载器来实现的 SystemClassLoader ExtClassLoader

OSGI 根据类加载器去实现热部署

waitingresult.com