Java虚拟机之运行时数据区(一)

news2024/11/24 15:03:54

Java虚拟机之运行时数据区

简述

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进行启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

jvm-runtion-area

一. 程序计数器

程序计数器是一块较小的内存空间,它可以看作的当前线程所执行的字节码行号指示器。

1.1. 作用

Java虚拟机中字节码解释器工作时时通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要它。

1.2. 特点

程序计数器有下面两个特点:

  • 线程私有(为了线程切换之后可以恢复到正确的位置,每个线程都需要一个独立的程序计数器,各个线程之间互不影响,独立存储);
  • 不存在内存溢出(这个确实是Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域);

1.3. 注意点

程序计数器在遇到Java方法和本地方法的时候情况不一样:

  • 如果正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
  • 如果正在执行的是本地方法,计数器的值是空(undefind);

二. Java虚拟机栈

2.1. 定义

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法执行的时候,Java虚拟机都会同步创建一个栈帧(栈帧用来存储局部变量表、操作数栈、动态连接、方法出口等信息)。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

2.2. 特点

Java虚拟机栈有下面的特点:

  • 线程私有,和线程的生命周期一致;
  • 这个区域存在两类异常情况:
    • 线程请求的栈深度大于虚拟机所允许的深度(递归操作),抛出StackOverflowError异常;
    • 如果Java虚拟机栈容量可以动态扩容,当无法申请到足够的内存会抛出OutOfMermoryError异常;

2.3. 注意点

关于Java虚拟机栈我们需要注意下面几点内容:

  • 垃圾回收不涉及栈内存;
  • 并不是栈内存分配的越大越好(栈内存越大线程数量越少),可以通过-Xss size指定栈内存大小(Linux/MacOS默认是1024KB);
  • 方法内的局部变量(没有逃离方法作用域)是线程安全的;

三. 本地方法栈

3.1. 定义

本地方法栈和虚拟机栈所发挥的作用相似,区别在于虚拟机栈是虚拟机执行Java方法,本地方法栈执行的本地方法是用native标识的非Java方法,例如Object对象中的一些方法:

public final native Class<?> getClass();
public native int hashCode();

3.2. 注意点

Java虚拟机规范对本地方法栈的语言、使用方法和数据结构没有做任何强制规范。HotSpot虚拟机将本地方法栈和虚拟机栈合并。本地方法栈和虚拟机栈一样会抛出相同的异常:

  • 线程请求的栈深度大于虚拟机所允许的深度:StackOverflowError异常;
  • 栈扩展失败抛出OutOfMermoryError异常;

四. Java堆

4.1. 定义

Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建;这块区域唯一的目的是存放对象实例(new关键字)。

4.2. 特点

Java的特点归纳:

  • 存放实例化对象和数组

  • 有垃圾回收机制

  • 线程共享

  • 堆无法扩容(-Xmx(最大)/-Xms(最小)设置堆大小)会抛出OutOfMermoryError异常;

五. 方法区

5.1. 定义

方法区是线程共享区域,在虚拟机启动时闯将,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码缓冲等数据。如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

方法区可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制;该调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

5.2. 永久代和元空间

逻辑上是堆的一部分,但是不同jvm实现对于方法区的实现不一样。HotspotJDK1.8之前是使用永久代实现,1.8之后使用元空间实现。

永久代使用Hotspot虚拟机设计团队选择把收集器的分代设计扩展到了方法区(也可以是使用永久代实现方法区),但是这样过设计更容造成Java内存溢出。可以通过参数-XX:MaxPermSize来设置方法区的大小。

元空间就是使用本地内存来实现的方法区。

jvm-method-area

变化:

  • JDK1.7前,运行时常量池和字符串常量池是存放在方法区中,HotSpot对方法区的实现称为永久代;
  • JDK1.7中,字符串常量池从方法区移到堆中,运行时常量池保留在方法区中;
  • JDK1.8中,HotSpot移除永久代,使用元空间代替,此时字符串常量池保留在堆中,运行时常量池保留在方法区中,只是实现不一样了,JVM内存变成了直接内存;

5.3. 注意点

方法区和Java堆一样不需要连续的内存、可以选择固定大小和可扩展之外,设置可以不实现垃圾回收机制。

虽然方法区的垃圾收集行为较少,但是也是很有必要。这个区域主要的回收目标是针对常量池的回收和对类型的卸载。

5.4. 运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述类信息之外,还有一项信息是常量池(Constant Pool Table),用来存放编译期生成的各种字面量和符号的引用。

运行时常量池相具有动态性,Java语言并不要求一定只有编译期才能产生,运行期间也可以将新的常量放入池中,例如String类的intern()方法。

运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池内存无法扩容将抛出OutOfMemoryError异常。

5.5. 静态常量池、运行时常量池和字符串常量池

在方法区中有几个概念容易混淆,下面逐一看一下。

Java文件编译之后,没有被加载的class文件的数据被称为静态常量池;但是经过Jvmclass文件装入内存、加载到方法区后,常量池就会变成运行时常量池。对应的符号引用在程序加载或者运行的时候会被转变为被加载到方法区的代码的直接引用,在Jvm调用这个方法的时候,就可以根据这个直接引用找到方法在方法区的位置,然后去执行。**字符串常量池(StringTable)**又是运行时常量池中的一小部分,字符串常量池不同的JDK版本位置有所不同。

5.6. 其他常量池

Java中基本类型的包装类的大部分都在堆中实现了常量池技术(也可以称为对象池)。

其中包括ByteShortIntegerLongCharacterBoolean;但是不包括FloatDouble

注意:Byte、Short、Integer、Long、Character这种整型包装类并不是所值都会用到对象池,只有在[-128,127]之间才可以使用。

这点我们可以在源码种看到:

public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
  	return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}

六. 直接内存

直接内存并不属于虚拟机运行时数据区的一部分。但是这部分确是被频繁使用,而且也可能导致OutOfMemoryError异常。

本机的直接内存的分配是不受到Java堆的大小的限制,但是受到本机总内存大小以及处理器寻址空间的限制,如果动态扩展失败会抛出OutOfMemoryError异常。

七. 字符串常量池

这里我们重点看一下字符串常量池相关知识。

7.1. 字符串的创建

我们创建字符串的方式有下面两种:直接赋值和new创建

7.1.1. 直接赋值

直接赋值的方式,返回的是字符串常量池中的对象引用。我们先看下面的代码。

String a = "hello world";

在执行这一行代码的时候,Jvm会先去常量池判断是否存在相同对象,如果有,则直接返回常量池中的引用,否则需要在常量池中创建一个对象,在返回该对象的引用。

7.1.2. new创建

使用new方法创建会在堆中和字符串常量池(StringTable)创建对象,但是默认返回堆中对象的引用。

String a = new String("hello world");

此段代码执行的时候是这样的,Jvm会先去StringTable中查看是否存在字符串hello world,当不存在的时候会现在字符串常量池中创建一个字符串对象,再去堆中创建一个对象,否则仅在堆中创建对象;这里需要注意new方法创建的字符串都是返回的是堆对象的引用

7.1.3. 实例

看下下面的例子:

  • String a = "hello";“hello”可以在编译器可以确定,此时会先检测字符串常量池中是否存在,存在的话变量a指向已存在的“hello”的地址引用,否则会添加“hello”到字符串常量池中,并返回其地址引用;
  • String b = new String("world");:使用new String创建,会将world存储到字符串常量池中,然后在堆中创建对象返回其堆对象地址的引用
  • String c = "Like" + "Code";:使用字符串拼接,会直接存储“LikeCode”字符串在常量池中返回其地址引用;
  • String d = a + "Tom";:使用字符串拼接(StringBuilderappend),因为a是一个引用,所已无法放入字符串常量池,但是“Tom”可以放入字符串常量池,最终d拿到的是StringBuilder之后的堆中对象的引用;
  • String e = new String("Write") + "Article";:用new String拼接不能再编译期确定,但会将“Write”和“Article”两个字符串存入常量池中,并在堆中创建对象,但是字符串常量不会存放“WriteArticle”这个字符串,除非执行intern方法;
  • String f = new String("Zhang") + new String("San");:用new String拼接不能再编译期确定,但会将“Zhang”“San”两个字符串存入常量池中,并在堆中创建对象,但是字符串常量不会存放“ZhangSan”这个字符串,除非执行intern方法;

7.2. 字符串拼接

字符串拼接,在低版本的JDK中使用的是StringBuilder进行appand之后在toString,在JDK9之后的版本使用StringConcatFactory.makeConcatWithConstants

String a = "hello ";
String b = "world";
String c = a + b; // StringBuidler的append方法
String d = a + "Java"; // StringBuidler的append方法

7.3. intern()方法

这个方法也很重要,存在于class文件中的常量池,被Jvm载入后,是可以进行扩充的,intern方法是为了在扩充常量池的一个方法。

当我们调用这个方法后,Jvm会查找常量池中是否有相同的字符串常量,有的话返回其引用,没有的话就将这个字符串对象的引用地址添加到字符串常量池中并返回。

7.3.1. 常量池中不存在字符串

下面我们看个例子:

// 第一部分
String a = new String("hello ");
String b = new String("world");
String c = a + b
// 第二部分
String d = c.intern();
System.out.println(d == c); // true
System.out.println(d == "hello world"); // true
System.out.println(c == "hello world"); // true

步骤:

  • 第一部分:

    • ab两个变量都会在堆在创建对象,并将helloworld分别放到字符串常量池中;
    • 变量c是使用StringBuidlerappend方法将ab变量字符串组合后new对象放入堆中,并返回堆中对象引用,但是因为ab是字符串引用在编译期无法确定所以不会放入字符串常量池中;
  • 第二部分:

    • 这里使用ntern方法,因为变量c在堆中hello world在字符串常量池中不存在,所以会将hello world放入常量池中,并返回堆中对象的引用,此时dc指向的是同一个对象。
    • 所以当d变量和c变量比较的时候是true
    • d == "hello world"比较的时候,d变量指向的堆中c对象的引用,而常量池中的hello world也会返回c对象的引用,结果是true
    • c == "hello world"同上;

内存结果如下:

jvm-string-table-intern

7.3.2. 常量池中存在字符串

再看一个例子:

String a = new String("hello ") + new String("world");
String b = "hello world";
String c = a.intern();
System.out.println(a == b); // false
System.out.println(a == c); // false
System.out.println(c == b); // true

步骤:

  • new String("hello ")new String("world")两个变量都会在堆在创建对象,并将helloworld分别放到字符串常量池中;变量a是使用StringBuidlerappend方法将helloworld变量字符串组合后堆中创建对象并返回对象引用;
  • 变量b是将hello world的放入字符串常量池中返回其在常量池中的引用
  • 执行a.intern()时,此时常量池中已经存在hello world了,这是返回的是常量池中hello world的引用;
  • a == b此时变量a持有的是堆中对象的引用,变量b持有的字符串常量池中引用,所有a != b;
  • a == c此时变量c持有的是字符串常量池中hello world的引用,所以a != c
  • c == b原理如上;

内存结果如下:

jvm-string-table-intern-2

7.4. 垃圾回收

字符串常量池也是可以被回收的,下面我们通过一些参数配置观察一下!

参数注释
-Xmx10m堆空间大小
-XX:+PrintStringTableStatistics打印串池统计信息
-XX:+PrintGCDetails打印GC日志详情
-verbose:gc打印GC日志

完整参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

jvm-string-table-gc-1

示例代码:

public class Example {
    public static void main(String[] args) {
        String name = "hello world";
        System.out.println(name);
    }
}

运行之后输出的串池统计结果如下:

StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1690 =     40560 bytes, avg  24.000
Number of literals      :      1690 =    152448 bytes, avg  90.206
Total footprint         :           =    673112 bytes

接着循环往串池中写入字符串:

public class Example {
    public static void main(String[] args) {
        for (int i = 0; i < 3000; i++) {
            new String("hello world" + i).intern();
        }
    }
}

此时会发生GC,串池中的字符个数减少:

[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->736K(9728K), 0.0007925 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
...
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      2145 =     51480 bytes, avg  24.000
Number of literals      :      2145 =    184960 bytes, avg  86.228
Total footprint         :           =    716544 bytes
Average bucket size     :     0.036
Variance of bucket size :     0.036
Std. dev. of bucket size:     0.191
Maximum bucket size     :         3

7.5. 调优处理

字串常量池是使用HashTable实现的(无法扩容),数据结构是数组+链表实现的。字符串hash值是key,地址是value,数组数量少(也可以说桶数量少),hash碰撞次数会增加,碰撞之后会使用链表处理hash冲突。Jvm中桶最小是1009,默认是60013。这里我们可以使用-XX:StringTableSize=1009进行配置。

可以到我另一篇文章中看哈希表:https://blog.csdn.net/yhflyl/article/details/121245579

下一篇见!

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

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

相关文章

房子装修有哪些注意事项要注意?

房子装修有哪些注意事项要注意&#xff1f;这里的东西太多了&#xff0c;可能三天三夜都搞不清楚&#xff0c;而且对于初次装修的业主来说&#xff0c;很多专业术语都是一头雾水&#xff0c;所以本文就简单介绍一下&#xff0c;从装修到装修&#xff0c;以及需要注意的几点。 房…

【深度学习】实验4答案:脑部 MRI 图像分割

DL_class 学堂在线《深度学习》实验课代码报告&#xff08;其中实验1和实验6有配套PPT&#xff09;&#xff0c;授课老师为胡晓林老师。课程链接&#xff1a;https://www.xuetangx.com/training/DP080910033751/619488?channeli.area.manual_search。 持续更新中。 所有代码…

数据之道读书笔记-05面向“联接共享”的数据底座建设

数据之道读书笔记-05面向“联接共享”的数据底座建设 在从信息化向数字化转型的过程中&#xff0c;企业积累了海量的数据&#xff0c;并且还在爆发式地增长。数据很多&#xff0c;但真正能产生价值的数据却很少。数据普遍存在分散、不拉通的问题&#xff0c;缺乏统一的定义和架…

[发送AT指令配置a7670C模块上网]

发送AT指令配置a7670C模块上网内核配置调试验证应用的编写AT的指令的使用内核配置 概要&#xff1a;基于rv1126平台调试SIMCOM 7670C 4G模块。 1&#xff0c;内核配置及内核代码修改 1.1按照芯片data sheet修改kernel代码&#xff0c;添加PID和VID。 此处根据不同的芯片按照其…

Mybatis plus注解@TableField详解

Mybatis plus注解TableField详解 目录1. 前言2. 应用场景2.1 更新null值2.2 查询时排除字段2.3 非数据库字段属性1. 前言 TableField 字段注解&#xff0c;该注解用于标识非主键的字段。将数据库列与 JavaBean 中的属性进行映射。 2. 应用场景 2.1 更新null值 想要在更新字…

auth模块方法的使用

文章目录1、创建超级用户(管理员)2、获取表&#xff0c;检验密码3、保存用户状态4、获取用户对象&#xff0c;校验用户是否登录5、验证用户是否登录6、修改密码7、注销8、注册9、方法总结10、如何扩展auth_user表1、创建超级用户(管理员) """ 在创建好一个djan…

医保医用耗材编码目录——在线查询

医保医用耗材编码目录是按照“统一分类、统一编码、统一维护、统一发布、统一管理”的总体要求下进行更新维护&#xff0c;旨在强调“分类”与“追溯”属性&#xff0c;助力将医保编码标准统一为新时期医保信息交换的通用语言。 对于医保医用耗材分类与代码数据的“分类”与“追…

高数 | 【数一】 多元函数积分学 —— 总复习框架总结

自用复习笔记。 整理参考于 2023张宇高数18讲、武忠祥十七堂课。 三重积分(质量) 概念与对称性计算 直角坐标系 先一后二法/先z后xy法/投影穿线法(柱体,侧面为柱体)先二后一法/先xy后z法/定限截面法(旋转体)柱面坐标系 = 极坐标下二重积分与定积分球面坐标系 应用 体积总…

开源版禅道的使用教程

文章目录一、禅道简介二、下载安装三、敏捷版基本功能使用四、其他版本使用一、禅道简介 1.官网介绍&#xff1a;国产开源项目管理软件。核心管理思想基于敏捷方法scrum。集产品管理、项目管理、质量管理、文档管理、组织管理和事务管理于一体。 scrum&#xff1a;迭代式增量软…

DHCP地址池耗尽攻击

DHCP地址池耗尽攻击 攻击原理简介 ​ 路由器内置的DHCP服务器&#xff0c;划定了一个地址池作为自动分配给接入终端号IP地址的范围。 ​ 攻击工具伪装成大量的接入主机&#xff0c;向路由器内的DHCP服务器请求大量的IP地址分配。 ​ 在DHCP的地址租期超时之前&#xff0c;已经分…

CentOS 7搭建Yunzai-Bot原神机器人

CentOS 7.6搭建Yunzai-Bot原神机器人 前言 目前使用较多的原神机器人&#xff1a; LittlePaimon ✨基于NoneBot2和go-cqhttp的原神Q群机器人 原神多功能机器人&#xff0c;查询游戏信息、图鉴攻略、树脂提醒等等&#xff0c;以及各种各样的好玩的功能&#xff0c;不仅仅是原神…

行业案例 | 睿眼攻击溯源组合拳让黑客攻击事件无所遁形

项目背景 近年来,网络安全形势愈发严峻&#xff0c;黑客入侵、信息泄露等信息安全事件层出不穷&#xff0c;给企业带来了巨大的经济损失。一是互联网出口应用多为Web应用&#xff0c;有效防护和监测Web应用的安全性是金融行业客户信息安全领域的一项重点工作&#xff1b;二是随…

C++11 lambda+包装器+可变参数模板

索引lambda表达式(1).什么是lambda(2).lambda基本规则(3).lambda实现原理包装器可变参数模板lambda表达式 (1).什么是lambda 假设有这样一个类 struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 }&#xff1b;现在要将商品分别按照名字…

“健康中国”战略下如何推进公共卫生建设,海尔生物医疗给出“智慧答案”

【潮汐商业评论/原创】 公共卫生是一个老话题&#xff0c;但在新时代的背景下正在呈现出诸多新故事。在人民健康需求日益提升的当下&#xff0c;推动公共卫生服务体系的高效、便捷升级成了新时期的一大命题&#xff0c;而这一问题的答案则指向了公共卫生需要“数智化”。 在此…

Python之第十章 IO及对象列化

目录 Python之第十章 IO及对象列化 1.IO流&#xff08;IO stream&#xff09; 1.概述 2.IO流定义 3.流的分类 2.open方法 1.过程 2.缓冲区&#xff08;buffer&#xff09; 使用缓存区的必要性&#xff1a; 缓冲区分类&#xff1a; 3.格式 4.b模式 5.模式 6.文件对…

CentOS 7搭建LittlePaimon原神机器人

CentOS 7.6搭建LittlePaimon原神机器人 前言 最近小伙伴说别人的QQ群里有个原神的机器人&#xff0c;可以随时查询自己账号的角色卡信息。然后我自己查了下资料&#xff0c;发现不是很难弄&#xff0c;所以帮忙也弄了一个。 目前使用较多的原神机器人&#xff1a; LittlePaim…

SpringMVC学习篇(八)

SpringMVC拦截器 1.拦截器和过滤器的区别 过滤器拦截器servlet规范中(java ee)规范中的一部分,任何java web工程都可以使用拦截器是框架提供的,如只有在SpringMVC框架下的工程才能使用其提供的拦截器在url-pattern中配置了/*之后,可以拦截任何一切资源拦截器只会拦截控制器方…

easyrecovery工具2023最新版一键恢复丢失数据免费下载

通常&#xff0c;许多人会将工作或生活中的数据存储在我们的计算机上。很多时候&#xff0c;由于我们的误操作或其他一些问题&#xff0c;很容易错误地删除一些文件和数据。特别是&#xff0c;一些计算机故障总是会导致数据丢失&#xff0c;这是非常麻烦的。当需要重新安装系统…

JS 对象总结

对象 创建对象 有两种方式&#xff1a; 通过 new 操作符实例化一个对象&#xff0c;再添加属性。 let person new Object(); person.name "孤城浪人"; person.sayName function() { console.log(this.name); };构造函数&#xff0c;若不需要传参&#xff0…

代码随想录——单词接龙(图论)

题目 字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列&#xff1a; 序列中第一个单词是 beginWord 。 序列中最后一个单词是 endWord 。 每次转换只能改变一个字母。 转换过程中的中间单词必须是字典 wordList 中的单词。 给你两个单词 b…