白话说Java虚拟机原理系列【第四章】:内存结构之方法区详解

news2025/1/15 12:55:39

文章目录

        • 执行引擎
        • 内存结构:运行时数据区
        • 方法区(永久代PermGen)
            • 方法区的设计初衷?
            • 方法区存的什么内容?
            • 方法区的异常:
            • 运行时常量池:
            • `方发表:`这里我们详细讲解


前导说明:
本文基于《深入理解Java虚拟机》第二版和个人理解完成,
以大白话的形式期望能用大家都看懂的描述讲清楚虚拟机内幕,
后续会增加基于《深入理解Java虚拟机》第三版内容,并进行二个版本的对比

执行引擎

即线程,每一个线程都可理解成是一个执行引擎。

内存结构:运行时数据区

在这里插入图片描述

运行时数据区:当JVM运行一个程序时,需要在内存中存储许多数据。比如字节码/程序创建的对象/传递的方法参数/返回值/局部变量等等。JVM会把这些东西都分开存储到几个不同的区域(运行时数据区)中并关联起来,便于它内部的管理,为了直观JVM规范指定了这几个区域的名字,如下:

  • 方法区
  • Java栈
  • PC寄存器
  • 本地方法栈
  • 直接内存:该区域是JDK1.7之后单独提出来的区域。

方法区(永久代PermGen)

线程共享此区域:即存与方法区中的数据有并发隐患,存在缓存一致性问题

方法区的设计初衷?

用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等。虽然JVM规范把方法区描述为堆内存的一个逻辑部分,但是它还有另一个名字叫Non-Heap(非堆)。

为了让GC垃圾收集器能收集方法区这个内存区域,JVM使用永久代的概念来实现了方法区,其实方法区和永久代是不等同的,永久代是因为GC分带收集而产生的概念,以此来让GC也能对方法区进行垃圾收集,这样就省去了专门为方法区编写内存管理的代码工作。而想要GC收集此空间的资源主要包括常量池类型的卸载,但是类型卸载的回收要求严苛,很难实现,目前GC对该区域的回收基本没有什么实际回收作用。

这样的以永久代实现方法区的只是HotSpod的做法,其他第三方实现的虚拟机有的都不存在永久代的说法。目前看用永久代实现方法区不太好,因为永久代有-XX:MaxPermSize的上限,这样就间接的导致了方法区也有了上限,会出现内存溢出。所以新的规划是放弃永久代(jdk 1.8),改用Native Memory(本地内存)来实现方法区,这样就没有限制了,但是需要自己编写内存管理功能。从JDK 1.7开始已经把存放在永久代(方法区)的字符串常量池移出。

方法区存的什么内容?

类被加载器加载后,将会提取class文件的内容存储到方法区,存储的内容基本包含一个class文件的全部信息,最后还会通过类加载器的defineClass()方法创建一个Class类的对象(对象内容当然也是class文件内的信息),该对象会存入堆内存,但是方法区中该class文件的存储区域会保存有与堆中对象的关联信息,以便能获取到相关的数据。

下边看下具体方法区会保存class文件的哪些信息?
1.类的全限定名,如java.io.FileOutputStream。
2.类的直接超类的全限定名,如java.lang.Object。
3.类是接口还是类类型
4.类的访问修饰符
5.等等。。。,其实就是几乎把整个class文件的接口全部保存了。具体可见class字节码文件格式
6.类静态变量,通过class文件常量池的概念就知道,静态变量也存于此,所以静态的属于类的说法在于此,因为不是保存在堆中,而是保存在方法区的该类的信息中,即有了类就有了静态变量,而不是有了对象才有的静态变量。
7.类加载器信息也会保存在该类在方法区中的结构中,用来确定哪个类加载器加载的该类。
8.指向堆中该类的Class对象的引用。
9.方法表,就是类中定义的各个方法,此处涉及到java多态的原理,在下边的方法调用小结讲解。

方法区的异常:

方法区无法满足内存分配时,将抛出OutOfMemoryError异常。

运行时常量池:
  • 位于方法区的一个区域。
  • 上边我们提到过,编译后的class文件中有一个常量池区域,保存的字面量和符号引用。这个常量池的内容在class文件被加载后会被放在这个单独的"运行时常量池"中。
  • 运行时常量池优于class文件中的常量池的地方是他是动态的,就是除了class文件中的常量池会进入这里外,程序运行时创建的一些常量也会放入这个运行时常量池中。【比如String.intern()方法,这个方法是判断当前变量是否在常量池中存在,如果存在则拿过来用,如果不存在则创建并返回引用,不过这个方法比较复杂,可以百度看详细或看api文档。
String.intern()方法:
判断这个常量是否存在于常量池。
  如果存在
   判断存在内容是引用还是常量,
    如果是引用,
     返回引用地址指向堆空间对象,
    如果是常量,
     直接返回常量池常量
  如果不存在,
   将当前对象引用复制到常量池,并且返回的是当前对象的引用
比如:
String a = "AA"; //它会先在常量池中创建AA,然后从常量池返回AA给a
String b = new String("BB"); //它会先常量池创建BB,然后在堆内存分配空间指向常量池,最后b指向堆得引用
String c = b.intern(); //这个方法将会做一些列检查,然后返回给c
注意:运行时常量池也可以保存引用哦。
注意:因为JDK 1.7将字符串常量移出了方法区,所以以上的描述只适用于JDK 1.7版本之前,之后的话创建的字符串将直接在堆中保存,即堆中维护着一个字符串常量池。
  • 异常:因为是占用方法区的内存区域,所以同样受方法区的限制,即当运行时常量池无法再申请内存时则抛出OutOfMemoryError异常。

总结:方法区中保存的是加载的class文件信息、常量池信息(保存的是已经经过常量池解析后的常量池,即符号引用已转为直接引用)、运行时常量池等。

方发表:这里我们详细讲解
  • 方法调用:注意方法调用不等于方法执行
    方法调用阶段唯一的任务就是要确定被调用方法的版本(就是确定要调用哪个方法),暂时还涉及不到方法代码的运行,即方法调用过程结果后,才会进行方法执行。
  • 符号引用:
    我们已经知道,java代码被编译成class文件后保存的都是符号引用(且这些符号引用都是指向class文件的常量池中),就是说一切方法的调用在class文件中保存的都是符号引用(因为压根代码都没进入内存何来有内存分配的地址呢?),而不是方法在实际运行时内存布局中的入口地址(就是直接引用的意思)。所以无法在编译后的class文件中确定直接引用,那么自然就需要运行时动态来确定具体的直接引用到底是哪个内存地址了,这就是java多态的本质。
  • 符号引用变成直接引用:其实这个变化的过程可以大致的分成2个阶段
    ①类加载阶段:就是在类被加载器加载时把一部分符号引用转换成直接引用,也叫做解析
    ②运行时阶段:另一部分没在解析过程转换的,就只能到程序运行时的时候动态转换了,也叫做分派

解析:能在这个过程完成符号引用转换成直接引用的条件:
①方法在程序真正运行之前就有一个可以确定的调用版本;
②并且这个方法的调用版本在运行期是不可变的。

换句话说:其实就是在程序代码写好、编译器在进行生成class文件的编译的时候,就能确定方法的调用版本了,因为类加载后,并没有做一些高级的工作,只是装载、连接、初始化,也就是说类加载能确定的,其实在代码编写的时候就已经确定了。

那么满足“编译期可知,运行期不变”的方法都是啥方法呢?
①static静态方法:属于类的,于类直接绑定
②private私有方法:也是类私有,其他外部不能访问
③final方法:这个比较特殊,因为被他修饰的方法不会被重写,所以也是属于当前类的,不会有不同的版本;

最后与字节码指令对应:我们知道有以下常规的4种调用指令
①invoke static:调用静态方法
②invoke special:调用私有方法、构造方法、父类方法
③invoke virtual:调用虚方法
④invoke interface:调用接口,会在运行时再确定一个实现此接口的对象
而从这里看,我们能清楚知道①、②两个调用指令正是调用解析过程的能将符号引用转换成直接引用的方法,当然final比较特殊,他是通过invoke virtual调用,就是会用多态方式在运行时才能确定具体的版本,但是因为final只会有一个版本,所以也就跟此处说的解析过程的方法没区别了,这就是所谓的静态绑定原理所在。很显然了除final方法以外的所有通过invoke virtual调用的方法都将是动态绑定的方法,即多态的功能原理。

分派:也可以说这里就是动态绑定的方法,就是只有运行时才能确定方法调用的版本,
换个角度说就是只有运行期间才能确定要调用的方法的内存地址是啥,
再换个角度就是只有运行时才能确定一个方法的符号引用到底对应的直接引用是啥。

先补充一下java的特性:继承、多态、封装,而此处就涉及到两个重要的名词,
①重载:同一个类中同方法名不同的参数结构的方法
②重写:发生在继承关系的子类中,子类重写父类的方法

为什么跟这两个概念有关?
我们知道方法调用的重点就是确定方法的版本,而可能导致一个方法有多个版本的情况,就只有【重写】和【重载】了。

当然我们会有疑问,那没有被重写和重载的方法也并没有归到静态绑定啊?
这部分方法其实方法的版本肯定也是只有一个,和final类似,只不过不像final一样是事先肯定能确定的只有一个版本,所以这些方法肯定也是invoke virtual调用,只不过最终只会有一个版本罢了,因为不如final典型特殊,所以仍然归到动态绑定里说,后边我们也会说清楚为什么他们这些普通方法只有一个版本。

我们就重点看看【重载】和【重写】到底怎么实现动态绑定吧?

重载:先看一个代码,如下:其中Human是父类,Man是子类
Human man = new Man();
这里有两个概念:
①变量的静态类型:Human就是man变量的静态类型
②变量的实际类型:Man就是man变量的实际类型

从这段代码看,其实Human man = new Man()被编译器编译的时候,常量池中就会记录man变量的符号引用是Human,也即编译期静态类型是知道的,但是实际类型则只有创建对象后才知道是Man类型,我们也可以通过javap查看字节码指令来知道编译的class文件中确实是已经确定了符号引用的值为Human,只不过运行后符号引用替换成直接引用后就变成了Man类对象在堆中的地址位置了。

举例:
public class T{
   static class Human{}
   static class Man extends Humman{}
   static class Woman extends Humman{}

   public void sayHello(Human m){System.out.println("Human")}
   public void sayHello(Man m){System.out.println("Man")}
   public void sayHello(Woman m){System.out.println("Woman")}

   public static void main(String[] args){
       Human m = new Man();
       Human w = new Woman();

       T t = new T();
       t.sayHello(m);
       t.sayHello(w);
   }
}

// 结果:
Human
Human

分析:从代码看,我们知道方法最终都是会进行编译的,那么三个sayHello最终会编译成符号引用,那么方法的符号引用的表示方法我们知道,就是方法名+符号描述符,符号描述符当然包括返回值和参数,因为返回值都一样,所以区分他们的地方就是参数类型了,我们知道参数类型是Human、Man、Woman这三个。

静态分派:我们从上边知道,虽然重载的方法都不是静态绑定的那种方法,但是他们经过编译器编译后确实就已经能确定方法的版本了(通过静态类型确定方法的版本),我们也知道编译成的指令只有在调用方法的时候才会真实的执行invokexxx指令,所以t.sayHello(m)这句代码,肯定是会编译成invokevirtual指令,那么指令指向的符号引用是哪个方法呢?很简单,因为调用语句传入的m是Human类型,即静态类型是Human,所以invokevirtual要调用的当然就是参数类型是Human的这个方法了,直接在编译期确定调用版本,这也叫静态分派,注意这个和静态绑定不是同一个概念。

==>总结:至此我们在编译期能确定方法版本的如下:

  • ①编译期能确定版本的方法:之前说的静态绑定的方法
  • ②编译期能确定版本的方法:重载的方法

重写:到这个环节还不能确定方法版本的,在java中也就只剩下继承关系中的子类中重写父类的方法的这些方法了吧,因为其他类型的方法上边都说完了。

例子:
public class T{
   static class Human{
       protected void sayHello(){System.out.println("Human");}
   }
   static class Man extends Human{
       protected void sayHello(){System.out.println("Man");}
   }
   static class Woman extends Human{
       protected void sayHello(){System.out.println("Woman");}
   }
   public static void main(String[] args){
       Human m = new Man();
       Human w = new Woman();
       m.sayHello();
       w.sayHello();
   }    
}

结果:
Man
Woman

显然:重写的方法并不是根据静态类型就能确定版本的了,否则调用结果就应该都是Human了。他的结果很明显是跟实际类型有关的。

分析:通过javap查看class文件字节码:
在这里插入图片描述
17、21行就是2个调用方法的语句,生成的指令都是invokevirtual,并且符号引用都是指向了常量池的22号位置,其实都是指向了Human.sayHello方法,为什么指向这里,原因很简单,因为编译器只能使用符号引用,所以m和w都是Human类型的变量所以肯定是指向Human.sayHello方法,那么为什么实际中他们运行起来又会调用具体的实际类型的方法?

这就要从invokevirtual的执行原理说起了,或者说是查找方法版本的过程:该指令会进行以下查找尝试:
①找到操作数栈顶的第一个元素所指向的对象的实际类型,记做C(如:m或w的实际类型)。
②如果在类型C中找到与常量池中的描述符和简单名称都相符的方法(如Man或Woman中的sayHello方法),则进行访问权限的验证,
如果验证通过,则返回这个方法的直接引用,查找过程即结束;
如果验证失败,则返回java.lang.IllegalAccessError异常;
③如果在类型C中没有找到相符的方法,则按照继承关系从下往上依次对C的各个父类进行②步骤的搜索和验证过程。
④如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

invokevirtual指令的第一步是在栈中进行的操作,所以已经是运行时的状态了,并不是在类加载过程中了,所以此时就是运行时过程的开始,也即动态了。所以最终两个invokevirtual得到了两种不同的直接引用(即w和m对象的实际类型的sayHello方法),即根据实际类型确定了方法的版本,这也是方法重写的本质,也叫做 动态分派

注意:这里看不太懂可以看完Java栈后再回来,因为这里讲的是方法的调用,而方法的调用是在Java栈中执行的,即所有方法的执行都在Java栈中,而此处说的操作数栈顶也是Java栈的结构。

==>归纳:方法调用的过程,其实就是执行具体的调用指令(invoke开头的指令),而指令的执行过程是需要先确定版本然后再调用,确定版本的方法我们已经知道,有解析和分派两种,在实际运行时,这两种方式是不会分隔的,即解析和分派都会起作用,只不过在确定方法版本的过程中,是先通过解析来确定,确定不了再使用分配确定,就好比静态方法,我们说是解析过程确定的,那么如果静态方法有重载呢?那当然解析过程就确定不了,还是要执行到分派过程的静态分派的时候才能确认,注意理解一下这里的概念,不要有理解偏差。

扩展:动态分派中,我们知道会通过实际类型获得方法的版本,那么哪些方法会作为数据源呢?也就是说比对的时候总要有一个池子吧?没错,JVM不会满内存的找方法区比对,而是我们上边提到的方发表,他只会从方发表中选取一个确定版本的方法再调用,这也是动态分派的底层实现。

方发表的实现:动态分派要想确定一个方法的版本,那肯定是要在运行时,在类的所有方法中进行搜索,以便找到合适的,但是如果这样搜索太消耗性能,毕竟子类中有父类中也有,对应关系不够明确,所以jvm在类被加载到运行时数据区的方法区后,会在当前类的存储区域保存一个虚方法表vtable,也就是此处说的方法表,也会保存一个接口方法表itable(用来为invokeinterface指令使用,和方法表作用相同),把类中的方法全都保存到这个方法表中,这样查找起来就简单的多了。
下边看看方法表的结构:
在这里插入图片描述
Son重写了Father的全部方法,所以Son中的重写的方法的方法表内容都指向了Son并没有指向Father,当然如果有没有重写的方法肯定是要指向Father的,而因为Son和Father都没有重写Object类的方法,所以这些方法都指向了Object类。

方法表的特性
①方法表中存储着各个方法的实际入口地址(即直接引用)
②如果某个方法没有在子类中被重写,那么子类的方发表中的该方法的地址入口和父类中同名方法的保存的地址相同,都是指向父类的实现入口;
③如果某个方法在子类中被重写了,那么子类的方法表中的该方法的地址入口将被替换成指向子类实现版本的入口地址。

==>总结:方法调用即方法版本的确认:
①编译期,确定private、static、final类型的方法,叫解析;
②编译期,也能通过方法静态类型确定重载的方法,叫静态分派;
③运行时期,通过方法表来确定继承关系中的重写方法的版本,叫做动态分派;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/110049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

斩获数亿元B轮融资,这家Tier 1抢跑「L2/L2+」主战场

伴随着汽车智能化演进加速,L2/L2+辅助驾驶功能已经成为各家车企抢夺市场的“要塞”。 据高工智能汽车研究院监测数据显示,今年1-9月前装标配搭载L2级辅助驾驶搭载量为395.19万辆,同比增长69.53%,前装搭载率为27.69%。…

倒角算法推导

推导原理基本很简单: 已知AB, BC两条线段,且交于B点,求倒角半径为 L,AB,BC的倒角 以最短边(假定为AB)长 LAB, 在BC中,以B为起点,找出与LAB同长度…

FOC算法与SVPWM技术

最近看到了FOC这个东西,感觉很有意思,想着以后用这个算法做个东西,目前的想法是用开源的ODrive方案,自己做一个有感单电机驱动的板子,并且加入一点自己的东西,但是这不是目前工作的重点,所以就先…

基于Vue+Element实现的电商后台管理系统的前端项目,主要包括商品管理、订单管理、会员管理、促销管理、运营管理、内容管理

前言 该项目为前后端分离项目的前端部分, 项目介绍 mall-admin-web是一个电商后台管理系统的前端项目,基于VueElement实现。主要包括商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等功能。 完整…

MySQL面试常问问题(锁 + 事务) —— 赶快收藏

目录 1.MySQL中有哪几种锁,列举一下? 2.说说InnoDB里的行锁实现? 3.意向锁是什么知道吗? 4.MySQL的乐观锁和悲观锁了解吗? 5.MySQL 遇到过死锁问题吗,你是如何解决的? 6.MySQL 事务的四大特性说一下…

ChatGPT的各项超能力从哪儿来?万字拆解追溯技术路线图来了

作者:符****尧、彭昊、Tushar Khot、郭志江等**** 符尧(yao.fued.ac.uk),爱丁堡大学 (University of Edinburgh) 博士生,本科毕业于北京大学。他与彭昊、Tushar Khot在艾伦人工智能研究院 (Allen Institute for AI) 共…

jQuery 插件开发

文章目录jQuery 插件开发插件概述常用插件文本溢出:dotdotdot.js单行文本省略多行文本省略延迟加载:lazyload.js插件编写方法类插件函数类插件jQuery 插件开发 插件概述 jQuery插件可以理解成是使用jQuery来封装的一个功能或特效。 一般来说&#xff…

【我亲身经历的2022年软件质量工作】

软件危机(softwarecrisis),20世纪60年代以前,计算机刚刚投入实际使用,软件设计往往只是为了一个特定的应用而在指定的计算机上设计和编制,采用密切依赖于计算机的机器代码或汇编语言,软件的规模…

如何避免编程从入门到放弃?

写代码不是什么太需要创造力的劳动,现在的代码从业者本质上与工业时代的纺织工人没什么差异。大多数人写代码也并不是真的有兴趣,只不过金钱的诱惑使然,这没什么不好,也十分正确。 但对于零基础转行编程的人来说,坚持下…

关于居住办公人口的统计技术解决方案

在数字化转型的浪潮下,大数据产业作为城市数字化转型的重要助力,带来了城市管理手段、模式、理念的深刻变革与创新。为了更好地了解国家城镇的职住分布结构,帮助城市管理部门制定更加合理的规划策略,为城市商业产业提供有效的规划…

面向对象的软件工程

面向对象的软件工程1 面向对象的演化1.1 生活中复杂系统的特点1.2 软件系统的复杂性1.2.1 复杂性的四个方面1.2.1.1 问题域的复杂性1.2.1.2 管理开发的困难性1.2.1.3 软件中的灵活性1.2.1.4 描述离散系统行为1.2.2 复杂系统的五个属性1.2.2.1 层次结构1.2.2.1.1 对象结构1.2.2.…

更改Docker容器网络地址

查看docker 网络列表 查看网络详情 查看容器信息,容器的ip docker inspect 容器id1.创建自定义网络(默认是桥接模式) 方式1: docker network create 网络名称方式2:指定IP和网关 docker network create --subnet17…

Django中继承父模版时子模板未继承父模板中的动态数据

解决办法: 自己定义一个 context_processors.py 其中的内容根据自己需求来: #__author:joy #date: def index(request):meve_data[首页,产品与服务,市场发展,经典案例,解决方案,联系我们]return {"meve_data":meve_data} 值得注意的是 re…

基于R语言、MaxEnt模型融合技术的物种分布模拟、参数优化方法、结果分析制图与论文写作

第一章 理论篇 以问题导入的方式,深 入掌握原理基础什么是 MaxEnt 模型?  MaxEnt 模型的原理是什么?有哪些用途?  MaxEnt 运行需要哪些输入文件?注意那些事项?  融合 R 语言的 MaxEnt 模型的优势&…

Cortex-A55核心板的温升实测!

HD-G2UL系列核心板是万象奥科全新发布的极具性价比产品,搭载64位Cortex-A551.0GHz处理器,搭配1G内存/8G存储售价仅为148元。本文将针对该系列核心板进行温升实测。 1. 测试准备 HDG2UL-IOT开发板,基于HD-G2UL-CORE工业级核心板设计&#xff0…

2022年安徽最新交安安全员考试模拟题及答案

百分百题库提供交安安全员考试试题、交安安全员考试真题、交安安全员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 15.为防止电焊弧光伤害眼睛,应采取的防护方式是使用()。 A.墨镜 B.…

SAP UI5 Smart Table 和 Smart Filter Bar 的联合使用方法介绍

SmartTable 的 _onMetadataInitialised 方法里: 如果标志位 bIsInitialised 已经赋值,说明已经初始化过了,直接返回。 这里说明 SmartTable 有一个自动调整宽度的属性设置:getEnableAutoColumnWidth 拿到 Table view 的metadat…

c站top1全栈接口测试教程 postman接口测试 接口自动化测试全套教程

本文适合已经掌握 Postman 基本用法的读者,即对接口相关概念有一定了解、已经会使用 Postman 进行模拟请求等基本操作,文章末尾也搭配了相应的视频教程,如果喜欢的同学呢可以来个一键三连哈。 工作环境与版本: Window 7&#xff…

0.96寸OLED显示屏介绍续

0.96 寸OLED 显示屏使用方法 (以中景园电子的0.96 寸OLED 显示屏为例) 0.96寸OLED显示屏实物图 七针SPI/IIC 0.96寸OLED显示屏使用方法: 七针SPI/IIC 0.96寸OLED显示屏共有七个管脚,1~7 分别为 GDN、VCC、D0、D1、RES、…

FineReport企业数据图表-JS实现参数控件赋值

1. 概述 1.1 版本 报表服务器版本 功能变更 11.0 -- 1.2 问题描述 参数界面中,往往需要在一个控件中动态的控制其他控件的值,如下图,当 username 有值时,state 自动变为 1,当 username 无值时,state 自…