初步理解JVM虚拟机

一 Java虚拟机运行时数据区 :

共享:

     堆:内存中最大的一个区域,对象实例和数组内部会划分出多个线程私有分配缓冲区;

   方法区:存储已经被虚拟机加载的类信息敞常量,静态变量 即使编辑器后的代码等数据;

私有:
   虚拟机栈:局部变量表 操作数栈 方法出口;

   本地方法栈:Native方法;

   程序计数器:虚拟机字节码指令的地址或者UNdefined;

 3.  稍微具体的结构图:

编译好class文件后才能交给JVM区执行,之后 class文件需要通过类加载机制把类加载到方法区 ,元数据区,然后需要交给执行引擎 将类中的指令解析出来去执行,

在执行的过程当中 ,再根据类去创建对象, 将对象创建到堆中,然后具体的执行某些逻辑就是在线程里面了,(线程时最小的执行单位);

栈帧:

        jvm. 给每一个线程分配一个虚拟机栈 这个栈是一个先进后出的结构, 这个栈会记录这个线程运行时候所需要的数据;在栈帧中会保留对堆中对象的引用。

在每个栈帧中包含的部分:

         (1)局部变量表

         (2)操作数栈。  【这两个是执行Java程序所需要的做基础的数据结构】

动态连结库:主要是指向方法区的某一个方法;

返回地址:方法执行完毕返回到的位置;

附加信息:JVM在具某些具体实现需要;

程序计数器:记录程序运行到了哪一步?【因为CPU是在线程之间不停轮换的,下一个CPU是怎么知道 我线程执行到哪里的了呢,这里就需要提到程序计数器的用处】

  插一句嘴:class文件可以说是JVM 的一个规范,文件中可以有版本号来版本兼容,执行的JVM虚拟机版本号必须>这个文件版本号;

二   虚拟机的本质以及整体流程:

(1)本质:

   将我们的class文件转化成操作系统具体的指令,class文件通过JDK的类加载模块将文件加载到内存当中去。

(2)流程

  1.  下面是我在整理到JVM 整个流程:

这个是网上流传的流程图 ,但是这里面我进行了补充下面相续说一下整个连接的过程 分成三部分。

第一部分 :验证阶段 ,这里面我进行了一个补充 ,验证是在三个地方进行的:对文件格式验证发生在加载阶段,如果通过可以顺利加载,加载后方方法区就在存在一个静态结构,堆中也存在了该class类型的对象,但是程序如果想用这个类,就一定要进行连接!

对元数据字节码验证: 堆class静态结构惊醒语法语义上的分析,保证不会产生危害虚拟机行为的,解析阶段可以在初始化之前和之后进行的,验证包含了很多步骤,分散在不同阶段。

第二部分 准备过程:

这里注意 : 方法区是抽象概念,永久带和元空间是具体实现!!要分清这个概念

第三部分 解析过程 :

问题一: A如何找到B呢?

此时在A 的class文件中将使用一个字符串代表B的地址。如果A发生了类加载 那么到了B就会触发B的类加载此时符号引用会被替换成B的实际地址。

如果不考虑异常的话,那么 JVM 虚拟机执行代码的逻辑就应该是这样:

do{
从程序计数器中读取 PC 寄存器的值 + 1;
根据 PC 寄存器指示的位置,从字节码流中读取一个字节的操作码;
if(字节码存在操作数) 从字节码流中读取对应字节的操作数;
执行操作码所定义的操作;
}while(字节码流⻓度>0)

三 类加载中双亲委派机制:

1. 双亲委派概念:

如果一个类加载器 收到了类加载请求 他首先不会自己去尝试加载这个类  而是把请求委托给父加载器完成 依次而上,因此所有的类加载请求都应该被传递到顶层的启动类加载其中 只有当夫类加载器在他的搜索范围中 没有找到所需的类的时候,子加载器才会尝试自己去加载该类。

2. 双亲委派机制:

   默认情况下 一个限定名的类 只会被一个类加载起加载并解析使用 这样在程序中他就是唯一的 不会产生歧义。   

 注意⚠️。里面所说的父加载器 子加载器 并非继承关系 而是逻辑关系。

3.双亲委派是怎么开始的?

首先检查该类是否已经被加载 如果已经加载 就直接读取缓存,如果没加载 就开启加载流程

双亲委派的作用?

保证Java 内置的一些类 无法被覆盖   无法被修改。比如Java.lang.object为了保证父类的安全——————————还有一个补充机制:沙箱保护机制

双亲委派就是向上委托查找 向下委托加载;(这里面的查找是从缓存中照 找到就可以直接返回 没找到就是父加载器中找)
关于类加载模块,以 JDK8 为例,最为重要的内容我总结为三点:
每个类加载器对加载过的类保持一个缓存。
双亲委派机制,即向上委托查找,向下委托加载。
沙箱保护机制。

JDK8中的类加载器都继承于一个统一的抽象类:


//类加载器的核心方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 每个类加载起对他加载过的类都有一个缓存,先去缓存中查看有没有加载过
Class<?> c = findLoadedClass(name);
if (c == null) {】
//没有加载过,就走双亲委派,找父类加载器进行加载。
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 父类加载起没有加载过,就自行解析class文件加载。
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//这一段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。
// 运行时加载类,默认是无法进行链接步骤的。
if (resolve) {
resolveClass(c);
}
return c;
}
}

这个方法里,就是最为核心的双亲委派机制。并且,这个方法是protected声明的,意味着,是可以被子类覆盖的,所以,双亲委派机制也是可以被打破的。

那么问题来了。为什么要打破双亲委派呢?想想Tomcat要如何加载webapps目录下的多个不同的应用?而关于类加载机制的所有有趣的玩法,也都在这个核心方法里。比如class文件加密加载,热加载等。

4. 沙箱委派机制:
双亲委派机制有一个最大的作用就是要保护JDK内部的核心类不会被应用覆盖。而为了保护JDK内部的核心类,
JAVA在双亲委派的基础上,还加了一层保险。就是ClassLoader中的下面这个方法。

private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// 不允许加载核心类
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}

5. 类和对象有什么关系

通过类加载模块,我们写的 class ⽂件就可以加载到 JVM 当中。但是类加载模块针对的都是类,⽽我们写的 java 程 序都是基于对象来执⾏。类只是创建对象的模板。那么类和对象倒是什么关系呢?
⾸先:类 Class JVM 中的作⽤其实就是⼀个创建对象的模板。也就是说他的作⽤更多的体现在创建对象的过程 当中。⽽在程序具体执⾏的过程中,主要是围绕对象在进⾏,这时候类的作⽤就不⼤了。因此,在 JVM 中,类并不 直接保存在最宝贵最核⼼的堆内存当中,⽽是挪到了堆内存以外的⼀部分内存中。这部分内存,在 JDK8 以前被成 为永久带PermSpace ,⽽在 JDK8 之后被改为了元空间 MetaSpace
在堆中,每⼀个对象的头部,还会保存这个对象的类指针 (classpoint) ,指向元空间中的类。这样我们就可以通过
⼀个对象的 getClass ⽅法获取到对象所属的类了
四、执⾏引擎 - 五分钟
之前已经看到过,在 Class ⽂件当中,已经明确的定义清楚了程序的完整执⾏逻辑。⽽执⾏引擎就是将这些字节
指令转为机器指令去执⾏了。这⼀块更多的是跟操作系统打交道,对开发⼯作其实帮助就不是很⼤了。所以,如果
不是专⻔研究语⾔,执⾏引擎这⼀块就没有必要研究太深了。

四、解释执⾏与编译执⾏

Java文件通过javac编译成class文件 ,这种中间码被称为“字节码”,然后有jvm加载字节码(类加载)。运行时 解释器将字节码解释为一行行的机器码来执行,在程序运行期间,即时编译器会针对热点代码将该部分字节码编译成机器码,来获得更高的执行效率。

在整个运行时解析器和即时编译器的相互配合,使Java程序几乎能够达到和编译型语言一样的执行速度。

JVM 中有两种执⾏的⽅式:
1. 解释执⾏就相当于是同声传译。 JVM 接收⼀条指令,就将这条指令翻译成机器指令执⾏。
2. 编译执⾏就相当于是提前翻译。好⽐领导发⾔前就将讲话稿提前翻译成对应的⽂本,上台讲话时就可以照着念 了。编译执⾏也就是传说中的 JIT 。
⼤部分情况下,使⽤编译执⾏的⽅式显然⽐解释执⾏更快,减少了翻译机器指令的性能消耗。⽽我们常⽤的 HotSpot 虚拟机,最为核⼼的实现机制就是这个 HotSpot 热点。他会搜集⽤户代码中执⾏最频繁的热点代码,形 成CodeCache ,放到元空间中,后续再执⾏就不⽤编译,直接执⾏就可以了。

但是编译执⾏起始也有⼀个问题,那就是程序预热会⽐较慢。毕竟作为虚拟机,你不可能提前预知到程序员要写 ⼀些什么稀奇古怪的代码,也就不可能把所有代码都提前编译成模板。⽽将执⾏频率并不⾼的代码也编译保存下 来,也是得不偿失的。所以,现在JDK 默认采⽤的就是⼀种混合执⾏的⽅式。他会⾃⼰检测采⽤那种⽅式执⾏更 快。虽然你可以⼲预 JDK 的执⾏⽅式,但是在绝⼤部分情况下,都是不需要进⾏⼲预的。
另外,现在也有⼀种提前编译模式, AOT 。可以直接将 Java 程序编译成机器码。⽐如 GraalVM ,可以直接 将 Java 程序编译成可执⾏⽂件,这样就不需要 JVM 虚拟机也能直接在操作系统上执⾏。
关于 AOT 是不是会⼀统天下,也是现在⾯试中⽐较喜欢问的问题。虽然在 SpringBoot3 等框架中已经有 了落地,但是从⽬前来看,AOT 还远没有成为主流,离⼀统天下还有点距离。
少了 JVM 这个中间商之后,虽然⼤部分情况下是可以提升程序执⾏性能的,但是,也并不是就完美⽆缺 了。毕竟很显然,这种⽅式其实是以丧失⼀定的跨平台特性作为代价的。
要注意,⽬前 AOT 这种⽅式还是不太安全的。毕竟 JVM 打了这么多年的怪,什么⽜⻤ 神都⻅多了。现 AOT 要绕开 JVM ,那么这些怪就都要⾃⼰去打了。中间有个什么疏忽,那是难免的
垃圾回收机制下一张在继续 ,下面说一点面试题吧

 典型面试题;

  (一) 基础类形装箱:

      下面代码输出结果是什么 ,为什么会有这样的输出结果:

          答案:  Integer里面有规定 如果传入的(int)是在-128~127 期间 那么就会返回数组中的对象,如果在-127~128 之外 就会返回一个新的对象。

  (二)Java中的静态方法可以重载嘛?

       完美回答: 不能,因为在JVM中 调用方法提供了集中不同的字节码指令:

  invokcvitual 是来调用对象的虚方法(也就是可重载的这些方法)

  invokespecial 根据编译时类型来调用实例方法(比如静态代码  (通常对应字节码层面的init方法),比如构造方法(通常对应字节码层面的init方法));

  invokestatic 调用类(静态方法);

 invokeinterface 调用接口方法;

所以看得出来  静态方法和重载方法他们调用指令都不一样,那肯定无法重载静态方法的;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值