这要分成两种情况。

第一种情况是服务端发数据给客户端,因为客户端此时已经崩溃了,所以没办法 ACK 服务端的报文。那么会触发服务端的重试功能,在超过重试上限之后,服务端会判定连接不可用,直接关闭连接。

第二种情况是,服务端和客户端之间没有啥报文要发的,而且开启了保活机制,那么超过保活期限,那么服务端也会关闭连接。

具体来说,TCP 设有一个保活计时器。服务器每收到一次客户端的数据,都会重新复位这个计时器,时间通常是设置为 2 小时。若 2 小时还没有收到客户端的任何数据,服务器就开始重试:每隔 75 分钟发送一个探测报文段,若一连发送 10 个探测报文后客户端依然没有回应,那么服务器就认为连接已经断开了。

服务端最终认定客户端崩了,关闭连接;

通过可达性分析算法判定对象是否存活:

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  • Java 虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象 (比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4 种

  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在, 垃圾收集器就永远不会回收掉被引用的对象,即使OOM。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了 SoftReference 类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了 WeakReference 类来实现弱引用。
  • 虚引用是最弱的一种引用关系。虚引用和引用队列联合使用,用来追踪对象的回收情况。虚拟机回收对象时,如果发现对象还存在虚引用,会在回收对象后将引用加入到关联的引用队列中。程序可以通过观察引用队列的方式,来感知对象即将被垃圾回收的时机。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

对象自救:

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”,没有必要执行则直接回收。

如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了

Java 代码首先被编译为字节码(.class),JVM 在运行时通过解释器执行字节码。当某部分的代码被频繁执行时,JIT 会将这些热点代码编译为机器码,以此来提高程序的执行效率。

那为什么 JIT 就能提高程序的执行效率呢,解释器不也是将字节码翻译为机器码交给操作系统执行吗?

解释器在执行程序时,对于每一条字节码指令,都需要进行一次解释过程,然后执行相应的机器指令。这个过程在每次执行时都会重复进行,因为解释器不会记住之前的解释结果。

与此相对,JIT 会将频繁执行的字节码编译成机器码。这个过程只发生一次。一旦字节码被编译成机器码,之后每次执行这部分代码时,直接执行对应的机器码,无需再次解释。

除此之外,JIT 生成的机器码更接近底层,能够更有效地利用 CPU 和内存等资源,同时,JIT 能够在运行时根据实际情况对代码进行优化(如内联、循环展开、分支预测优化等),这些优化是在机器码级别上进行的,可以显著提升执行效率。

Java 的执行过程分为两步,第一步由 javac 将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析。

第二步,解释器会逐行解释字节码并执行,在解释执行的过程中,JVM 会对程序运行时的信息进行收集,在这些信息的基础上,JIT 会逐渐发挥作用,它会把字节码编译成机器码,但不是所有的代码都会被编译,只有被 JVM 认定为热点代码,才会被编译。

JVM 中有一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被认定为热点代码,然后编译存入 codeCache 中。当下次执行时,再遇到这段代码,就会从 codeCache 中直接读取机器码,然后执行,以此来提升程序运行的性能。

为什么不一开始就把所有字节码翻译成机器码?

首先JVM是运行字节码的,这是他能够跨平台运行的核心,所以必须运行字节码,而运行字节码的过程中,如果遇到一条字节码就把他翻译成机器码并存储下来,这是需要空间存储的,如果是冷门代码,得不偿失

JIT优化手段:

锁消除:如果synchronized锁住的区域经过JIT分析发现不会产生线程安全问题,会把锁消除掉

标量替换:某对象没有逃逸出方法,那么这个对象的字段可以拆成标量

栈上分配:对象没有逃逸出方法,就可以在栈上分配,本质其实是标量替换

方法内联:简单的方法可以直接内联到调用处

逃逸分析:

全局逃逸:对象超出方法或线程范围,比如存储在静态字段或者作为方法的返回值

参数逃逸:对象被作为参数传毒,但方法调用期间不会全局逃逸

无逃逸:对象没有逃逸出方法

未关闭的资源,如文件、数据库连接、网络连接,使用完后没正确关闭

集合是静态的,此时静态集合无法被垃圾回收,里面的对象也无法被回收,例如ThreadLocalMap

不正确的引用,A->B,此时B不需要了,但A没取消对B的引用,B无法释放

线程池没终止,线程对象无法被回收

Redis集群由多个节点组成,每个节点都是一个主从集群。整体结构如下:

在这种结构之下,现在就会有一个问题,当我存放一个键值对的时候,放哪个节点上?

对此 Redis Cluster 用的是槽映射的解决方案。Redis Cluster 将所有的key 按照 CRC16 算法映射到16384个槽,这些槽会被分配到这些节点上。大多数情况下槽是均匀分配的,但是小部分情况并不会均匀分配。

整个结构如下图:

所以 Redis 的高可用就源自两方面:

  • 如果节点内的主节点崩溃了,那么从节点经过主从选举就可以顶上;
  • 如果某个节点全崩溃了,那么还有别的节点可以用。虽然会损失数据,但是不至于完全不可用;

对于sentinel和cluster应该选哪种,单机无瓶颈就选sentinel,单机有瓶颈就选cluster

Redis Cluster 是一个对等结构和主从结构的混合架构。Redis Cluster 由多个节点组成,这些节点之间地位是平等的,也就是说它们构成了一个对等结构。

但是从细节上来说,每一个节点都是一个主从集群,也就是说每一个节点都是类似于 Redis Sentinel 模式,并借此来保证高可用。

Redis Cluster 借鉴一致性哈希的思想,利用 CRC16 将 key 分散到 16384 个槽(哈希槽就相当于一致性哈希中的虚拟节点)上面,而后再次将这些槽分配给不同的节点。可以平均分,也可以不是平均分。

通过这种混合模式,Redis 能有效应对各种问题。

首先是从对等结构上来说,就算是某个节点彻底不可用,也不会影响到别的节点,整个集群还是能够提供有损服务的。

而从主从结构上来说,通过数据同步和主从选举,这样即便主节点崩溃了, 也能选举出来一个新的从节点顶上。

Redis Cluster 能够撑住极高的并发,并且能够提供极高的可用性,所以已经成了当下大规模分布式系统里面的核心组件。

二、pipeline 是什么

pipeline 是客户端的一种 批量发送命令 的方式:

它允许客户端一次性把多个命令发到 Redis 服务器,然后 Redis 一次性返回结果。
减少了网络往返(RTT),因此性能更高。

✅ 在单机 Redis 下,pipeline 可以极大提高性能。
❌ 但在 Redis Cluster 下,有一个问题:

但是 Redis Cluster 并不是毫无缺点,最大的问题就是难以处理跨槽的问题。

这最典型的例子就是 pipeline。例如说在 pipeline 里面要处理分散在不同槽上的多个 key,那么pipeline 就会返回错误,这需要客户端进行处理。而有些语言的 Redis 客户端其实没有那么智能。

从我个人使用经验上来说,在使用 Redis Cluster 的时候,就要避免跨槽的问题。即便使用 Redis pipeline,如果跨槽其实意义就不大了,毕竟我用 pipeline 就是为了高性能,即便我的客户端能帮我处理跨槽的问题,但是性能还是损耗极大。

所以我即便要操作跨槽的 key,也更加倾向于自己将 key 分组,落到同一个节点上的 key 作为一组,而后分批操作。这样分组之后,用 pipeline 也就没有跨槽的问题了。

Redis Cluster 这种对等集群和主从集群的混合模式,在别的中间件里面也能看到类似的设计,甚至于可以说现代的大规模分布式软件的高可用都是通过这种设计来保证的。

举个例子来说,Kafka 的一个 Topic 有多个分区,这些分区之间地位是平等的,所以可以看做是对等结构。而每一个分区本身也是一个主从结构,也有数据复制和主从选举。所以Kafka 就算一个分区出问题,或者逻辑分区的主分区出现问题,依旧能够正常对外提供服务。

再举个例子来说,MySQL 的分库分表也可以看做是这种形态。一个逻辑表被分库分表之后,每一个物理表地位都是平等的,也就是可以看做是对等结构。而每一个物理表都是存储在 MySQL 主从集群上的,那么也就是说物理表本身也有主表和从表。通过这种混合模式可以保证极高的可用性。

clone:用于克隆对象

equals:用于比较两个对象是否相同

getClass:获取到对象的类对象,也就是Class对象

hashCode:获取到对象的hash值

toString:返回对象的字符串表示形式,一般交给子类重写

notify:当对象被当作锁时使用,让出锁使用权,随机唤醒一个阻塞线程

notifyAll:同上,但会唤醒所有线程竞争锁

wait:当对象被当作锁时使用,让出锁使用权,阻塞等待

finalize:用于对象垃圾回收自救,子类重写,将自己和引用链搭上关系,垃圾回收器会调用该方法,搭上关系了就不再清理该对象,但只会调用一次,即如果调用了一次,搭上关系了,后面关系断了,又被垃圾回收器盯上了,那么直接回收,不再给自救的机会。当然也可以用作其它作用,比如清理释放一些资源,当对象被垃圾回收时,finalize中进行一些善后处理,但finalize的调用时机是由垃圾回收器决定的,可能并不会在对象成为垃圾后立马被调用,所以不推荐这种方式,该方法jdk9被标记为过时

为什么finalize方法非常不好,非常影响性能?

非常不好:

FinalizerThread是守护线程,代码很有可能还没来得及执行完,线程就结束了,造成资源没有正确释放

finalize方法中的异常会被吞掉,不抛出,可能不能判断有没有在释放资源时发送错误

影响性能:

重写了finalize方法的对象在第一次被gc时,不能及时释放内存,需要等待FinalizerThread调用完finalize,第二次gc时才能真正释放内存

gc时说明内存本就不足,finalize调用又慢(涉及到队列的移除等操作),finalize中可能还有释放连接资源等耗时操作,不能及时释放内存,这可能会让对象移到老年代(内存担保机制),老年代积累垃圾过多,可能触发full gc

hashCode依赖的字段最好是不变的,如果易变,可能出现这种情况:把一个对象放入map中,然后更改这个对象的属性,那么这个对象的hashCode也会变,那么再get时,就get不出来了!

  • 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  • 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。

默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。那么编译器是希望我们手动赋值,还是帮我们自动赋值呢?我觉得应该是前者,因为这能很好让我们程序员在写代码的时候先初始化再使用一个变量,很清楚的知道使用一个变量时,它的值是多少,这才是比较好的规范。

所以成员变量和局部变量都应该是没有默认值的,我们需要先手动赋值,再使用变量。

对于编译器(javac)来说,局部变量没手动赋值很好判断,因为就在一个方法代码块中。没有手动赋值可以直接报错。

而成员变量可能是运行时手动赋值,编译器不知道在哪个方法就被赋值了,甚至在哪个地方可能反射给它赋值,这就无法判断是先使用还是先赋值初始化,而误报“没默认值”又会影响用户体验(都tm运行了你和我说我代码有问题?为什么编译不帮我检查出来?),所以采用自动赋默认值

kotlin就得强制赋值

字符串常量池在运行时常量池中,jdk7开始字符串常量池则被移入到堆中,就是说jdk7开始字符串常量池和常量池分开了。

当在双引号””中有字面量时,就会在串池创建一个该对象,如出现“a”,就会在串池中有个”a”,又如String s = “b”;那么串池中就有个”b”,这里注意,除了后面说的一种特殊情况,其它正常来说只要在双引号有字面量,就会在串池创建对象,如String s = new String(“c”);这里既有双引号,又有关键字new,就会在串池和堆中分别创建”c”和一个字符串对象”c”,这句话创建了两个对象!但s的引用是指向堆中的字符串对象。

当要在串池中创建对象时,会去检查串池中有没有该对象,没有才创建,如String s1 = “a”;String s2 = “a”,则s1和s2的地址是一样的,都是指向串池中的”a”,但如果是String s1 = new String(“a”);String s2 = new String(“a”),则s1和s2的地址是不一样的,因为两者分别指向堆中的两个不同的字符串对象,当然,在创建s1的时候在串池中创建”a”,但创建s2时发现串池中已经有”a”了,就不会再创建。

字符串拼接有两种,一种是字符串常量拼接,另一种是字符串变量拼接。当加号+左右都是常量时才是常量拼接,当出现一个变量或加号左右都是变量时就是变量拼接

编译器在编译期间(javac)会先把所有常量拼接好,换句话说class文件中只有变量拼接。先说字符串常量拼接吧,字符串常量拼接原理是编译器优化,如有String s1 = “ab”;String s2 = “a” + “b”;这里s2是由两个常量拼接来的,在编译器这句话就会变成String s2 = “ab”;也就是说在class文件就没有”a”和”b”了,这就是之前说的特殊情况,java文件中的双引号中有字面量,但不会在串池中创建对象,因为被编译器优化了,实际上在class文件中没有”a”和”b”,故这里s1和s2都是串池中的”ab”,是同一个串池对象。

再说字符串变量拼接,字符串变量拼接原理是StringBuilder,就是说有String s1 = “a”;String s2 = “b”;String s3 = s1 +s2;那么这句话本质其实是,String s3 = new StringBuilder().append(“a”).append(“b”).toString();可以理解为String s3 = new String(“ab”),但这里不会往串池中放入”ab”,因为这是拼接后的结果,class文件中没有”ab”。最终结果是:串池中有”a”,”b”,堆中有个字符串对象”ab”,过程中出现了个StringBuilder对象。当然,串池从jdk7开始也是在堆中,但只是堆单独划出的一部分,没有和堆融合。再补充一点,被final修饰的变量可以当常量处理。

那么,String s = new String(“a”) + new String(“b”);这句话创建了几个对象呢,首先有双引号,双引号中有字面量”a”,”b”,所以会在串池中创建两个对象”a”,”b”,因为还有两个new,所以会在堆中创建两个字符串对象,对象的值也分别为”a”,”b”,但和串池中的不是同一个,一个在串池,一个在堆中。又因为这是字符串变量拼接,所以还有new一个StringBuilder对象,最后结果是String s = new String(“ab”);又在堆中创建了一个字符串对象”ab”,所以一句话总共创建了六个对象,串池中”a”,”b”,堆中”a”,”b”,”ab”还有一个StringBuilder。

String s = new String(“a”) +”a”+”b”;这句话创建了几个对象?5个(串池中a,ab,堆中a,aab,stringBuilder)

intern方法会将一个字符串对象主动放入串池:

  • 如果串池中已经有这个字符串:

那么就不会再放入,并返回串池中该对象的引用。

  • 如果串池中没有该字符串:
    • jdk7以前:新建一个字符串对象放入串池,也就是说串池和堆中的对象不是同一个,然后会返回串池里的那个的引用。
    • jdk7开始:把堆中的引用放入串池,即串池和堆中的对象是同一个,方法返回串池中的引用。即这里串池中的地址,堆中的地址和返回的地址是同一个,都源自于堆中的那个对象。

比如有String s1 = “a”;String s2 = s1.intern();那么s1==s2的,因为串池中已经有”a”了,intern()返回的是串池中的引用。

再比如String s1 = new String(“a”) + new String(“b”);String s2 = s1.intern(),在jdk7以前,s1 != s2,因为intern是串池中创建新的对象,两个”ab”是不同的;jdk7开始s1==s2,因为intern是把堆中的地址放入串池,串池和堆中的”ab”是同一个对象。