【jvm系列-05】精通运行时数据区共享区域---方法区_jvm的方法区是共享的嘛

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

在《深入理解Java虚拟机》这本书中,对方法区存储内容的概述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。 但是随着JDK的不断迭代,其内存存储的东西也会有着稍小的变化。

类型信息:对加载的类型,包括类class、接口interface、枚举enum、注解annotation等,JVM必须在方法区中存储以下类型

♟ 这个类型的完整有效名称,全名就是 包名.类名

♟ 直接父类的完整有效名,interface和Object都是没有父类的

♟ 这个类型的修饰符,如public、static、final、abstract

♟ 直接接口的一个有序列表,如这个类可能实现多个接口

属性信息:域的相关信息主要包括以下:域名称、域类型、域修饰符等

方法信息:方法信息主要包括一些方法名称、返回类型、参数的数量和类型、方法的修饰符、方法的字节码、局部变量表以及其大小、异常表等

4.2,static final

在静态变量中,一般是随着累的加载而加载,他们成为类数据在逻辑上的一部分,并且类变量被类所有的实例共享,即使类实例不存在也可以进行访问。

而被声明的final的类变量的处理方法则不同,该变量在编译阶段就会被分配了;而没有被声明final的类变量,在准备阶段进行初步的赋值,在初始化阶段进行一个最终的赋值。

如写一个Java测试类,定义一个被final修饰的类变量和不被final修饰的类变量,并且这个类型为基础数据类型

public class Test {
    public static final int i = 10;
    public static  int k = 20;

    public static void main(String[] args) {
        System.out.println(i+k);
    }
}

然后在编译好的文件中,输入反编译命令,并将最终输出的文件加载到zhs.txt文件中

javap -v -p Test.class > zhs.txt

接下来重点分析这两个变量,可以发现这个加了final修饰的i,在编译阶段就进行了分配,赋值为10;而没有加final修饰的k,在编译阶段没有赋予默认值,而是在准备阶段赋值的默认值,在初始化阶段赋予的最终值

public static final int i;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: int 10

public static int k;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC

4.3,运行时常量池
4.3.1,什么是常量池

一个有效的字节码文件除了包含类的版本信息、字段、方法以及接口等描述信息之外,还包含一项重要的信息,那就是常量池表(Constant Pool Table),其中包括各种字面量和对类型,字段和方法的符号引用,如下

Constant pool:
   #1 = Methodref          #6.#27         // java/lang/Object."<init>":()V
   #2 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #30            // com/fc/v2/util/Test
   #4 = Fieldref           #3.#31         // com/fc/v2/util/Test.k:I

一个Java源文件在编译之后会产生一个字节码文件,而字节码文件需要数据支持,通常这种数据量很大不能直接存储到字节码文件里面,因此就通过常量池的方式,提前将数据存储在常量池中,然后根据引用去获取对应的数据,如在栈帧的 动态链接 就是通过这种方式来获取数据的。

在常量池中存储的数据类型主要有:数量值,字符串值,类引用,字段引用,方法引用

常量池就可以看做成是一张表,虚拟机指令根据这张常量表找到执行的类名、方法名、参数类型和字面量等类型。

4.3.2,什么是运行时常量池

上面提到了常量池,常量池是字节码文件的一部分,用于存放编译期生成的各个字面量和符号引用,这部分内容在类加载之后存放到方法区的运行时常量池中;而运行时常量池是属于方法区的一部分,接下来详细的描述一下上面是运行时常量池

⚽ 在将类和接口加载到虚拟机之后,就会创建对应的运行时常量池

⚽ JVM会为每个已加载的类型都维护一个常量池,池中的数据可以通过索引访问

⚽ 运行时常量池包含多种不同的常量,包括编译期就已经明确的数值,如栈帧的大小,以及运行期间才能获取到的方法或者字段引用,此时不再是常量池中的符号地址#,而是具体的真实地址。

⚽ 运行时常量池具备动态性,如实际大小可能比计算的大小大

⚽ 当创建接口或者类的运行时常量池时,如果构造的运行时常量池所需要的内存空间超过了方法区所能提供的最大值,则JVM就会抛出OutOfMemoryError异常

5,方法区的使用

接下来查看一段简单的代码,如下

/\*\*
 \* @author zhenghuisheng
 \* @date : 2023/4/4
 \*/
public class Test {
    public static void main(String[] args) {
        int x = 500;
        int y = 100;
        int a = x/y;
        int b = 50;
        System.out.println(a+b);
    }
}

然后编译之后,通过jclasslib插件查看对应的字节码,在前面的章节又讲如何安装使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uBRWlWCU-1680600889287)(img/1680577628874.png)]

其对应的字节码文件如下

 0 sipush 500
 3 istore_1
 4 bipush 100
 6 istore_2
 7 iload_1
 8 iload_2
 9 idiv
10 istore_3
11 bipush 50
13 istore 4
15 getstatic #2 <java/lang/System.out>
18 iload_3
19 iload 4
21 iadd
22 invokevirtual #3 <java/io/PrintStream.println>
25 return

这个具体的操作流程如下,由于没有new对象,因此在下图中暂时先不展示堆空间。

首先程序计数器的记录的地址为0,并将这个500这个数值压入到操作数栈中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rtwA3FrZ-1680600889288)(img/1680577383605.png)]

然后就是这个istore_1,将操作数栈中存储的值存储到本地变量表中。而这个本地变量表,如果是实例方法或者是构造方法,第一个数据存储的应该是this,而这里的是static的静态方法,因此第一个slot存储的不是this。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lRt7QCtH-1680600889288)(img/1680578174090.png)]

后续的操作和上面的一样,这个iload_1就是将数据从本地变量本中取出来放到操作数栈顶,iload_2原理一样,然后结果这个idiv除法运算,将结果5存放在本地变量表3的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4JeLuyz6-1680600889289)(img/1680578487922.png)]

随后就是将50入栈,也存储到本地变量表中4的位置,然后通过这个getstatic #2,就是获取常量池中的#2的位置,然后最终可以获取到这个System类,out类和对应的type属性,然后加载到方法区中。如果这些类或者属性在方法区中存在就不会进行加载,如果不存在就会将这些加载到方法区中,然后将这些#等符号的间接引用变成地址的直接引用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgTqNNY1-1680600889289)(img/1680578804544.png)]

然后通过这个iload3和iload4将本地变量表的数据加载到操作数栈中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bqWqPPVX-1680600889290)(img/1680579228326.png)]

然后再经过iadd计算,再调用这个invokevirtual #3的符号引用,最终可以定位到一个打印操作,最终return结束

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cq8gc5s-1680600889290)(img/1680579346070.png)]

通过上图可以发现,无论是在哪一个操作,程序计数器都会指向对应的执行位置。

6,方法区的演进细节

在虚拟机中,只有HotSpot虚拟机才有永久代,JRockit和J9是不存在永久代的概念的。在HotSpot虚拟机的方法区变化如下(方法区是一种概念,永久代和元空间属于具体实现):

  • 在jdk6及以前:有永久代,静态变量存放在永久代上面
  • 在jdk7中:有永久代,但逐步去除,字符串常量池、静态变量保存在堆中
  • 在jdk8及以后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间中,字符串常量池和静态变量在堆中
6.1,方法区的具体实现以及内部组成的演进

jdk6的方法区的组成如下,也称为永久代,其静态变量,运行时常量池,字符串常量池等都是保存在这个永久代的里面,并且字符串常量池是属于运行时常量池的一部分

在这里插入图片描述

而在jdk7开始,就将字符串常量池和静态变量存放在堆里面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YV3PdXgK-1680600889291)(img/1680591492386.png)]

而从jdk8开始,永久代已经不存在了,取而代之的是本地内存的 元空间,会将运行时常量池,类信息等全部存储在本地内存中,并且静态变量和字符串常量池都是存储在堆中

在这里插入图片描述

在官方文档中https://openjdk.org/jeps/122,也提到过这个删除这个永久代的动机,如下,主要就是说参考了这个 JRockit 内部的实现,这样用户可以不必自己去配置这个永久代。这样这块空间可以不用jvm本身去管理,从而交给本地内存区实现。

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

其实这项改动还是很有必要的,接下来从两个方面来说明为啥要替换

  • 永久代的空间大小设置是很难确定的,在某些场景下,如果动态的加载类过多,就很容易出现OOM,比如一些Web项目。而元空间和永久代之间最大的区别在于:元空间不在虚拟机中,而是使用的是本地内存,因此元空间的大小只受本地内存限制
  • 永久代的调优比较困难
6.2,字符串常量池为何要存储到堆中

在jdk7的时候,将静态变量和字符串常量池都存储到了堆中,其主要原因是在永久代中,其触发的回收效率很低,在full gc的时候才会触发。而full gc的老年代空间不足,或者永久代空间不足时才会触发,因此这就导致了这个字符串常量池的回收率不高。

而在如今的开发中,可能会创建大量的字符串,如果还是存储在方法区内部,那么其回收率会比较低,很容易导致永久代的内存不足,因此选择将这个字符串常量池存放到堆中,这样就可以快速的实现内存回收

7,方法区的垃圾回收机制

在运行时数据区中,方法区又被称为non-heap,就是非堆的意思。并且在《java虚拟机规范》中描述,对方法区中的约束是非常宽松的,提到过不要求虚拟机在方法区中实现垃圾回收

一般来说这个区域的回收效果比较难令人满意,但是有时又确实是有必要的,因此在HotSpot虚拟机中,方法区的垃圾回收主要是回收两部分内容:常量池中废弃的常量和不再使用的类型

常量回收的策略就是只要该常量没有被任何地方引用,就可以被回收,回收废弃常量和回收Java堆的对象非常类似

类型回收的条件相对比较苛刻,需要同时的满足以下三点条件:

  • 该类的实例被回收,该类以及对应的子类在堆中不存在

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

存中…(img-pWFKktVU-1715701108312)]
[外链图片转存中…(img-u0pj5Wau-1715701108312)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值