JVM——字符串常量池

news/2024/5/14 17:03:00/文章来源:https://blog.csdn.net/YYBDESHIJIE/article/details/137001729

在Java程序中String类的使用几乎无处不在,String类代表字符串,字符串对象可以说是Java程序中使用最多的对象了。首先,在Java中创建大量对象是非常耗费时间的。其次,在程序中又经常使用相同的字符串对象,如果每次都去重新创建相同的字符串对象将会非常浪费空间。最后,字符串对象具有不可变性,即字符串对象一旦创建,内容和长度是固定的,既然这样,那么字符串对象完全可以共享。所以就有了StringTable这一特殊的存在,StringTable叫作字符串常量池,用于存放字符串常量,这样当我们使用相同的字符串对象时,就可以直接从StringTable中获取而不用重新创建对象。本贴对于开发人员意义重大,弄懂字符串常量池及其字符串的相关内容,对程序优化至关重要。

1、String的基本特性

1.1、String类概述

String是字符串的意思,可以使用一对双引号引起来表示,而String又是一个类,所以可以用new关键字创建对象。因此字符串对象的创建有两种方式,分别是使用字面量定义和new的方式创建,如下所示:

// 字面量的定义方式:
String s1 = “xiaoyang”;
// 以new的方式创建:
String s2 = new String(“hello”)

String类声明是加final修饰符的,表示String类不可被继承;String类实现了Serializable接口,表示字符串对象支持序列化;String类实现了Comparable接口,表示字符串对象可以比较大小。

String在JDK 8及以前版本内部定义了final char[]value用于存储字符串数据。JDK 9时改为finalbyte[] value。String在JDK 9中存储结构变更通知如下图所示:
在这里插入图片描述
上图这两段话的大致意思如下:String类的当前实现将字符串存储在char数组中,每个char类型的字符使用2字节(16位)。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而大多数字符串对象只包含Latin-1字符。这些字符只需要1字节的存储空间,也就是说这些字符串对象的内部字符数组中有一半的空间并没有使用。

我们建议将String类的内部表示形式从UTF-16字符数组更改为字节数组加上字符编码级的标志字段。新的String类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符1字节)或UTF-16(每个字符2字节)编码的字符。编码标志将指示所使用的编码。

基于上述官方给出的理由,String不再使用char[]来存储,改成了byte[]加上编码标记,以此达到节约空间的目的。JDK9关于String类的部分源码如代码清单如下所示:
在这里插入图片描述
可以看出来已经将char[]改成了byte[]。

String类做了修改,与字符串相关的类(如AbstractStringBuilder、StringBuilder和String Buffer)也都随之被更新为使用相同的表示形式,HotSpot VM的内部字符串操作也做了更新。

1.2、String的不可变性

String是不可变的字符序列,即字符串对象具有不可变性。例如,对字符串变量重新赋值、对现有的字符串进行连接操作、调用String的replace等方法修改字符串等操作时,都是指向另一个字符串对象而已,对于原来的字符串的值不做任何改变。下面通过代码验证String的不可变性,如代码清单如下所示:
在这里插入图片描述
上面的代码解析如下表所示:
在这里插入图片描述
下面的代码也能说明String的不可变性,如代码清如下所示:
在这里插入图片描述
在上面的代码中,因为change(String str,char ch[])方法的两个形参都是引用数据类型,接收的都是实参对象的首地址,即str和ex.str指向同一个对象,ch和ex.ch指向同一个对象,所以在change方法中打印str和ch的结果和实参ex.str和ex.ch一样。虽然str在change方法中进行了拼接操作,str的值变了,但是由于String对象具有不可变性,str指向了新的字符串对象,就和实参对象ex.str无关了,所以ex.str的值不会改变。而ch在change方法中并没有指向新对象,对ch[0]的修改,相当于对ex.ch[0]的修改。

2、字符串常量池

因为String对象的不可变性,所以String的对象可以共享。但是Java中并不是所有字符串对象都共享,只会共享字符串常量对象。Java把需要共享的字符串常量对象存储在字符串常量池(StringTable)中,即字符串常量池中是不会存储相同内容的字符串的。

2.1、字符串常量池的大小

String的StringTable是一个固定大小的HashTable,不同JDK版本的默认长度不同。如果放进StringTable的String非常多,就会造成Hash冲突严重,从而导致链表很长,链表过长会造成当调用String.intern()方法时性能大幅下降。

在JDK6中StringTable长度默认是1009,所以如果常量池中的字符串过多就会导致效率下降很快。在JDK 7和JDK 8中,StringTable长度默认是60013。StringTable长度默认是60013。

使用“-XX:StringTableSize”可自由设置StringTable的长度。但是在JDK 8中,StringTable长度设置最小值是1009。

下面我们使用代码来测试不同的JDK版本中对StringTable的长度限制,如代码清单如下所示:
在这里插入图片描述
先运行上面的Java程序,然后使用jps和jinfo命令来查看当前Java进程和打印指定Java进程的配置信息。

当使用JDK 8时,StringTable的长度默认值是60013,如下图所示:
在这里插入图片描述
当JDK 8设置StringTable的长度过短的话会抛出“Could not create the Java Virtual Machine”异常。如下图所示,设置的StringTable长度为10时抛出异常:
在这里插入图片描述
上面程序测试了不同版本的JDK对于StringTable的长度有不同的限制,接下来测试不同的StringTable长度对于性能的影响,业务需求为产生10万个长度不超过10的字符串,如代码清单如下所示:

import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;/*** @title GenerateString* @description 生产10万个长度不超过10的字符串,包含a-z,A-Z* @author: yangyongbing* @date: 2024/3/25 12:39*/
public class GenerateString {public static void main(String[] args) throws IOException {FileWriter fw = new FileWriter("words.txt");Random random = new Random();for (int i = 0; i < 100000; i++) {int length = random.nextInt(10) + 1;fw.write(getString(length) + "\n");}fw.close();}public static String getString(int length) {String str = "";Random random = new Random();for (int i = 0; i < length; i++) {//26个大写字母编码:>=65,大小写字母编码相差32int num = random.nextInt(26) + 65;num += random.nextInt(2) * 32;str += (char) num;}return str;}
}

上述代码会产生一个文件,下面对比当StringTable设置不同长度时读取该文件所用的时耗,如代码清单下图所示:
在这里插入图片描述
当字符串常量池的长度设置为“-XX:StringTableSize=1009”时,读取的时间为143ms。当字符串常量池的长度设置为“-XX:StringTableSize=100009”时,读取的时间为47ms。由此可以看出当字符串常量池的长度较短时,代码执行性能降低。

2.2、字符串常量池的位置

除String外,在Java语言中还有8种基本数据类型,为了节省内存、提高运行速度,Java虚拟机为这些类型都提供了常量池。

8种基本数据类型的常量池都由系统协调使用。String类型的常量池使用比较特殊,当直接使用字面量的方式(也就是直接使用双引号)创建字符串对象时,会直接将字符串存储至常量池。当使用其他方式(如以new的方式)创建字符串对象时,字符串不会直接存储至常量池,但是可以通过调用String的intern()方法将字符串存储至常量池。

HotSpot虚拟机中在Java 6及以前版本中字符串常量池放到了永久代,在Java 7及之后版本中字符串常量池被放到了堆空间。字符串常量池位置之所以调整到堆空间,是因为永久代空间默认比较小,而且永久代垃圾回收频率低。将字符串保存在堆中,就是希望字符串对象可以和其他普通对象一样,垃圾对象可以及时被回收,同时可以通过调整堆空间大小来优化应用程序的运行。

下面用代码来展示不同JDK版本中字符串常量池的变化,代码清单如下所示:
在这里插入图片描述
当使用JDK 6时,设置永久代(PermSize)内存为20MB,堆内存大小最小值为128MB,最大值为256MB,运行代码后,报出永久代内存溢出异常,如下图所示:
在这里插入图片描述
在JDK 7中设置永久代(PermSize)内存为20MB,堆内存大小最小值为128MB,最大值为256MB,运行代码后,报出如下图所示堆内存溢出异常。由此可以看出,字符串常量池被放在了堆中,最终导致堆内存溢出。
在这里插入图片描述
在JDK 8中因为永久代被取消,所以PermSize参数换成了MetaspaceSize参数,设置元空间(MetaspaceSize)的大小为20MB,堆内存大小最小值为128MB,最大值为256MB时,运行代码报错和JDK7版本一样,也是堆内存溢出错误。

2.3、字符串常量对象的共享

因为String对象是不可变的,所以可以共享。存储在StringTable中的字符串对象是不会重复的,即相同的字符串对象本质上是共享同一个。Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),如代码清单如下所示:
在这里插入图片描述
Debug运行并查看Memory内存结果如下图所示:
在这里插入图片描述
code(1)代码运行之前,字符串的数量为3468个,code(1)语句执行之后,字符串的数量为3469个,说明code(1)语句产生了1个新的字符串对象。当code(2)语句执行之后,字符串的数量仍然为3469个,说明code(2)语句没有产生新的字符串对象,和code(1)语句共享同一个字符串对象“hello”。当code(3)语句执行之后,字符串的数量为3470个,说明code(3)语句又产生了1个新的字符串对象,因为code(3)语句的字符串“atguiu”和之前的字符串常量对象不一样。

只有在StringTable中的字符串常量对象才会共享,不是在StringTable中的字符串对象,不会共享。例如new出来的字符串不在字符串常量池,代码清单如下所示:
在这里插入图片描述
Debug运行并查看Memory内存结果如下图所示:
在这里插入图片描述
code(4)代码运行之前,字符串的数量为3466个,code(4)语句执行之后,字符串的数量为3468个,说明code(4)语句产生了两个新的字符串对象,一个是new出来的,一个是字符串常量对象“hello”。当code(5)语句执行之后,字符串的数量为3469个,说明code(5)语句只新增了1个字符串对象,它是新new出来的,而字符串常量对象“hello”和code(4)语句共享同一个。

3、字符串拼接操作

3.1、不同方式的字符串拼接

在日常开发中,大家会经常用到字符串的拼接,字符串的拼接通常使用“+”或String类的concat()方法,它们有什么不同呢?另外,使用针对字符串常量拼接和字符串变量拼接又有什么区别呢?字符串拼接结果存放在常量池还是堆中呢?通过运行和分析下面的代码,相信你可以得出结论。

代码清单如下:
在这里插入图片描述

验证常量与常量拼接在编译期优化之后,其拼接结果放在字符串常量池。

上面的代码解析如下表所示:
在这里插入图片描述
通过上面的代码我们可以得出以下结论:

  • (1)字符串常量池中不会存在相同内容的字符串常量。
  • (2)字面常量字符串与字面常量字符串的“+”拼接结果仍然在字符串常量池。
  • (3)字符串“+”拼接中只要其中有一个是变量或非字面常量,结果不会放在字符串常量池中。
  • (4)凡是使用concat()方法拼接的结果也不会放在字符串常量池中。
  • (5)如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

3.2、字符串拼接的细节

为什么这几种字符串拼接结果存储位置不同呢?下面我们将通过查看源码和分析字节码等方式来为大家揭晓答案。

如下图所示,字节码命令视图可以看出代码清单中StringTest类的test1方法中两个字符串加载的内容是相等的,也就是编译器对"a"+“b”+“c"做了优化,直接等同于"abc”。
在这里插入图片描述
如下图所示:
在这里插入图片描述
从字节码命令视图可以看出代码清单中StringTest类的test2方法中,两个字符串拼接过程使用StringBuilder类的append()方法来完成,之后又通过toString()方法转为String字符串对象。而StringBuilder类的toString()方法源码如代码清单所示:
在这里插入图片描述

它会重新new一个字符串对象返回,而直接new的String对象一定是在堆中,而不是在常量池中。

下面查看String类的concat()方法源码,如代码清单如下所示:
在这里插入图片描述
只要拼接的不是一个空字符串,那么最终结果都是new一个新的String对象返回,所以拼接结果也是在堆中,而非常量池。

3.3、“+”拼接和StringBuilder拼接效率

在上一节,我们提到了“+”拼接过程中,如果“+”两边有非字符串常量出现,编译器会将拼接转换为StringBuilder的append拼接。那么使用“+”拼接和直接使用StringBuilder的append()拼接,效率有差异吗?代码清单:
在这里插入图片描述

演示了字符串“+”拼接和StringBuilder的append()的效率对比。

创建100000个字符串的拼接使用test1()方法耗时4014ms,占用内存1077643816字节,使用test2()方法耗时7ms,占用内存13422072字节,明显test2()方法的效率更高。这是因为test2()方法中StringBuilder的append()自始至终只创建过一个StringBuilder对象。test1()方法中使用String的字符串拼接方式会创建多个StringBuilder和String对象。

4、intern()的使用

StringTest5类的test4()方法中可以看出:
在这里插入图片描述

无论是哪一种字符串拼接,拼接后调用intern()结果都在字符串常量池。这是为什么呢?查看intern()方法的官方文档解释说明,如下图所示:
在这里插入图片描述
当调用intern()方法时,如果池中已经包含一个等于此String对象的字符串,则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。也就是说,如果在任意字符串上调用intern()方法,那么其返回地址引用和直接双引号表示的字面常量值的地址引用是一样的。例如:new String(“I love yang”).intern()==“I love yang”和“I love yang” ==new String(“I love yang”).intern()的结果都是true。

4.1、不同JDK版本的intern()方法

虽然intern()方法都是指返回字符串常量池中字符串对象引用,但是在不同的JDK版本中,字符串常量池的位置不同,决定了字符串常量池是否会与堆中的字符串共享问题。下面通过代码清单查看不同JDK版本的字符串常量共享的区别。
在这里插入图片描述
在JDK6中,上述代码test1()和test2()方法运行结果都是false和true。这是因为JDK6时,HotSpot虚拟机中字符串常量池在永久代,不在堆中,所以,字符串常量池不会和堆中的字符串共享,即无论是test1()还是test2(),s1指向的是堆中的字符串对象的地址,而s2和s指向的是永久代中字符串对象的地址。

在JDK7和JDK8中上述代码test1()方法运行结果是false和true,test2()方法运行结果是true和true。这是因为HotSpot虚拟机在JDK 7和JDK 8中,字符串常量池被设置在了堆中,所以,字符串常量池可以和堆共享字符串。在test1()方法中,由于是先给s赋值"ab",后出现s1.intern()的调用,也就是在用intern()方法之前,堆中已经有一个字符串常量"ab"了,字符串常量池中记录的是它的地址,intern()方法也直接返回该地址,而给s1变量赋值的是新new的堆中的另一个字符串的地址,所以test1()方法运行结果是false和true。在test2()方法中,由于是先调用s1.intern(),后出现给s赋值"ab",此时intern()方法之前,堆中并不存在字符串常量"ab",所以就直接把s1指向的new出来的堆中的字符串“ab”的地址放到了字符串常量表中,之后给s变量赋值“ab”时,也直接使用该地址,所以test1()方法运行结果是true和true。

上述表达使用内存示意图说明的话,JDK 6的test1()和test2()的内存示意图一样,如下图所示:
在这里插入图片描述
JDK 7和JDK 8的test1()和test2()的内存示意图不一样,如下图所示:
在这里插入图片描述

4.2、intern()方法的好处

根据上一个小节的分析,JDK 7及其之后的版本,intern()方法可以直接把堆中的字符串对象的地址放到字符串常量池表共享,从而达到节省内存的目的。下面通过一段代码,分别测试不用intern()方法和使用intern()方法的字符串对象的个数及其内存占用情况,如下代码清单所示:
在这里插入图片描述
当没有使用intern()时花费的时间为7307ms,通过JDK自带工具jvisualvm.exe结合VisualVM Launcher插件可以查看其运行时内存中的字节数和实例数,如下图所示:
在这里插入图片描述
当使用intern()时花费的时间为1311ms,其运行时内存中的字节数和实例数,如下图所示:
在这里插入图片描述
从上面的内存监测样本可以得出结论,程序中存在大量字符串,尤其存在很多重复字符串时,使用intern()可以大大节省内存空间。例如大型社交网站中很多人都存储北京市海淀区等信息,这时候如果字符串调用intern()方法,就会明显降低内存消耗。

5、字符串常量池的垃圾回收

字符串常量池中存储的虽然是字符串常量,但是依然需要垃圾回收。我们接下来验证字符串常量池中是否存在垃圾回收操作。首先设置JVM参数“-Xms15m -Xmx15m -XX:+PrintStrin gTableStatistics -XX:+PrintGCDetails”,然后分别设置不同的运行次数,其中“-XX:+PrintString TableStatistics”参数可以打印StringTable的使用情况,测试代码如下所示:
在这里插入图片描述
当循环次数为100次时,关于StringTable statistics的统计信息如下图所示:
在这里插入图片描述
图中方框标记内容为堆空间中StringTable维护的字符串的个数,并没有GC信息。

当循环次数为10万次时,关于StringTable statistics的统计信息如下图所示:
在这里插入图片描述
堆空间中StringTable维护的字符串的个数就不足10万个,并且出现了GC信息,如下图所示:
在这里插入图片描述
说明此时进行了垃圾回收使得堆空间的字符串信息下降了。

6、G1中的String去重操作

不同的垃圾收集器使用的算法是不同的,其中G1垃圾收集器对字符串去重的官方说明如下图所示:
在这里插入图片描述
上面内容大致意思就是许多大型Java应用的瓶颈在于内存。测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是字符串对象。更进一步,这里面差不多一半字符串对象是重复的,重复的意思是指string1.equals(string2)的结果是true。堆上存在重复的字符串对象必然是一种内存的浪费。在G1垃圾收集器中实现自动持续对重复的字符串对象进行去重,这样就能避免浪费内存。那么G1垃圾收集器是如何对字符串进行去重的呢?

G1垃圾收集器对重复的字符串对象去重的步骤如下:

  • (1)当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象,都会检查是否为候选的要去重的字符串对象。
  • (2)如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的字符串对象。
  • (3)使用一个哈希表(HashTable)来记录所有的被字符串对象使用的不重复的char数组。当去重的时候会查这个哈希表,来看堆上是否已经存在一个一模一样的char数组。
  • (4)如果存在,字符串对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收。
  • (5)如果查找失败,char数组会被插入到HashTable,这样以后就可以共享这个数组了。

实现对字符串对象去重的相关命令行选项如下:

  • (1)UseStringDeduplication(bool):开启String去重,默认是不开启,需要手动开启。
  • (2)PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息。
  • (3)StringDeduplicationAgeThreshold(uintx):达到这个年龄的字符串对象被认为是去重的候选对象。

7、小结

首先介绍了String类的创建方式及其特性。字符串的分配和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化,使用字符串常量池实现对字符串常量对象的共享以节省大量的内存空间。

接着介绍了不同版本的JDK,字符串常量池在内存中的位置是不一样的。在JDK6版本中,字符串常量池存放在永久代。JDK7及其之后的版本放在了堆空间。JDK这么做的原因是因为永久代的空间是比较小的,如果字符串对象非常多的时候内存就明显不够用了。另一个原因是把字符串对象保存在堆中,String和其他普通对象一样,可以通过调整堆空间大小来优化应用程序的运行。

通过案例演示使用不同方式创建字符串、拼接字符串,都将会对程序性能产生很大的影响,当出现大量字符串拼接时,使用字符串缓冲区StringBuilder或StringBuffer将提高字符串拼接效率。我们又通过案例讲解了Sting类中intern()方法的作用,当应用程序需要存储大量相同字符串的时候,调用intern()方法,可以大大降低内存消耗。

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

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

相关文章

python编程软件有什么

Python开发软件可根据其用途不同分为两种&#xff0c;一种是Python代码编辑器&#xff0c;一种是Python集成开发工具&#xff0c;两者的配合使用可以极大的提高Python开发人员的编程效率&#xff0c;以下是常用的几款Python代码编辑器和Python集成开发工具。 一、Python代码编…

Ubuntu20.04LTS+uhd3.15+gnuradio3.8.1源码编译及安装

文章目录 前言一、卸载本地 gnuradio二、安装 UHD 驱动三、编译及安装 gnuradio四、验证 前言 本地 Ubuntu 环境的 gnuradio 是按照官方指导使用 ppa 的方式安装 uhd 和 gnuradio 的&#xff0c;也是最方便的方法&#xff0c;但是存在着一个问题&#xff0c;就是我无法修改底层…

docker安装elasticseachkibana

1.docker安装es 创建本机挂载目录&#xff0c;与容器上目录映射 /Users/wangpei/2024/mydata/elasticsearch conf下创建yml文件 echo "http.host : 0.0.0.0" >> /Users/wangpei/2024/mydata/elasticsearch/config/elasticsearch.yml 安装容器&#xff1a; d…

RabbitMQ 延时消息实现

1. 实现方式 1. 设置队列过期时间&#xff1a;延迟队列消息过期 死信队列&#xff0c;所有消息过期时间一致 2. 设置消息的过期时间&#xff1a;此种方式下有缺陷&#xff0c;MQ只会判断队列第一条消息是否过期&#xff0c;会导致消息的阻塞需要额外安装 rabbitmq_delayed_me…

【MySQL】DQL-条件查询语句全解(附带代码演示&案例练习)

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C Linux的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的…

C++中的STL简介与string类

目录 STL简介 STL的版本 STL的六大组件 string类 标准库中的string类 string类的常用接口 string类对象对容量的操作 size()函数与length()函数 capacity()函数 capacity的扩容方式 reserve()函数 resize()函数 string类对象的操作 push_back()函数 append()函数 operator()函数…

Rust高级爬虫:如何利用Rust抓取精美图片

引言 在当今信息爆炸的时代&#xff0c;互联网上的图片资源丰富多彩&#xff0c;而利用爬虫技术获取这些图片已成为许多开发者的关注焦点。本文将介绍如何利用Rust语言进行高级爬虫编程&#xff0c;从而掌握抓取精美图片的关键技术要点。 Rust爬虫框架介绍 Rust语言生态中有…

【谷歌开发者月刊】聚焦三月精彩内容,让开发思路更加开阔

随着春日的到来&#xff0c;阳光渐煦&#xff0c;正是吸收能量的大好时机&#xff0c;我们也为开发者们带来了众多更新内容&#xff0c;为您的开发之路提供思路&#xff01;本月精彩内容众多&#xff0c;快来一起查收&#xff01; 本月看点 01Android 15 首个开发者预览版到来0…

git基本操作(小白入门快速上手一)

1、前言 我们接上一篇文章来讲&#xff0c;直接开干 1.1、工作区 1. 工作区很好理解&#xff0c;就是我们能看到的工作目录&#xff0c;就是本地的文件夹。 2. 这些本地的文件夹我们要通过 git add 命令先将他们添加到暂存区中。 3. git commit 命令则可以将暂存区中的文件提交…

在.Net6中用gdal实现第一个功能

目录 一、创建.NET6的控制台应用程序 二、加载Gdal插件 三、编写程序 一、创建.NET6的控制台应用程序 二、加载Gdal插件 Gdal的资源可以经过NuGet包引入。右键单击项目名称&#xff0c;然后选择 "Manage NuGet Packages"&#xff08;管理 NuGet 包&#xff09;。N…

SD 修复 Midjourney 有瑕疵照片

Midjourney V6 生成的照片在质感上有了一个巨大的提升。下面4张图就是 Midjourney V6 生成的。 如果仔细观察人物和老虎的面部&#xff0c;细节真的很丰富。 但仔细观察上面四张图的手部细节&#xff0c;就会发现至少有两只手是有问题的。这也是目前所有 AI 绘图工具面临的问题…

阿里云2核4G服务器租用价格30元、165元和199元1年

阿里云2核4G服务器租用优惠价格&#xff0c;轻量2核4G服务器165元一年、u1服务器2核4G5M带宽199元一年、云服务器e实例30元3个月&#xff0c;活动链接 aliyunfuwuqi.com/go/aliyun 活动链接如下图&#xff1a; 阿里云2核4G服务器优惠价格 轻量应用服务器2核2G4M带宽、60GB高效…

thinkadmin 新版安装步骤

1.通过 Composer 安装: ( 推荐方式,默认只安装 admin 模块 ) ### 创建项目( 需要在英文目录下面执行 ) composer create-project zoujingli/thinkadmin### 进入项目根目录 cd thinkadmin### 数据库初始化并安装 ### 默认使用 Sqlite 数据库,若使用其他数据库请按第二步修…

大话设计模式之原型模式

原型模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;它用于创建对象的复制&#xff0c;同时又能保持对象的封装。原型模式通过复制现有对象的方式来创建新的对象&#xff0c;而无需知道具体创建过程的细节。 在原型模式中&#xff0c;通常会有…

经纬恒润AUTOSAR产品成功适配芯来RISC-V车规内核

近日&#xff0c;经纬恒润AUTOSAR基础软件产品INTEWORK-EAS&#xff08;ECU AUTOSAR Software&#xff0c;以下简称EAS&#xff09;在芯来提供的HP060开发板上成功适配芯来科技的RISC-V处理器NA内核&#xff0c;双方携手打造了具备灵活、可靠、高性能、强安全性的解决方案。这极…

C++王牌结构hash:哈希表开散列(哈希桶)的实现与应用

目录 一、开散列的概念 1.1开散列与闭散列比较 二、开散列/哈希桶的实现 2.1开散列实现 哈希函数的模板构造 哈希表节点构造 开散列增容 插入数据 2.2代码实现 一、开散列的概念 开散列法又叫链地址法(开链法)&#xff0c;首先对关键码集合用散列函数计算散列地址&…

微软开源项目Garnet:Redis的竞争者还是替代者?

对于开源社区&#xff0c;最近的一大新闻就是Redis宣布从7.4版本开始&#xff0c;将采用Redis源代码可用许可证&#xff08;RSALv2&#xff09;和服务器端公共许可证&#xff08;SSPLv1&#xff09;的双重许可证&#xff0c;取代原有的BSD三条款许可证。这一变化引发了开发者社…

面试算法-126-二叉树的所有路径

题目 给你一个二叉树的根节点 root &#xff0c;按 任意顺序 &#xff0c;返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,null,5] 输出&#xff1a;[“1->2->5”,“1->3”] 解 class …

WIFI驱动移植实验:WIFI从路由器动态获取IP地址与联网

一. 简介 前面两篇文章&#xff0c;一篇文章实现了WIFI联网前要做的工作&#xff0c;另一篇文章配置了WIFI配置文件&#xff0c;进行了WIFI热点的连接。文章如下&#xff1a; WIFI驱动移植实验&#xff1a;WIFI 联网前的工作-CSDN博客 WIFI驱动移植实验&#xff1a;连接WIF…

pdfjs 实现给定pdf数据切片高亮并且跳转

pdfjs 实现给定pdf数据切片高亮并且跳转 pdfjs 类的改写基本展示需求的实现高亮功能的实现查询功能分析切片数据处理 pdfjs 类的改写 需求&#xff1a; pdf文件被解析成多个分段&#xff0c;每个分段需要能够展示&#xff0c;并且通过点击分段实现源pdf内容的高亮以及跳转需求…