[JVM]JVM内存模型,类加载过程,双亲委派模型

1. JDK,JRE,JVM分别是什么,它们之间有什么联系?

JDK: 是Java开发工具包,包含了编写,编译,调试和运行Java程序所需要的所有工具和组件。比如编译器Javac,JavaAPI,调试工具等,JDK是针对Java开发人员的,它包含了Jre,还有编译器和其他工具,可以用来编写和调试Java程序

JRE: 是Java运行时环境,包括了Java虚拟机和Java标准类库,用于在计算机中运行Java程序。

JVM: 是Java虚拟机,是Java程序的运行环境,负责将Java代码转化成为可以在计算机上运行的机器码,并且提供必要的条件支持。

2. JVM内存区域划分

JVM内存区域包括:程序计数器,栈区,堆区,方法区。

程序计数器: 内存中最小的区域,保存了下一条要执行的指令地址在哪。这里的指令就是使用JVM加载后的字节码文件,程序要想执行,那么此时就要使用JVM把字节码文件加载起来,放到内存中国,程序就会一条一条的把指令从内从中取出来,放到CPU上执行,那么此时就需要随时记住,当前执行到哪一条了。

我们知道CPU是并发执行程序的,CPU不是只给你一个进程提供服务的,要伺候所有的进程。因为操作系统是以线程为单位进行跳读执行的。每个线程都记录自己的执行位置。

还有就是我们的程序计数器,是在线程中存在的,并且每个线程只有一个程序计数器。

栈区: 用来存储局部变量和方法调用的信息。方法调用的时候,每次调用一个新的方法,就会设计到一个入栈操作,每次执行完一个方法之后,都涉及到出栈操作。

看如下代码:

void A(){
    B();
}
void B(){
    C();
}
void C(){

}

在这里插入图片描述

如果不停的在栈区中存放方法的信息,那么就会造成栈溢出。那么此时就要联想到我们的递归方法,自己调用自己,那一个方法的信息不停的添加到栈区中,如果我们此时的递归条件,没有写正确,那么就会造成栈溢出。

在JVM中,栈的空间是比较小的,并且在JVM中可以配置栈空间的大小,但是一般也就是几M,或者几十M。这里的栈区其实就和我们在学习数据结构时学的基本一样。

我们要注意的是,这个栈区是在每个线程中存储一份的。

堆区: 一个进程只有一份,那么就是进程中的多个线程,共享我们这里的堆区 这个也是北村中空间占有最大的区域,我们一般写代码时,new出来的对象,都添加到对中,对象的成员变量自然也是在堆中了。

但是这里有这一句话,大家看对不对,内置类型的变量,在栈区上。引用类型的变量在堆区上。其实这个说法是错误的,正确的应该是局部变量,在栈区,成员变量和new出来的对象,在堆上

void func(){
    String s = new String()l
}

此时这个操作在执行的时候,s和new String()是两个东西。
在这里插入图片描述

因为String 是引用类型,那么此时这里的s是一个引用类型的变量,但是它在一个方法中,方法一执行结束,那么此时这个s就从栈区消失了,那么此时的这个s就是一个局部变量。

这里的new String() 是一个对象的实体,是在堆上储存的

class Test{
    String s = new String();
}
new Test();

这里的s是一个引用类型的变量,并且此时s是在Test类中的一个成员变量,那么此时的这个s就就是在堆区中的。

同样我们的new String() 这个对象主体,也是在堆区中的。
在这里插入图片描述

方法区: 方法区中,存放的是"类对象"

.class文件会被加载到内存中,也就被JVM构造成类对象(加载的称为"类加载")

这里的类加载,就是放到方法区中,那么类对象就是描述这个类长啥样。好比说,类的名字是什么,里面有哪些成员,有哪些方法,每个成员叫啥名字,是啥类型,public/private,每个方法叫啥名字,是啥类型,public。private,方法里面包含的指令。这里我们有没有想到Java的反射机制呢???😏😏😏

类对象中还有一个很重要的东西,就是静态成员。静态成员是使用static关键字修饰的成员,也称为类属性,而我们的普通成员,叫做实例属性

3. JVM类加载过程

类加载:其实就是设计一个运行是环境的一个重要的核心的功能。类加载就是把.class文件,加载到内存中,构建成类对象

类加载大致分为三个大步骤:
在这里插入图片描述

Loading环节:

先找到对应的.class文件,然后打开并读取.class文件,同时初步的生成一个类对象

在Loading的一个关键环节,.class文件到底是啥样的呢?
在这里插入图片描述

.class文件的格式正如上图,其中 u4 就是4个字节的unsigned int u2 就是2个字节的unsigned int ,cp_info/field_info 都是结构体。field_info表示的是类中的信息,method_info就表示的是方法中的信息

我们可以观察这个.class文件的格式,既可以看到 .class文件就把.java文件中的核心信息都表述进去了,只不过组织格式发生了变化。

Linking:

连接一般就是创建好几个实体之间的联系。

Verification: 表示的是校验过程,主要就是验证读到的内容是不是和规范中规定的格式,完全匹配。如果发现这里读到的数据格式不符合规范,就会类加载失败,并且抛出异常。

PreParation: 准备阶段是这个是为类中定义的变量,静态变量,被static修饰的变量,分配内存并设置变量初始化的阶段。

给静态变量分配内存,并且设置0值

Resoulation:表示的是解析。.class文件中,常量是集中放置的,每个常量有一个编号。.class文件的结构体里初始化情况下只是记录了编号。就需要根据编号找到对应的内容,填充到类对象中。

Initializing: 真正对类对象进行初始化,尤其是针对静态成员。

4. 一个经典面试题

大家看看下面的代码片段,判断输入结构是什么?

class A{
    public A(){
        System.out.println("执行A类中无参的构造方法");
    }
    {
        System.out.println("执行A类中的构造方法块");
    }
    static{
        System.out.println("执行A类中的静态代码块");
    }
}
class B extends A{
    public B(){
        System.out.println("执行B类中无参的构造方法");
    }
    {
        System.out.println("执行B类中的构造方法块");
    }
    static{
        System.out.println("执行B类中的静态代码块");
    }
}
public class TestDemo2 extends B {
    public static void main(String[] args) {
        new TestDemo();
        new TestDemo();
    }
}

这里有一个大的原则:

  1. 类加载阶段会进行 静态代码块的执行,要想创建实例,那么势必要先进行类加载
  2. 这里的静态代码块只是在类加载阶段执行一次
  3. 构造方法和构造代码块,每次实例化都会被执行,构造代码块在构造方法的前面执行。
  4. 父类执行在前,子类执行在后。
  5. 我们这里的main方法开始执行,然后调用TestDemo2的方法,因此要执行main方法,就需要先加载TestDemo2
  6. TestDemo2类继承自B,要加载TestDemo2,就要先加载B
  7. B继承自A,要加载B,就要先加载A

所以最后的执行结果就是:

执行A类中的静态代码块
执行B类中的静态代码块
执行A类中的构造方法块
执行A类中无参的构造方法
执行B类中的构造方法块
执行B类中无参的构造方法
执行A类中的构造方法块
执行A类中无参的构造方法
执行B类中的构造方法块
执行B类中无参的构造方法

5. JVM 双亲委派模型

其实这个JVM中的双亲委派模型在面试中是非常喜欢考的,但是这个东西是不重要的,这个东西其实也不是很难理解的,这个东西在我们日常编码的时候没有指导意义。即使它有一个高大上的名字。

其实这里的双亲委派模型就是一个类加载环节

这个环节是处于Loading状态的

双亲委派模型,描述的就是JVM中的一个类加载器,如何根据类的权限名,就如(java.lang.String)找到.class文件的过程。

这个类加载器是JVM中提供了一个专门的对象,叫做类加载器,负责进行类加载,当然找文件的过程也是有类加载器来负责的。

这里的.class文件,可能放置的位置有很多,有的放到JDK目录中,有的放到项目目录中,还有在其他的特定位置。

默认的类的加载器,主要有3个

  1. BootStrapClassLoader:负责加载标准库中的类(String,ArrayList,random,Scanner等)
  2. ExtensionClassLoader: 负责加载JDK扩展的类
  3. ApplicationClassLoader:负责加载当前项目中的类

其实我们程序员也可以自定义类加载器,来加载其他目录中的类,就比如Tomcat就自定义了类加载器,用来专门加载webapps里面的.class文件

我们的双亲委派模型,就描述这个找目录的过程,也就是上述类加载器是如何配合的

考虑加载我们的java.util.String

  1. 程序启动,先进入ApplicationclassLoader类加载器
  2. ApplicationClassLoader就会检查下它的父加载器是否已经加载过了,如果没有,就调用父加载器extensionClassLoader
  3. ExtensionclassLoader也会检查下,它的父加载器是否加载过了,如果没有,就调用父加载器BootStrapClassloader
  4. BootStropClassLoader也会检查下,它的父加载器是否加载过了,但是自己没有父加载器,于是就扫描自己负责的目录
  5. java.lang.String这个类在标准库中内找到,直接由BootStrapClassLoader负责后序的加载过程,查询环节就结束了。
    在这里插入图片描述

类加载器,加载我们自己写的Test类

首先前面还是一样的

  1. 程序启动,先进入ApplicationclassLoader类加载器
  2. ApplicationClassLoader就会检查下它的父加载器是否已经加载过了,如果没有,就调用父加载器extensionClassLoader
  3. ExtensionclassLoader也会检查下,它的父加载器是否加载过了,如果没有,就调用父加载器BootStrapClassloader也会检查下,它的父加载器是否加载过了,但是自己没有父加载器,于是就扫描自己负责的目录
  4. BootStropClassLoader也会检查下,它的父加载器是否加载过了,但是自己没有父加载器,于是就扫描自己负责的目录。但是还是没有找到,那么此时就回到子记载器中继续扫描。
  5. ExtensionClassLOader也扫描自己负责的目录,也没扫描到,回到子加载器继续扫描
  6. ApplicationClassLoader也扫描自己负责的目录,能找到Test类,于是进行后溪的加载,查找目录环节结束。
    在这里插入图片描述
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 我不认为JVM可以“打破”双亲委派模型JVM使用双亲委派模型是为了保证类的加载和安全性,这是JVM的基本设计原则之一。 在双亲委派模型中,当一个类被加载时,首先会委托其父类加载器尝试加载该类。如果父类加载器无法加载该类,则会将该请求传递给其父类加载器的父类加载器,直到最终传递到启动类加载器。如果启动类加载器也无法加载该类,则会抛出ClassNotFoundException异常。 这种模型的好处是可以防止类的重复加载,并且可以保证类的安全性,因为只有在受信任的类加载器中加载的类才能访问受信任的资源。如果JVM打破了这种模型,可能会导致类的重复加载和不安全的类访问,从而破坏JVM的基本设计原则。 虽然JVM不能打破双亲委派模型,但是它提供了一些机制来绕过该模型。例如,可以使用Java的反射机制或者自定义类加载器来加载类,但是这些机制都需要开发者自己负责类的加载和安全性,因此需要谨慎使用。 ### 回答2: JVM双亲委派模型是一种类加载机制,用于保证Java程序的安全性和稳定性。在双亲委派模型中,JVM首先会查找并加载自定义类加载器的类,如果找不到,才会向上一级的父类加载器请求加载。这种模型的好处是可以避免类重复加载和类冲突,确保程序的运行稳定性。 但有时候,我们可能需要打破双亲委派模型,以满足特定的需求。下面是一些打破双亲委派模型的方式: 1. 自定义类加载器:可以自己编写一个类加载器,并重写其中的加载类的逻辑。这样就可以绕过父类加载器,直接加载自定义的类。 2. 加载本地库:JVM类加载器无法加载本地库,因此可以通过JNI(Java Native Interface)来打破双亲委派模型,直接加载本地库。 3. 使用反射技术:通过反射,可以调用私有的加载类方法,从而绕过双亲委派模型,加载特定的类。 4. 使用SPI机制:在Java标准库中,有一些接口通过SPI(Service Provider Interface)机制提供了灵活的类加载方式,可以通过实现自己的SPI来打破双亲委派模型。 需要注意的是,打破双亲委派模型可能会导致类的重复加载和类冲突,引发程序的运行问题。因此,在使用这些方法时,需要谨慎考虑,确保安全性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小周学编程~~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值