JAVA基础
Integer:-128到127范围和范围外的区别
以下代码的执行结果是: True
,False
public static void main(String[] args) {
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
- 答案:
- 整型字面量的值在-128到127之间,不会new新的Integer对象,而是直接引用常量池中的Integer对象
- 所以 -128到127可以直接使用
==
,此范围之外的比较则需要equals()
如何实现对象克隆?
- 答案:
- 实现
Cloneable
接口并重写Object
类中的clone()
方法; - 实现
Serializable
接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下:
- 实现
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class MyUtil {
private MyUtil() {
throw new AssertionError();
}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) throws Exception {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bout);
oos.writeObject(obj);
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bin);
return (T) ois.readObject();
// 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
}
}
- 注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时
阐述final、finally、finalize的区别
- final(修饰符:关键字):
- 如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。
- 将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。
- 被声明为final的方法只能使用,不能在子类中被重写。
- finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
- finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。
JVM
JVM的主要组成部分及其作用
-
JVM包括
类加载子系统
、堆
、方法区
、栈
、本地方法栈
、程序计数器
、直接内存
、垃圾回收器
、执行引擎
。 -
类加载子系统
- 类加载子系统负责加载class信息,加载的类信息存放于方法区中。
- 直接内存
- 直接内存是在Java堆外的、直接向系统申请的内存空间。
- 访问直接内存的速度会优于Java堆。出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
- 垃圾回收器
- 垃圾回收器可以对堆、方法区、直接内存进行回收。
- 执行引擎
- 执行引擎负责执行虚拟机的字节码,虚拟机会使用即时编译技术将方法编译成机器码后再执行。
jvm 运行时数据区/栈(stack)、堆(heap)和方法区(method area)的用法。
-
堆(heap):通过new关键字和构造器创建的对象,堆是垃圾收集器管理的主要区域
-
栈(stack):基本数据类型的变量,对象的引用,函数调用的现场
-
方法区(method area):已经被JVM加载的:类信息、常量、静态变量、JIT编译器编译后的代码等数据
-
常量池:程序中的字面量(literal),直接定义的100、"hello"和常量,常量池是方法区的一部分
-
方法区和堆都是各个线程共享的内存区域
-
现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured
-
String str = new String("hello");
此语句中:变量str在栈、new创建出来的字符串对象在堆、"hello"这个字面量在方法区。 -
栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整
-
栈空间用光了会引发
StackOverflowError
,而堆和常量池空间不足则会引发OutOfMemoryError
-
补充:
- 较新版本的Java(从Java 6的某个更新开始)中,由于JIT编译器的发展和"逃逸分析"技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。
- 运行时常量池相当于Class文件常量池具有动态性,Java语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String类的intern()方法就是这样的。
描述一下JVM加载class文件的原理机制
- 答案:
- JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的。
- Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
- 由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
- 类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。
- 当类被加载后进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)。
- 最后JVM对类进行初始化,包括:
- 如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
- 如果类中存在初始化语句,就依次执行这些初始化语句。
- 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(
java.lang.ClassLoader
的子类)。 - 从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。
- PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。
- 类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。
- JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
- Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
- Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
- System:又叫应用类加载器,其父类是
Extension
。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path
所指定的目录中记载类,是用户自定义加载器的默认父加载器。
Java 中会存在内存泄漏吗,请简单描述
- 答案:
- 理论上Java因为有**垃圾回收机制(GC)**不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);
- 然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。
- 例如Hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。
- 下面例子中的代码会导致内存泄露:
- 此代码实现了一个栈(先进后出(FILO))结构,看着似乎没有什么明显的问题,它可以通过你编写的各种单元测试。
- 但其中的pop方法却存在内存泄露的问题,当我们用pop方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete reference)。
- 在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发
Disk Paging
(物理内存与硬盘的虚拟内存交换数据),甚至造成OutOfMemoryError
。
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack() {
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem) {
ensureCapacity();
elements[size++] = elem;
}
public T pop() {
if(size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
Redis
Redis有哪些功能?
- 基于本机内存的缓存。
- Redis持久化的功能
- 哨兵(Sentinel)和复制
- Sentinel可以管理多个Redis服务器,它提供了监控、提醒以及自动的故障转移功能;
- 复制则是让Redis服务器可以配备备份的服务器;
- Redis也是通过这两个功能保证Redis的高可用;
- 4、集群(Cluster)单台服务器资源是有上限的
- CPU和IO资源:通过主从复制,进行读写分离,把一部分CPU和IO的压力转移到从服务器上,主从模式只是数据的备份,并不能扩充内存。
- 内存资源上限:横向扩展,让每台服务器只负责一部分任务,然后将这些服务器构成一个整体,对外界来说,这一组服务器就像是集群一样。
Redis单线程?
为什么是单线程
- 代码更清晰,处理逻辑更简单;
- 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;
- 不存在多线程切换而消耗CPU;
- 无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善(集群);
真的是单线程的吗?
- Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;
- redis内部使用了基于epoll的多路服用,也可以多部署几个redis服务器解决单线程的问题;
- redis主要的性能瓶颈是内存和网络;
- 内存瓶颈解决:增加内存
- 网络瓶颈解决:redis6.0引入了多线程的概念
- redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等
- 执行命令的核心模块还是单线程的。
Redis持久化的几种方式?
- redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
- RDB:简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上;
- AOF:则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
- 其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式来进行数据恢复,这是因为AOF方式的数据恢复完整度更高。
- 如果没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。
jedis 和 redisson 的区别?
- Jedis 和 Redisson 都是Java中对Redis操作的封装。
- Jedis 只是简单的封装了 Redis 的API库,可以看作是Redis客户端,它的方法和Redis 的命令很类似。
- Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 更加大。
- Jedis相比于Redisson 更原生一些,更灵活。
保证缓存和数据库数据的一致性
1、淘汰缓存
数据如果为较为复杂的数据时,进行缓存的更新操作就会变得异常复杂,因此一般推荐选择淘汰缓存,而不是更新缓存。
2、选择先淘汰缓存,再更新数据库
- 假如先更新数据库,再淘汰缓存,如果淘汰缓存失败,那么后面的请求都会得到脏数据,直至缓存过期。
- 假如先淘汰缓存再更新数据库,如果更新数据库失败,只会产生一次缓存穿透,相比较而言,后者对业务则没有本质上的影响。
3、延时双删策略
- 如下场景:同时有一个请求A进行更新操作,另一个请求B进行查询操作。
请求A进行写操作,删除缓存
请求B查询发现缓存不存在
请求B去数据库查询得到旧值
请求B将旧值写入缓存
请求A将新值写入数据库
次数便出现了数据不一致问题。采用延时双删策略得以解决。
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
这么做,可以在限定时间内将造成的缓存脏数据再次删除。这个时间设定可根据业务场景进行一个调节。
4、数据库读写分离的场景
- 两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
- 依旧采用延时双删策略解决此问题。
- 请求A进行写操作,删除缓存
- 请求A将数据写入数据库了,
- 请求B查询缓存发现,缓存没有值
- 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
- 请求B将旧值写入缓存
- 数据库完成主从同步,从库变为新值
缓存穿透、击穿、雪崩? 解决方法?
缓存穿透
理解/概念
- 指查询一个根本不存在的数据,缓存层和持久层都不会命中。
- 缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义。
解决方法
- 缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)
- 适用于数据命中不高、数据变化频繁、实时性高的应用场景,代码维护简单,缓存空间占用多,数据不一致
- value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
- 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
- 布隆过滤器(Bloom Filter)
- 适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少
- 在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在再进入缓存层、存储层。可以使用bitmap做布隆过滤器。
- 布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。
- 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
- 布隆过滤器拦截的算法描述:
1. 初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。
2. 添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。
3. 判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率。
缓存击穿
理解/概念
- 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃
- 系统中存在以下两个问题时需要引起注意:
- 当前key是一个热点key(例如一个秒杀活动),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。
解决方法
- 分布式互斥锁
- 只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。set(key,value,timeout)
- 永不过期
- 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
- 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存
缓存雪崩
理解/概念
- 如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
解决方法
- 分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。
- 缓存层高可用:可以把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。
- 二级缓存、双缓存策略:采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底
- 数据预热:可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
- 加锁排队. 限流-- 限流算法. 1.计数 2.滑动窗口 3. 令牌桶Token Bucket 4.漏桶 leaky bucket [1]
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
Redis怎么实现分布式锁?
- redis命令:set users 10 nx ex 12 原子性命令--------可以通过lua脚本,保证分布式锁的原子性
实现思想为:
- 获取锁的时候,使用setnx加锁,key为锁名,value值为一个随机生成的UUID。
- 获取锁成功后,使用expire命令为锁添加一个超时时间,若超过这个时间则放弃获取锁。
- 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
//使用uuid,解决锁释放的问题
@GetMapping
public void testLock() throws InterruptedException {
String uuid = UUID.randomUUID().toString();
Boolean b_lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
if(b_lock){
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)){
return;
}
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",++num);
Object lockUUID = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUUID.toString())){
redisTemplate.delete("lock");
}
}else{
Thread.sleep(100);
testLock();
}
}
- Redisson 实现
Redis分布式锁有什么缺陷?
Redis 分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。
Redis容易产生的几个问题:
- 锁未被释放
- B锁被A锁释放了
- 数据库事务超时
- 锁过期了,业务还没执行完
- Redis主从复制的问题
Redis如何做内存优化?
- 缩短键值的长度
- 缩短值的长度才是关键,如果值是一个大的业务对象,可以将对象序列化成二进制数组;
- 首先应该在业务上进行精简,去掉不必要的属性,避免存储一些没用的数据;
- 其次是序列化的工具选择上,应该选择更高效的序列化工具来降低字节数组大小;
- 以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等
- 共享对象池
- 对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。
- 字符串优化
- 编码优化
- 控制key的数量