一、沉默王二-字符串
1、java字符串常量池
1)字符常量池作用
“先从这道面试题开始吧!”
String s = new String("二哥");
这行代码创建了两个对象。
“使用 new 关键字创建一个字符串对象时,Java 虚拟机会先在字符串常量池中查找有没有‘二哥’这个字符串对象
如果有,就不会在字符串常量池中创建‘二哥’这个对象了,直接在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的对象地址返回赋值给变量 s。”
“如果没有,先在字符串常量池中创建一个‘二哥’的字符串对象,然后再在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的字符串对象地址返回赋值给变量 s。”
我画图表示一下,会更加清楚。
在 Java 中,栈上存储的是基本数据类型的变量和对象的引用,而对象本身则存储在堆上。
对于这行代码 String s = new String("二哥");,它创建了两个对象:一个是字符串对象 "二哥",它被添加到了字符串常量池中,另一个是通过 new String() 构造方法创建的字符串对象 "二哥",它被分配在堆内存中,同时引用变量 s 存储在栈上,它指向堆内存中的字符串对象 "二哥"。
“为什么要先在字符串常量池中创建对象,然后再在堆上创建呢?这样不就多此一举了?”三妹敏锐地发现了问题。
我回答,“是的。由于字符串的使用频率实在是太高了,所以 Java 虚拟机为了提高性能和减少内存开销,在创建字符串对象的时候进行了一些优化,特意为字符串开辟了一块空间——也就是字符串常量池。”
String s = "三妹";
当执行 String s = "三妹" 时,Java 虚拟机会先在字符串常量池中查找有没有“三妹”这个字符串对象,如果有,则不创建任何对象,直接将字符串常量池中这个“三妹”的对象地址返回,赋给变量 s;如果没有,在字符串常量池中创建“三妹”这个对象,然后将其地址返回,赋给变量 s。
Java 虚拟机创建了一个字符串对象 "三妹",它被添加到了字符串常量池中,同时引用变量 s 存储在栈上,它指向字符串常量池中的字符串对象 "三妹"。你看,是不是省了一步,比之前高效了。
“有了字符串常量池,就可以通过双引号的方式直接创建字符串对象,不用再通过 new 的方式在堆中创建对象了,对吧?”
“是滴。new 的方式始终会创建一个对象,不管字符串的内容是否已经存在,而双引号的方式会重复利用字符串常量池中已经存在的对象。”我说。
来看下面这个例子:
String s = new String("二哥");
String s1 = new String("二哥");
按照我们之前的分析,这两行代码会创建三个对象,字符串常量池中一个,堆上两个。
再来看下面这个例子:
String s = "三妹";
String s1 = "三妹";
这两行代码只会创建一个对象,就是字符串常量池中的那个。这样的话,性能肯定就提高了!
2)字符常量池位置
到了 Java 8,永久代(PermGen)被取消,并由元空间(Metaspace)取代。元空间是一块本机内存区域,和 JVM 内存区域是分开的。不过,元空间的作用依然和之前的永久代一样,用于存储类信息、方法信息、常量池信息等静态数据。
与永久代不同,元空间具有一些优点,例如:
- 它不会导致 OutOfMemoryError 错误,因为元空间的大小可以动态调整。
- 元空间使用本机内存,而不是 JVM 堆内存,这可以避免堆内存的碎片化问题。
- 元空间中的垃圾收集与堆中的垃圾收集是分离的,这可以避免应用程序在运行过程中因为进行类加载和卸载而频繁地触发 Full GC。
再画幅图,对比来看一下,就会一目了然。
永久代、方法区、元空间
- 方法区是 Java 虚拟机规范中的一个概念,就像是一个接口吧;
- 永久代是 HotSpot 虚拟机中对方法区的一个实现,就像是接口的实现类;
- Java 8 的时候,移除了永久代,取而代之的是元空间,是方法区的另外一种实现,更灵活了。
永久代是放在运行时数据区中的,所以它的大小受到 Java 虚拟机本身大小的限制,所以 Java 8 之前,会经常遇到 java.lang.OutOfMemoryError: PremGen Space 的异常,PremGen Space 就是方法区的意思;而元空间是直接放在内存中的,所以只受本机可用内存的限制。
2、String.intern()
-
第一,使用双引号声明的字符串对象会保存在字符串常量池中。
-
第二,使用 new 关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象。
-
第三,针对没有使用双引号声明的字符串对象来说,就像下面代码中的 s1 那样:
String s1 = new String("二哥") + new String("三妹");
如果想把 s1 的内容也放入字符串常量池的话,可以调用 intern() 方法来完成。
不过,需要注意的是,Java 7 的时候,字符串常量池从永久代中移动到了堆中,虽然此时永久代还没有完全被移除。Java 8 的时候,永久代被彻底移除。
这个变化也直接影响了 String.intern() 方法在执行时的策略。
Java 7 之前,执行 String.intern() 方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象;
Java 7 之后呢,由于字符串常量池放在了堆中,执行 String.intern() 方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象了,而是直接保存堆中对象的引用,也就节省了一部分的内存空间。
举例:
String s1 = new String("二哥三妹");
String s2 = s1.intern();
System.out.println(s1 == s2);//return ->false
第一行代码,字符串常量池中会先创建一个“二哥三妹”的对象,然后堆中会再创建一个“二哥三妹”的对象,s1 引用的是堆中的对象。
第二行代码,对 s1 执行 intern() 方法,该方法会从字符串常量池中查找“二哥三妹”这个字符串是否存在,此时是存在的,所以s2 引用的是字符串常量池中的对象。
也就意味着 s1 和 s2 的引用地址是不同的,一个来自堆,一个来自字符串常量池,所以输出的结果为 false。
下一个例子:
String s1 = new String("二哥") + new String("三妹");
String s2 = s1.intern();
System.out.println(s1 == s2);//return->true
第一行代码,会在字符串常量池中创建两个对象,一个是“二哥”,一个是“三妹”,然后在堆中会创建两个匿名对象“二哥”和“三妹”,最后还有一个“二哥三妹”的对象,s1 引用的是堆中“二哥三妹”这个对象。
第二行代码,对 s1 执行 intern() 方法,该方法会从字符串常量池中查找“二哥三妹”这个对象是否存在,此时不存在的,但堆中已经存在了,所以字符串常量池中保存的是堆中这个“二哥三妹”对象的引用,也就是说,s2 和 s1 的引用地址是相同的,所以输出的结果为 true。
再来解释一下 String s1 = new String("二哥") + new String("三妹") 这行代码。”我对三妹的表现非常开心。
- 创建 "二哥" 字符串对象,存储在字符串常量池中。
- 创建 "三妹" 字符串对象,存储在字符串常量池中。
- 执行
new String("二哥"),在堆上创建一个字符串对象,内容为 "二哥"。 - 执行
new String("三妹"),在堆上创建一个字符串对象,内容为 "三妹"。 - 执行
new String("二哥") + new String("三妹"),会创建一个 StringBuilder 对象,并将 "二哥" 和 "三妹" 追加到其中,然后调用 StringBuilder 对象的 toString() 方法,将其转换为一个新的字符串对象,内容为 "二哥三妹"。这个新的字符串对象存储在堆上。
也就是说,当编译器遇到 + 号这个操作符的时候,会将 new String("二哥") + new String("三妹") 这行代码编译为以下代码:
new StringBuilder().append("二哥").append("三妹").toString();
3、字符串拼接与拆分
1)字符串拼接
循环体内,拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符。原因就在于循环体内如果用 + 号操作符的话,就会产生大量的 StringBuilder 对象,不仅占用了更多的内存空间,还会让 Java 虚拟机不停的进行垃圾回收,从而降低了程序的性能。”
更好的写法就是在循环的外部新建一个 StringBuilder 对象,然后使用 append() 方法将循环体内的字符串添加进来:
append源码
public StringBuilder append(String str) {
super.append(str);
return this;
}
这 3 行代码其实没啥看的。我们来看父类 AbstractStringBuilder 的 append() 方法:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
1)判断拼接的字符串是不是 null,如果是,当做字符串“null”来处理。
2)获取字符串的长度。
3)ensureCapacityInternal() 方法。由于字符串内部是用数组实现的,所以需要先判断拼接后的字符数组长度是否超过当前数组的长度,如果超过,先对数组进行扩容,然后把原有的值复制到新的数组中。
4)将拼接的字符串 str 复制到目标数组 value 中。
5)更新数组的长度 count。
2)字符串拆分
假如现在有这样一串字符序列“沉默王二,一枚有趣的程序员”,需要按照中文逗号“,”进行拆分,这意味着第一串字符序列为逗号前面的“沉默王二”,第二串字符序列为逗号后面的“一枚有趣的程序员”。在拆分之前,要先进行检查,判断一下这串字符是否包含逗号,否则应该抛出异常。
如果你想把分隔符包裹在拆分后的字符串的第一部分,可以这样做:
String cmower = "沉默王二,一枚有趣的程序员";
if (cmower.contains(",")) {
String [] parts = cmower.split("(?<=,)");
System.out.println("第一部分:" + parts[0] +" 第二部分:" + parts[1]);
}
程序输出的结果如下所示:
第一部分:沉默王二, 第二部分:一枚有趣的程序员
可以看到分隔符,包裹在了第一部分,如果希望包裹在第二部分,可以这样做:
String [] parts = cmower.split("(?=,)");
“?<= 和 ?= 是什么啊?”三妹好奇地问。
“它其实是正则表达式中的断言模式。”我说,“你有时间的话,可以看看前面我推荐的两份开源文档。”
“split() 方法可以传递 2 个参数,第一个为分隔符,第二个为拆分的字符串个数。”我说。
String cmower = "沉默王二,一枚有趣的程序员,宠爱他";
if (cmower.contains(",")) {
String [] parts = cmower.split(",", 2);
System.out.println("第一部分:" + parts[0] +" 第二部分:" + parts[1]);
}
传递 2 个参数的时候,会直接调用 substring() 进行截取,第二个分隔符后的就不再拆分了。
二、小博哥-编程基础
1、dubbo
随着互联网场景中所要面对的用户规模和体量的增加,系统也需要做相应的拆分设计和实现。随之而来的,以前的一套系统,现在成了多个微服务。如:电商系统,以前就在一个工程中写就可以了,现在需要拆分出,用户、支付、商品、配送、活动、风控等各个模块。那么这些模块拆分后,如何高效的通信呢?
- 关于通信,就引入了 RPC 框架,而 Dubbo 就是其中的一个实现方式。
- 那为啥用 Dubbo 呢?其实核心问题就一个,为了提高通信效率。因为 Dubbo 的底层通信是 Socket 而不是 HTTP 所以通信的性能会更好。同时 Dubbo 又有分布式的高可用设计,在一组
- 部署了交易服务的实例宕机后,会被从注册中心摘除,之后流量会打到其他服务上。
Dubbo 的使用分为2方,一个是接口的提供方,另外一个是接口的调用方。接口的提供方需要提供出被调用方使用接口的描述性信息。这个信息包括:接口名称、接口入参、接口出参,只有让调用方拿到这些信息以后,它才能依托于这样的接口信息做一个代理操作,并在代理类中使用 Socket 完成双方的信息交互。
所以你看上去调用 RPC 接口好像和使用 HTTP 也没啥区别,无非就是引入了 POM 配置,之后再配置了注解就可以使用了。但其实,它是把你的 Jar 当做代理的必要参数使用了。
1)接口提供方
- 1. 接口定义
public interface IUserService {
Response<UserResDTO> queryUserInfo(UserReqDTO reqDTO);
}
所有的 Dubbo 接口,出入参,默认都需要继承 Serializable 接口。也就是 UserReqDTO、UserResDTO、Response 这3个类,都得继承 Serializable 序列化接口。
- 2.接口实现
@Slf4j
@DubboService(version = "1.0.0")
public class UserService implements IUserService
Dubbo 的实现接口,需要被 Dubbo 自己管理。所以 Dubbo 提供了 @DubboService 注解。
- 3.工程配置
application.yml
dubbo:
application:
name: xfg-dev-tech-dubbo
version: 1.0.0
registry:
address: zookeeper://127.0.0.1:2181 # N/A - 无zookeeper可配置 N/A 走直连模式测试
protocol:
name: dubbo
port: 20881
scan:
base-packages: cn.bugstack.dev.tech.dubbo.api
base-packages 扫描的是哪里配置了 Dubbo 的 API 入口,给它入口就行,它会自己找到实现类。但!你要知道 Java 的 Spring 应用能扫描到,能被 Spring 管理,那么 pom 要直接或者间接的引导到定义了 Dubbo 的模块。
再有一个问题,Spring 应用开发,讲究约定大于配置。你 Application 应用的包名,应该是可以覆盖到其他包名的。比如 Application 都配置到 cn.bugstack.dev.tech.dubbo.a.b.c.d.* 去了,它默认就扫不到 cn.bugstack.dev.tech.dubbo.api 了。
注意:address:如果配置的是 N/A 就是不走任何注册中心,就是个直连,主要用于本地验证的。如果你配置了 zookeeper://127.0.0.1:2181 就需要先安装一个 zookeeper 另外,即使你配置了注册中心的方式,也可以直连测试。
2)接口使用方
- POM 引入
<dependency>
<groupId>cn.bugstack</groupId>
<artifactId>xfg-dev-tech-dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
POM 的配置,就是把 Jar 包给引用进来。因为 Dubbo 需要根据这个接口,做一个代理操作。不引入,你代码就爆红啦!爆红啦!
- 消费配置
源码:application.yml
dubbo:
application:
name: xfg-dev-tech-dubbo
version: 1.0.0
registry:
address: zookeeper://127.0.0.1:2181
# address: N/A
protocol:
name: dubbo
port: 20881
配置了 zookeeper 你就用第一个,代码中对应 @DubboReference(interfaceClass = IUserService.class, version = "1.0.0")
配置了 N/A 你就用第二个,代码中必须指定直连。@DubboReference(interfaceClass = IUserService.class, url = "dubbo://127.0.0.1:20881", version = "1.0.0")
- 代码配置
源码:cn.bugstack.dev.tech.dubbo.consumer.test.ApiTest
// 直连模式;@DubboReference(interfaceClass = IUserService.class, url = "dubbo://127.0.0.1:20881", version = "1.0.0")
@DubboReference(interfaceClass = IUserService.class, version = "1.0.0")
private IUserService userService;
@Test
public void test_userService() {
UserReqDTO reqDTO = UserReqDTO.builder().userId("10001").build();
Response<UserResDTO> resDTO = userService.queryUserInfo(reqDTO);
log.info("测试结果 req: {} res: {}", JSON.toJSONString(reqDTO), JSON.toJSONString(resDTO));
}
2、原理分析
都说 Jar 是提供可描述性信息的,对方才能代理调用。那么这个过程是怎么干的呢,总不能一问这个,就让小卡拉米们去手写 Dubbo 呀!所以小傅哥会通过最简单模型结构,让你了解这个 Dubbo 通信的原理,方便小卡拉米们上手。
- 如果所示,接口使用方,对接口进行代理。什么是代理呢,代理就是用一个包装的结构,代替原有的操作。在这个包装的结构里,你可以自己扩展出任意的方法。
- 那么,这里的代理。就是根据接口的信息,创建出一个代理对象,在代理对象中,提供 Socket 请求。当调用这个接口的时候,就可以对接口提供方的,发起 Socket 请求了。
- 而 Socket 接收方,也就是接口提供方。他收到信息以后,根据接口的描述性内容,进行一个反射调用。这下就把信息给请求出来,之后再通过 Socket 返回回去就可以了。