JVM——StringTable面试案例+垃圾回收+性能调优+直接内存

JVM——引言+JVM内存结构_北岭山脚鼠鼠的博客-CSDN博客

书接上回内存结构——方法区。

这里常量池是运行时常量池。

方法区

面试题

intern()方法 

intern() 方法用于在运行时将字符串添加到内部的字符串池stringtable中,并返回字符串池stringtable中的引用。

返回值

当调用 intern() 方法时,如果字符串池中已经存在相同内容的字符串,则返回字符串池中的引用;否则,将该字符串添加到字符串池中,并返回对字符串池中的新引用。

StringTable_常量池与串池的关系

从字节码和常量池角度看。

准备如下一个类

package test;

public class test {

    public static void main(String args[])   {
        String s1="a";
        String s2="b";
        String s3="ab";

    }
}

在命令行先用javac test.java编译出字节码文件,然后用javap -v test.class显示反编译内容

如果没有生成LocalVariableTable,可以通过这条命令 javac -g:vars xxxxxx.java

看见main方法部分的指令中第一条是ldc #2,到常量池中2号位置加载一个信息,该信息有可能是常量,也有可能是对象的引用。这里加载了一个字符串对象a。

 astroe_1将a存入局部变量表中的1号位置,astore将b存入2,astore将ab存入3。

局部变量表是栈帧的东西。

常量池最初存在于字节码文件,运行时会被加载到运行时常量池,Constant Pool中的符号会被加载到运行时常量池。

//常量池中的信息,都会被加载到运行时常量池中,这时a,b,ab都是常量池中的符号,还没有变为java 字符串对象,也还没有加入到stringtable中。

ldc #2会把a符号变为“a”字符串对象,然后会拿着“a”去StringTable串池里面找有没有取值相同的key,没有的话会把“a”放入stringtable并返回引用,有的话会使用串池中的a对象的引用。

每个字符串对象都不是事先放在串池里面,而是用到的时候才开始创建,懒加载。

这里容易混淆,stringtable=字符串常量池=串池,是在堆中的一块区域,常量池是另一个东西,是内存元空间中的一块区域.(1.8)

 StringTable_字符串变量拼接

public class test {

    public static void main(String args[])   {
        String s1="a";
        String s2="b";
        String s3="ab";
        String s4=s1+s2;
    }
}

加上这个字符串拼接的代码之后再次反编译后。

s4那里创建一个StringBuilder对象并调用其init方法,()V表示调用无参构造。

aload_1表示准备好了s1这个参数,astore_1是存入了局部变量表。

然后调用了append方法,最后那里调用了一个toString方法。

s1+s2    =    new StringBuild().append("a").append("b").toString();

astore 4是将上面toString得到的新的String对象("ab")存入s4.

 然后在这里

输出一个false,s3是在串池中的,s4是引用一个新的字符串对象,值一样,s3在串池,s4在堆里面,位置不同,是两个对象,输出false.

  StringTable_编译期优化

public class test {

    public static void main(String args[])   {
        String s1="a";
        String s2="b";
        String s3="ab";
        String s4=s1+s2;  //new StringBuild().append("a").append("b").toString()
        String s5="a"+"b"; //这就是javac找编译期的优化,结果已经在编译期确定为ab

        System.out.println(s3==s5);

    }
}

编译之后如下

s5="a"+"b"对应的是ldc #4,直接在字符串常量池找到已经拼接好的“ab”,并存入了局部变量表5号中

String s3="ab"也是到常量池中找“ab”.

区别就是s3没有找到ab对象并加了进去,s5找到并获取了s3存入的ab对象,因此两个获得的是同一个对象。

因此s3==s5结果为true.

简单说,就是**new出来的放在堆里**,**直接赋值的放在串池里**,**虽然串池也在堆里**,**但是区域不同(地址不同)**,而==比较的是地址,因此false(s3==s4时)。

 

 StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

intern_1.8

public class test {

    //["a","b"]
    public static void main(String args[])   {
        String s=new String("a")+new String("b");//new String("ab")=new StringBuild().append(a).append(b).toString()

        //堆中 new String("a")  new String("b") new String("ab")
        String s2=s.intern();  //将字符串对象尝试放入串池,如果有则不会放入,并将串池中的对象返回给s2。
        System.out.println(s2=="ab");  //true
        System.out.println(s=="ab");     //true
    }
}

上面这段代码中s=new ....已经在堆里放了a,b,ab,在串池放了a,b.

s.intern()则是把s在堆中的地址也放入了串池,因此s2在串池取到的“ab”和堆中的“ab”地址是相等的。

public class test {

    //["a","b"]
    public static void main(String args[])   {

        String x="ab";
        String s=new String("a")+new String("b");//new String("ab")=new StringBuild().append(a).append(b).toString()

        //堆中 new String("a")  new String("b") new String("ab")
        String s2=s.intern();  //将字符串对象尝试放入串池,如果有则不会放入,并将串池中的对象返回给s2。
        System.out.println(s2==x);  //true
        System.out.println(s==x);   //false
    }
}

 这一段代码里面,ab早已在串池,s.intern()没有放进去,于是s2取到了串池中的“ab”,而不是堆中的“ab”.

intern_1.6

  • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
  • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份创建个新的,放入串池,并会把串池中的对象返回

 下面的代码在1.8是true,true,在1.6是true,false

public class test {

    //["a","b"]
    public static void main(String args[])   {

        String s=new String("a")+new String("b");//new String("ab")=new StringBuild().append(a).append(b).toString()

        //堆中 new String("a")  new String("b") new String("ab")
        String s2=s.intern();  //将字符串对象尝试放入串池,如果有则不会放入,并将串池中的对象返回给s2。

        String x="ab";
        System.out.println(s2==x);  //
        System.out.println(s==x);   //
    }
}

面试题

public class test {
    public static void main(String args[])   {
        String s1="a";
        String s2="b";
        String s3="a"+"b";//串池中ab
        String s4=s1+s2;//堆里new String("ab")
        String s5="ab"; //应用串池ab
        String s6=s4.intern();  //串池已有,获得串池ab
//问
        System.out.println(s3==s4);  //false
        System.out.println(s3==s5);  //true
        System.out.println(s3==s6);  //true

        String x2=new String("c")+new String("d"); //堆中cd
        String x1="cd";     //串池中cd
        x2.intern();        //尝试入池失败
//问 ,如果调换了x1,x2位置呢?如果是jdk1.6呢?
        System.out.println(x1==x2);  //  false
    }
}

String x1=“cd”和x2.intern调换之后就是true,x1直接用串池中的ab.

调换之后换成jdk1.6的话,也是false,串池中是x2的副本,不是x2故x1从串池取到的不是x2.

StringTable_位置

1.6串池在常量池中,1.8串池去到了堆中。

原因:

永久代内存回收效率很低,永久代需要fullGC时才会触发永久代的垃圾回收,而fullGC只有在老年代空间不足时才会触发,触发时间晚,间接导致串池回收效率不高。

1.8在堆中只要Minor GC就会触发StringTable串池的垃圾回收。

案例

1.6下报错

1.8下报错

并不是堆内存不足报错

 原因:

-XX:+UseGCOverheadLimit是打开

-XX:-UseGCOverheadLimit是关闭

 1.8中,如果98%的时间花在垃圾回收上但只回收了2%的堆空间,则jvm已经无药可救,便不会再尝试垃圾回收,会报错OverHead Limit。

 关闭后1.8报错

堆空间不足

 证明了1.8串池用的堆空间,1.6串池用的永久代。

StringTable_垃圾回收

两个参数,一个是打印字符串表的统计信息,可以看见串池中字符串实例个数以及占用大小

一个是打印垃圾回收详细信息,次数和回收时间。

 串池统计信息,底层采用哈希表实现,数组加链表结构。

数组个数成为桶buckets,下图默认是60013个,entries键值对个数是1754个,literals字符串常量也是1754个。总占用Total footprint.

 修改代码如下

在try中加个循环不断往串池加入新对象。

运行后可以看见多了100个字符串对象进StringTable 

 

 加多循环次数

 此时只有7000多个。

 由于内存空间分配失败触发了一次垃圾回收。

minorGc新生代垃圾回收。

 这里证明了StringTable也是会发生垃圾回收的。

StringTable性能调优

  • 调整-xx:StringTableSize=桶个数

串池底层是哈希表,性能跟大小相关,桶的个数多,碰撞几率低,查找速度快,桶的个数较少的话,碰撞几率更高,导致链表较长,查找速度变慢。

这里调优就是调整桶的个数,下图中使用参数设定了桶的个数为20万个。

 

上面是用BufferReader读取了单词表48万个单词,然后全部放入串池中。上面输出了入池所花费的时间。

花费时间0.4s.

 统计信息可以看见桶个数有20万个。平均每个桶两个单词多。因此效率较高。

 在去除20w的参数后花费时间变多,默认桶个数是6万

 桶的个数最小是1009,设置为1009后再次测试变得更慢了。

 系统里字符串常量个数较多就可以调整桶的个数变大,使其有更好的分布,减少冲突,使串池效率提升。

StringTable_性能调优案例

  • 调整-xx:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

网络段子:

推特要存储用户地址在内存中,但是堆内存不够大并且很多内存重复,解决方法就是采用了字符串的intern()方法,可以去除重复地址。

在上面的代码基础上改动。

 循环读取10次48w个单词,使其出现10次重复的情况。

使用一个生命周期更长的List<String>中防止被垃圾回收。这里先不入池。并且在开始前和结束后都有一个输入。

使用Java VisualVM查看信息

读取文件前

char和String有个11mb左右

读取后char和String有300mb,占了80%多。

 修改代码

在添加到list集合之前先入池,然后返回值是串池中的字符串对象。串池外的会被垃圾回收。

 这一次的内存占用只剩30%多,一共也就30多mb.与之前的内存差异明显。

  案例证明如果引用里面有大量的字符串且字符串可能存在重复问题可以让字符串入池减少字符串对象个数,减少堆内存的使用。

直接内存

直接内存不属于java虚拟机内存管理,而是属于系统内存,是操作系统的内存。

定义

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JM 内存回收管理

基本使用

NIO的 directByteBuffer

使用directBuffer比用传统阻塞IO 传输效率快很多。

 

传统IO操作的过程。 

 java本身没有磁盘读写能力,需要调用操作系统提供的函数。

切换到内核态之后就可以有cpu函数读取磁盘文件内容,读取进来后会在系统内存中划出一块系统缓冲区,系统缓冲区java代码不能运行,因此java代码会在堆内存分配一块java缓冲区,对应代码里new的byte[]数组,要读取数据还要将其从系统缓冲区读入到java缓冲区。

然后到了下一个用户态再去调用输入流的写操作,反复进行读写。

这里数据经过了两次复制,造成效率较低。

directByteBuffer的过程

 这里直接在操作系统划出了一块缓冲区,这里java代码可以直接访问。两边的代码共享的一个区域。复制次数少了一次,效率提高。

内存溢出

这个直接内存的特点还有不受JVM内存回收管理。

 上面这个代码每次分配100mb内存。

然后就把报错了直接内存溢出,循环了36次,3.6G.

 分配和释放原理

  • 使用了Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • BvteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 BvteBuffer 对象,一旦 BteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

通过一个Unsafe对象进行管理的,而不是垃圾回收

 运行之后可以看见进程占用了一个G的内存。

 然后置为null之后,1个G的内存被回收

原理说明 

在java较为底层的一个Unsafe类可以做到分配直接内存和释放直接内存。一般都是jdk内部自己使用的

分配内存那个方法返回了一个long类型的地址,下面就可以用这个地址来进行内存的释放。

分配之后 

 释放之后

 上面说明了直接内存的分配和释放是通过一个Unsafe对象进行管理的,而不是垃圾回收。

 源码分析

 在源码里面可以看见调用了分配内存的方法。

在cleaner的create方法参数中的一个回调任务对象Deallocator。

在该方法中可以看见Unsafe释放内存的方法。

直接内存的释放借助了虚引用的机制。

意思就是JAVA对象被回收,触发直接内存回收

禁用显式回收对直接内存的影响

  • 使用了Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • BvteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 BvteBuffer 对象,一旦 BteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

在做jvm调优时经常会用的一个参数

-XX:+DisableExplicitGC  禁用显式垃圾回收 

systemgc()触发的是fullGC,对新生代和老年代都会回收,造成程序暂停时间较长

然后运行之后System.gc()会失效,直接内存无法被回收,ByteBuffer只能等到真正的垃圾回收才会被清理。造成直接内存长时间占用较大。 

 解决方案

直接使用unsafe对象进行直接内存的释放。

手动管理

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Git命令详解

1 常用命令 1&#xff09;初始化本地仓库 git init <directory> 是可选的&#xff0c;如果不指定&#xff0c;将使用当前目录。 2&#xff09;克隆一个远程仓库 git clone <url> 3&#xff09;添加文件到暂存区 git add <file> 要添加当前目录中的所…

Open3D 最小二乘拟合平面(SVD分解法)

目录 一、算法原理二、代码实现三、结果展示1、点云2、拟合结果四、优秀博客本文由CSDN点云侠原创,原文链接。爬虫网站自重。 一、算法原理 本文实现矩阵奇异值分解方法的最小二乘拟合平面。原理如下: 对于得到的 n n

在Visual Studio上,使用OpenCV实现人脸识别

1. 环境与说明 本文介绍了如何在Visual Studio上&#xff0c;使用OpenCV来实现人脸识别的功能 环境说明 : 操作系统 : windows 10 64位Visual Studio版本 : Visual Studio Community 2022 (社区版)OpenCV版本 : OpenCV-4.8.0 (2023年7月最新版) 实现效果如图所示&#xff0…

【BASH】回顾与知识点梳理(三十一)

【BASH】回顾与知识点梳理 三十一 三十一. 进程的管理31.1 给进程发送讯号kill -signal PIDlinux系统后台常驻进程killall -signal 指令名称 31.2 关于进程的执行顺序Priority 与 Nice 值nice &#xff1a;新执行的指令即给予新的 nice 值renice &#xff1a;已存在进程的 nice…

uniapp微信小程序消息订阅快速上手

一、微信公众平台小程序开通消息订阅并设置模板 这边的模板id和详细内容后续前后端需要使用 二、uniapp前端 需要是一个button触发 js&#xff1a; wx.getSetting({success(res){console.log(res)if(res.authSetting[scope.subscribeMessage]){// 业务逻辑}else{uni.request…

软考A计划-系统集成项目管理工程师-法律法规-上

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

HLK-LD105/2410B/2420模块测试

HLK105/2410B/2420模块测试 &#x1f4cc;模块资料地址&#xff1a;https://h.hlktech.com/Mobile/download &#x1f33f;HLK-LD105模块&#xff1a; 10G微波雷达 &#x1f33f;HLK-LD2420-24G&#xff1a;24G毫米波雷达 &#x1f33f;HLK-LD2410B-24G&#xff1a;24…

stm32红绿灯源代码示例(附带Proteus电路图)

本代码不能直接用于红路灯&#xff0c;只是提供一个思路 #include "main.h" #include "gpio.h" void SystemClock_Config(void); void MX_GPIO_Init(void) {GPIO_InitTypeDef GPIO_InitStruct {0};/* GPIO Ports Clock Enable */__HAL_RCC_GPIOB_CLK_ENAB…

webshell绕过

文章目录 webshell前置知识进阶绕过 webshell 前置知识 <?phpecho "A"^""; ?>运行结果 可以看到出来的结果是字符“&#xff01;”。 为什么会得到这个结果&#xff1f;是因为代码的“A”字符与“”字符产生了异或。 php中&#xff0c;两个变…

MTK Android非常用分辨率修改充电动画

非标准分辨率的屏,配置MTK Android的关机充电动画. 环境 芯片 MTK 系统 Android 服务器 ubuntu 屏幕分辨率356*400,不是常见的分辨率. 原始充电动画显示异常,画面扭曲. 方法 确定使用的图片 vendor/mediatek/proprietary/bootable/bootloader/lk/dev/logo 这个目录下…

[GitOps]微服务版本控制:使用ArgoCD 部署Grafana Loki

背景介绍 请回答&#xff1a;你们是如何保证线上部署的服务&#xff0c;从服务版本到参数配置&#xff0c;都是和测试通过的版本是一致的呢&#xff1f; 本文将介绍GitOps的基本原理以及ArgoCD的使用&#xff1a;ArgoCD部署Grafana Loki 到k8s集群。 本文项目地址&#xff1…

Hugging News #0814: Llama 2 学习资源大汇总

每一周&#xff0c;我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新&#xff0c;包括我们的产品和平台更新、社区活动、学习资源和内容更新、开源库和模型更新等&#xff0c;我们将其称之为「Hugging News」。本期 Hugging News 有哪些有趣的消息&#xff0…

06_布隆过滤器BloomFilter_副本

06——布隆过滤器BloomFilter 一、是什么 由一个初始值都为零的bit数组和多个哈希函数构成&#xff0c;用来快速判断集合中是否存在某个元素 设计思想&#xff1a; 1. 目的&#xff1a;减少内存占用 1. 方式&#xff1a;不保存数据信息&#xff0c;只是在内存中做一个是否存…

类之间的比较

作者简介&#xff1a; zoro-1&#xff0c;目前大一&#xff0c;正在学习Java&#xff0c;数据结构等 作者主页&#xff1a; zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f496; 类之间的比较 固定需求式比较器 固定需求式 通过…

css学习2(利用id与class修改元素)

1、id选择器可以为标有特定id的html元素指定特定的样式。 2、选择器以#开头&#xff0c;后跟某id的属性值。 3、class选择器用于描述一组元素的样式&#xff0c;class可以在多个元素使用。 4、类选择器用.选择。 5、指定特定的元素使用class。 6、元素的多个类用空格分开&…

若依项目的介绍(前后端分离版本)

目录 一、若依介绍 &#xff08;一&#xff09;简单介绍 &#xff08;二&#xff09;若依版本 &#xff08;三&#xff09;Git远程拉取步骤 二、项目的技术介绍 &#xff08;一&#xff09;后端技术 1.spring boot 2.Spring Security安全控制 3.MyBatis 4.MySQL和R…

npm install ffi各种失败,换命令npm i ffi-napi成功

网上各种帖子安装ffi&#xff0c;基本上到了windows build tools这里会卡住。 使用命令npm install --global --production windows-build-tools 安装报错信息如下&#xff1a; PS E:\codes\nodejsPath\tcpTest> npm install --global --production windows-build-tools …

超声波传感器(HC-SR04)按时序图手撕驱动

目录 1、简介 2、传感器介绍 2.1 引脚介绍 2.2 时序图介绍 3、 需求与接线 3.1 任务需求 3.2 接线 4、Cubemax配置 4.1 SYS配置 4.2 RCC配置 4.3 时钟树配置 4.4 GPIO初始化 4.5 定时器配置 4.6 生成代码 5、 keil端代码编写 5.1 微妙函数封装 5.2 超声波驱动封装…

html | 基于iframe的简易富文本编辑器

效果图 支持: 选中后 ctrlI 斜体 代码 思路就是在iframe种嵌套html和css。 <pre> - 支持: 选中后 ctrlI 斜体 - todo: 鼠标实现单击斜体 </pre> <iframe name"richedit" style"height:30%; width:100%;"></iframe><script…

【论文解读】Hybrid-SORT: Weak Cues Matter for Online Multi-Object Tracking

因为Hybrid-SORT的baseline是基于OCSORT进行改进的&#xff0c;在这之前建议先了解byteTrack和【】的相关知识 1.介绍 1.1 基本框架 多目标跟踪(MOT)将问题分为两个子任务。第一个任务是检测每个帧中的对象。第二个任务是将它们在不同的框架中联系起来。关联任务主要通过显式…