深入研究Java的String常量池

news/2024/9/15 22:47:52/ 标签: java, 开发语言

文章目录

  • 一、StringTable
      • 分析一段代码
      • 示例一
      • 示例二
      • 示例三
  • 二、 intern
    • 1、StringTable位置
    • 2、StringTable 性能调优
    • 3、intern深入分析
      • 3.1 思考
      • 3.2 JDK6中的解释
      • 3.3 JDK7中的解释
      • 3.4 详细分析
      • 3.5 intern正确使用的例子
      • 3.6 intern使用不当的例子

一、StringTable

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象,就是说在生成字节码文件的时候并不会将常量写入串池中,而是执行到了才会写入(懒加载
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder + new String(xxx);
  • 对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。Javac 编译器会进行一个叫做 常量折叠的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一

  • 对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string";

    并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

    • 基本数据类型( bytebooleanshortcharintfloatlongdouble)以及字符串常量。
    • final 修饰的基本数据类型和字符串变量
    • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

分析一段代码

java">public static void main(String[] args) throws ExecutionException, InterruptedException {String s1 = "a";String s2 = "b";String s3 = "ab";String s4 = s1 + s2;String s5 = "a" + "b";System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // true
}

1、初始的时候,StringTable【串池】为空,他是一个HashTable结构,不能扩容。

2、常量池中的信息,都会被加载到运行时常量池,这时abab都是常量池中的符号,还没有变为java字符串对象

3、执行String s1 = "a",字节码ldc #2会把符号a变为"a"字符串对象,然后看StringTable中是否存在字符串"a",如果不存在,就将这个字符串放入串池,如果存在,就直接使用串池中的字符串对象

4、执行String s2 = "b",字节码ldc #3会把符号b变为"b"字符串对象,然后看StringTable中是否存在字符串"b",如果不存在,就将这个字符串放入串池,如果存在,就直接使用串池中的字符串对象

5、执行String s3 = "ab",字节码ldc #4 会把符号ab变为字符串"ab"对象,然后看StringTable中是否存在字符串"ab",如果不存在,就将这个字符串放入串池,如果存在,就直接使用串池中的字符串对象

6、String s4 = s1 + s2,底层字节码执行的就是new StringBuilder().append("a").append("b").toString(),这里toString()就是执行的new String("ab"),因此s4指向的是堆区地址,而s3指向的是串池中的地址,因此s3s4不相等

7、String s5 = "a" + "b",字符串常量拼接,编译器底层会进行优化,javac在编译期间,结果就被确定为ab,在常量池中找到"ab"存在,所以s5指向的也是串池中的地址



示例一

1、当执行到ldc #2时,会把符号 a 变为"a"字符串对象,并放入串池中(hashtable结构 不可扩容)

2、当执行到 ldc #3 时,会把符号 b 变为 "b"字符串对象,并放入串池中

3、当执行到 ldc #4 时,会把符号 ab 变为 "ab" 字符串对象,并放入串池中

最终StringTable ["a", "b", "ab"]

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

java">public class StringTableStudy {public static void main(String[] args) {String a = "a"; String b = "b";String ab = "ab";}
}

常量池中的信息,都会被加载到运行时常量池中,但这是abab 仅是常量池中的符号,还没有成为java字符串

java">0: ldc           #2                  // String a
2: astore_1
3: ldc           #3                  // String b
5: astore_2
6: ldc           #4                  // String ab
8: astore_3
9: return


示例二

1、通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()

2、最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

java">public class HelloWorld {public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "ab";String s4 = s1 + s2; //new StringBuilder().append("a").append("2").toString()  new String("ab")System.out.println(s3 == s4); //false// 结果为false,因为s3是存在于串池之中,s4是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中}
}
java">Code:stack=2, locals=5, args_size=10: ldc           #2              // String a2: astore_13: ldc           #3              // String b5: astore_26: ldc           #4              // String ab8: astore_39: new           #5              // class java/lang/StringBuilder12: dup13: invokespecial #6              // Method java/lang/StringBuilder."<init>":()V16: aload_117: invokevirtual #7              // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;20: aload_221: invokevirtual #7              // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;24: invokevirtual #8              // Method java/lang/StringBuilder.toString:()Ljava/lang/String;27: astore        429: return


示例三

1、使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了"ab",所以s5直接从串池中获取值,和s3相等

2、使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建

java">public class HelloWorld {public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "ab";String s4 = s1 + s2; //new StringBuilder().append("a").append("2").toString()  new String("ab")String s5 = "a" + "b";System.out.println(s5 == s3); //true}
}
java">Code:
stack=2, locals=6, args_size=10: ldc           #2              // String a2: astore_13: ldc           #3              // String b5: astore_26: ldc           #4              // String ab8: astore_39: new           #5              // class java/lang/StringBuilder12: dup13: invokespecial #6              // Method java/lang/StringBuilder."<init>":()V16: aload_117: invokevirtual #7              // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;20: aload_221: invokevirtual #7              // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;24: invokevirtual #8              // Method java/lang/StringBuilder.toString:()Ljava/lang/String;27: astore        4				 29: ldc           #4              // String ab  31: astore        5				// s5的ab初始化时直接从串池中获取字符串33: return


二、 intern

JDK1.8

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,就在常量池中创建一个指向该字符串对象的引用
  • 如果有该字符串对象,即字符串常量池中保存了对应的字符串对象的引用,则放入失败

无论放入是否成功,都会返回串池中的字符串对象


示例

1、String x = "ab",创建字符串"ab",将该字符串放入串池中,此时串池内容为["ab"]

2、String s = new String("a") + new String("b"),堆区创建字符串new String("a")new String("b")new String("ab"),将“a”"b"放入串池,此时串池内容["ab", "a", "b"]

3、String s2 = s.intern(),尝试将字符串s放入串池,但是现在串池已经有ab这个字符串了,所以放入失败,s指向堆区,但是返回的s2是串池的字符串"ab"

4、对于new String("a")这个语句创建了两个对象,第一个对象是"a"字符串存储在常量池,第二个对象是在Java堆中的String对象

java">public static void main(String[] args) {String x = "ab";String s = new String("a") + new String("b");String s2 = s.intern(); // 将这个字符串对象尝试放入串池,现在串池有,放入失败System.out.println(s2 == x); // trueSystem.out.println(s == x); // false
}
java">public static void main(String[] args) {String s = new String("a") + new String("b");String s2 = s.intern(); // 将这个字符串对象尝试放入串池,现在串池没有,放入成功System.out.println(s2 == "ab"); // trueSystem.out.println(s == "ab"); // true
}


JDK1.6

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中【因为JDK1.6中,字符串常量池在方法区】,该对象仍然指向堆区地址
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象



JDK1.8测试

java">public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "a" + "b";String s4 = s1 + s2;String s5 = "ab";String s6 = s4.intern();System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // trueString x2 = new String("c") + new String("d");String x1 = "cd";x2.intern();System.out.println(x1 == x2); // falseString x4 = new String("e") + new String("f");x4.intern();String x3 = "ef";System.out.println(x3 == x4); // true
}


1、StringTable位置

JDK1.6 时,StringTable是属于常量池的一部分,在方法区中,但是方法区只有在fullgc时垃圾才会被回收,回收效率太低。因此从JDK1.7往后,StringTable放在中,MinorGC就会触发垃圾回收,减轻无用字符串对内存的占用。StringTable在内存紧张时,会发生垃圾回收



2、StringTable 性能调优

  • StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间【避免了Hash冲突】

    -XX:StringTableSize=桶个数(最少设置为 1009 以上)

  • 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池,保证相同字符串在StringTable只保存一份


3、intern深入分析

以下内容参考自美团技术博客:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

3.1 思考

String s = new String("abc")这个语句创建了几个对象?

答案:上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。

看一段代码:

java">public static void main(String[] args) {String s = new String("1");s.intern();String s2 = "1";System.out.println(s == s2);String s3 = new String("1") + new String("1");s3.intern();String s4 = "11";System.out.println(s3 == s4);
}

打印结果

  • jdk6 下false false
  • jdk7 下false true

然后将s3.intern();语句下调一行,放到String s4 = "11";后面。将s.intern(); 放到String s2 = "1";后面

java">public static void main(String[] args) {String s = new String("1");String s2 = "1";s.intern();System.out.println(s == s2);String s3 = new String("1") + new String("1");String s4 = "11";s3.intern();System.out.println(s3 == s4);
}

打印结果

  • jdk6 下false false
  • jdk7 下false false

3.2 JDK6中的解释

  1. jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA 堆 区域是完全分开的。
  2. 上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。

3.3 JDK7中的解释

  1. 在 Jdk6 以及以前的版本中,字符串的常量池放在 Perm 区(JDK7及之前方法区的实现),Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。
  2. 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到 Java 堆 区域。因为Perm 区域太小,同时在Perm区的字符串需要等到FullGC才能被回收。在JDK8中,方法区的实现采用了元空间matespace,放在了本地内存。

3.4 详细分析

  • 在第一段代码中,先看 s3和s4字符串。String s3 = new String("1") + new String("1");,这句代码中现在生成了2个对象,是字符串常量池中的“1” 和 堆中 中的 s3引用指向的对象。中间还有2个匿名的new String(“1”) 我们不去讨论它们。此时s3引用对象内容是”11”,但此时常量池中是没有 “11”对象的。执行s3.intern()将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,在jdk6中,会在常量池中生成一个 “11” 的对象,但是在 jdk7 中常量池不在 Perm 区域了,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。 最后String s4 = "11"; 这句代码中”11”是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。

  • 再看 s 和 s2 对象。 String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。接下来String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同

  • 第二段代码中的 s 和 s2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。

  • 再看s3和s4,执行String s4 = "11";声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行s3.intern();时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。


3.5 intern正确使用的例子

java">public static void main(String[] args) throws Exception {Integer[] DB_DATA = new Integer[10];Random random = new Random(10 * 10000);for (int i = 0; i < DB_DATA.length; i++) {DB_DATA[i] = random.nextInt();}long t = System.currentTimeMillis();for (int i = 0; i < MAX; i++) {//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])); arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();}System.out.println((System.currentTimeMillis() - t) + "ms");System.gc();}

分析结果

在这里插入图片描述

在这里插入图片描述

  • 通过上述结果,我们发现不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。
  • arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); 在堆区创建一个字符串后,然后将字符串放入常量池,不管是否放进去,最后返回的都是常量池的对象,因此新创建的堆区对象是没有引用的,所以在youngGc的时候就会被回收。但如果不使用intren,那么每个在堆区创建的字符串对象都被引用的,在youngGC时也就不会被回收掉

3.6 intern使用不当的例子

在使用 fastjson 进行接口读取的时候,我们发现在读取了近70w条数据后,我们的日志打印变的非常缓慢,每打印一次日志用时30ms左右,如果在一个请求中打印2到3条日志以上会发现请求有一倍以上的耗时。在重新启动 jvm 后问题消失。继续读取接口后,问题又重现。

这是因为 fastjson 中对所有的 json 的 key 使用了 intern 方法,缓存到了字符串常量池中,这样每次读取的时候就会非常快,大大减少时间和空间。而且 json 的 key 通常都是不变的。这个地方没有考虑到大量的 json key 如果是变化的,那就会给字符串常量池带来很大的负担。因为字符串常量池大小是固定的为1009,如果放入的字符串过多,会造成hash冲突,导致之后的查询速度缓慢。
这个问题 fastjson 在1.1.24版本中已经将这个漏洞修复了。程序加入了一个最大的缓存大小,超过这个大小后就不会再往字符串常量池中放了。


http://www.ppmy.cn/news/1505048.html

相关文章

PatchCore:工业异常检测中的全面召回

PatchCore&#xff1a;工业异常检测中的全面召回 前言相关介绍PatchCore的工作原理&#xff1a;优点&#xff1a;缺点&#xff1a; 实验环境项目地址LinuxWindows 项目结构具体用法准备数据进行训练进行测试 常见问题ModuleNotFoundError: No module named patchcore解决方法 O…

[PM]面试题-综合问题

思维题 说说当前的科技行业 web3是我比较感兴趣的方向, 在国内还处于起步阶段, web3重要的特点是去中心化, 依赖的技术有以太坊, 区块链, 智能合约, 现在位置还没有特别成熟的产品形态, 发展的比较好的方向就是数字藏品和游戏方向 列举一个你认为比较好的APP, 说明其独特之处…

一则悬空指针案例

int* foo() {int a; // 变量a的作用域开始a 100;char *c "xyz"; // 变量c的作用域开始return &a; } // 变量a和c的作用域结束先来看这样一段代码。这段代码虽然可以编译通过&#xff0c;但是其实非常糟糕&#xff0c;变量 a 和 c…

【linux运维】大型文件查询特定字符串方案总结(如2GB的文本文件)

对于大型文件&#xff08;如2GB的文本文件&#xff09;&#xff0c;直接使用 grep 或其他文本处理工具可能会消耗大量的内存资源。为了更高效地处理这种情况&#xff0c;可以采用以下几种策略&#xff1a; 1. 使用 grep 的性能优化 尽管 grep 是非常高效的工具&#xff0c;但…

Unity复制对象时让私有变量也被复制的简单方法

Unity复制对象时&#xff0c;如果一个变量为公共变量&#xff08;public&#xff09;&#xff0c;那么这个变量的值会被复制到新的对象中去&#xff0c;但是如果一个变量是私有变量&#xff08;private&#xff09;&#xff0c;默认是不会被复制的&#xff0c;如果希望被复制&a…

pytorch学习笔记5 tensor 广播broadcasting

不同shape直接加减&#xff0c;系统会自动做broadcasting操作 先右对齐&#xff08;小维度对齐&#xff09; 比如&#xff1a;Feature maps&#xff1a; [4,32,14,14] Bias&#xff1a;[32,1,1] >][1,32,1,1] >[4,32,14,14] 做到与Feature maps的shape相同&#xff0c;才…

【Vulnhub系列】Vulnhub Connect-The-Dots 靶场渗透(原创)

【Vulnhub系列靶场】Vulnhub Connect-The-Dots靶场渗透 原文转载已经过授权 原文链接&#xff1a;Lusen的小窝 - 学无止尽&#xff0c;不进则退 (lusensec.github.io) 一、主机发现 二、端口扫描 PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 2.0.8 or…

C++选择题带答案

(1) 若变量已正确定义并赋值&#xff0c;表达式____不符合C语言语法。 A) a*b/c; B) 3.14%2 C) 2, b D) a/b/c (2) _____是不正确的字符常量。 A)ˊnˊ B) ˊ1ˊ C)"a" D) ˊ101ˊ (3) 在 C 程序中&#xf…

【Python学习手册(第四版)】学习笔记14-迭代器和列表解析(一)

个人总结难免疏漏&#xff0c;请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文主要以通俗易懂的语言介绍迭代器&#xff08;文件迭代、手动迭代iter和next等&#xff09;&#xff0c;列表解析式包括基础知识包括写法、文件上使用列表解析…

STL-stack容器适配器

目录 一、容器适配器 二、使用 三、模拟实现 一、容器适配器 适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结)&#xff0c;该种模式是将一个类的接口转换成客户希望的另外一个接口。 STL中&#xff0c;stack是由dequ…

sql注入时,waf屏蔽了information_schema如何处理?

information_schema可代替库 如果使用正则表达式屏蔽了information&#xff0c;则想一想有没有能代替的information的库。 例如&#xff1a;x$schema_table_statistics这个库里面就有table_schema和table_name&#xff0c;但是没有列名&#xff0c;那么列名就需要使用无列名注…

基于FPGA的数字信号处理(20)--半减器和全减器

目录 1、前言 2、半减器 3、全减器 4、减法器 文章总目录点这里&#xff1a;《基于FPGA的数字信号处理》专栏的导航与说明 1、前言 既然有半加器和全加器&#xff0c;那自然也有半减器和全减器了。尽管在电路中减法的实现基本都是 补码 加法 的形式&#xff0c;但是正所谓…

RK3568笔记五十一:W25Q64测试(spi 标准接口 )

若该文为原创文章&#xff0c;转载请注明原文出处。 前面有测试过W25Q64&#xff0c;但那是自己编写的驱动&#xff0c;现在使用内核自带的驱动&#xff0c;只需要通过SPI标准接口&#xff0c;编写应用程序即可以读写W25Q64. 一、硬件原理图 SPI 引脚 功能 MOSI GPIO3_C1 …

LeetCode——572. 另一颗树的子树

通过万岁&#xff01;&#xff01;&#xff01; 题目&#xff1a;给你两棵树&#xff0c;然后问subRoot是不是root的子树。也就是root某个节点的所有孩子节点在值和结构上完全与subRoot相同。思路&#xff1a;我的思路比较简单&#xff0c;就是遍历root&#xff0c;遇到root中…

【vulnhub】Zico2靶机

信息收集 靶机IP发现 进行端口扫描&#xff0c;发现开放了22&#xff08;ssh&#xff09;&#xff0c;80&#xff08;http&#xff09;和111&#xff08;rpcbind&#xff09;端口。 进行目录扫描 dirsearch -u http://192.168.93.152 网址IP访问 后面拼接/dbadmin/目录 用弱口…

HarmonyOS鸿蒙应用开发之Text组件的使用

文章目录 示例代码说明Text组件常用属性1. 文本内容2. 字体样式3. 文本对齐4. 布局与边距5. 文本截断与换行6. 其他属性 对比Android的TextView1.基本概念2.属性对比3.小结 注意事项 Text组件在ArkTS中的用法非常灵活&#xff0c;可以通过代码动态地设置文本内容、样式等属性。…

深入理解Shell中的`echo`命令

在Shell脚本编程中&#xff0c;echo命令是最基本的命令之一&#xff0c;用于向终端输出文本。然而&#xff0c;echo的用途远不止于此。本文将深入探讨echo命令的多种用法&#xff0c;包括打印命令执行结果和控制命令执行流程。 echo命令基础 echo命令用于在终端输出字符串或变…

[kimi笔记].net平台

.NET 是一个由微软公司开发的软件开发平台&#xff0c;它的历史可以追溯到2000年左右。.NET 框架最初是为Windows操作系统设计的&#xff0c;但随着时间的发展&#xff0c;微软逐步扩展了.NET 的应用范围&#xff0c;使其可以在多个平台上运行。以下是.NET 几个平台的历史和平台…

简单搭建dns服务器

目录 一.安装服务 二.编写子配置文件 三.编写主配置文件 四.编写文件 五.重启服务测试 配置端&#xff1a;IP地址为172.25.254.100、主机名为node1.rhel9.org 测试端&#xff1a;IP地址为172.25.254.101、主机名为node2.rhel9.org 一.安装服务 [rootnode1 ~]# dnf inst…

未授权访问漏洞系列详解③!

Elasticsearch未授权访问漏洞 ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于RESTful web接口。Elasticsearch是用Java开发的&#xff0c;并作为Apache许可条款下的开放源码发布&#xff0c;是当前流行的企业级搜索引…