Java 语言中一个显著的特点就是引入了垃圾回收机制,在编写程序的时候不再需要考虑内存管理。垃圾回收机制可以有效的防止内存泄露,提高内存的内存率。
垃圾回收器通常是作为一个单独的低级线程运行,不可预知的情况下对堆中已经死亡的或者长时间没有使用的对象进行清理和回收。
回收机制的算法有:标记清除算法、复制算法、标记压缩算法等等。
首先有三个代,新生代、老年代、永久代。
在新生代有三个区域:一个Eden区和两个Survivor区。当一个实例被创建了,首先会被存储Eden 区中。
具体过程是这样的:
Java内存模型决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,定义了线程和主内存之间的抽象关系。
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(并不真实存在),本地内存中存储的是在主内存中共享变量的副本。
有两条规定:
主要管理的是堆内存。
如果新创建的对象占用内存很大,则直接分配到老年代
解决了内存碎片化问题。整个过程中,永远有一个Survivor区是空的,另一个非空的Survivor区是无碎片的。
我们可以修改虚拟机的参数,获取Heap Dump的文件,后缀名是.hprof。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:\jvm
之后可以使用JDK自带的一个工具jvisualvm来进行排查和定位。
单位:字节
boolean(1) = byte(1) < short(2) = char(2) < int(4) = float(4) < long(8) = double(8)
实现Runnable接口这种方式更受欢迎,已经继承别的类的情况下只能实现接口。
触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
线程有三个状态,就绪态,运行态,等待态。Sleep(n)方法是让当前线程在n秒内不会参与CPU竞争。线程进入等待队列,n秒之后再次进入就绪队列。
Sleep(0)是让线程直接进入就绪状态。
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对象调用wait方法导致本线程放弃对象锁,进入等待池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入锁池准备抢夺对象锁。
Thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
可以使用synchronized、lock、volatile来实现同步。
volatile是轻量级的synchronized,比它的执行成本更低,因为它不会引起线程的上下文切换,它保证了共享变量的可见性,可见性的意思是当一个线程修改一个变量时,另外一个线程能读到这个修改的值。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。还有就是它通过添加内存屏障的方式禁止指令的重排序。
这是一个属于JUC的工具类,从1.5开始。主要用到方法是countDown() 和 await()。
思路:我们可以在创建CountDownLatch对象,然后将此对象通过构造参数传递给子线程,在开启子线程后主线程调用await()方法阻塞主线程,子线程调用countDown()方法计数器减一。
synchronize是java中的关键字,可以用来修饰实例方法、静态方法、还有代码块;主要有三种作用:可以确保原子性、可见性、有序性。
synchronized的底层原理是跟monitor有关,也就是视图器锁,每个对象都有一个关联的monitor,当Synchronize获得monitor对象的所有权后会进行两个指令:加锁指令跟减锁指令。
monitor里面有个计数器,初始值是从0开始的。如果一个线程想要获取monitor的所有权,就看看它的计数器是不是0,如果是0的话,那么就说明没人获取锁,那么它就可以获取锁了,然后将计数器+1,也就是执行monitorenter加锁指令;monitorexit减锁指令是跟在程序执行结束和异常里的,如果不是0的话,就会陷入一个堵塞等待的过程,直到为0等待结束。
主要参数有:
当需要任务大于核心线程数时候,就开始把任务往存储任务的队列里,当存储队列满了的话,就开始增加线程池创建的线程数量,如果当线程数量也达到了最大,就开始执行拒绝策略,比如说记录日志,直接丢弃,或者丢弃最老的任务,或者交给提交任务的线程执行。
当一个线程完成时,它会从队列中取下一个任务来执行。当一个线程无事可做,且超过一定的时间(keepAliveTime)时,如果当前运行的线程数大于核心线程数,那么这个线程会停掉了。
HashMap底层是基于数组+链表实现的,通过添加键的hashcode与上数组的长度来得到这个元素在数组中的位置,如果这个位置没有数据,那么就把这个数据当做第一个节点。如果这个位置有了链表,那么在JDK1.7的时候使用的是头插法,在JDK1.8的时候使用尾插法。
HashMap在JDK1.8的版本中引入了红黑树结构做优化,当链表元素个数大于等于8时,链表转换成树结构;链表元素个数小于等于6时,树结构还原成链表。
因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,显然树的效率更高一些。
链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是树和链表相互转换的时间也不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。
HashMap底层是数组,在第一次put的时候会初始化,发生第一次扩容到16。它有一个负载因子是0.75,下一次扩容的时候就是当前数组大小*0.75。扩大容量为原来的2倍。
ConcurrentHashMap大部分的逻辑代码和HashMap是一样的,主要通过synchronized和来保证节点在插入扩容的时候是线程安全的。
ConcurrentHashMap的扩容核心逻辑主要是给不同的线程分配不同的数组下标,然后每个线程处理各自下表区间的节点。同时处理节点复用了hashMap的逻辑,通过位运行,可以知道节点扩容后的位置,要么在原位置,要么在原位置+oldlength位置,最后直接赋值即可。
ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成的。HashEntry封装的就是每一个键值对。,每一个Segment元素存储的是HashEntry数组 + 链表。Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment本身可以充当锁的角色。ConcurrentHashMap在put的时候需要进行两次hash,第一次需要确定在Segment数组的位置,第二次hash是确定在HashEntry数组中的位置。同样在get的时候也需要经过两次hash。
@Component: 会被spring容器识别,并转为bean。
@Repository: 对Dao实现类进行注解。
@Service: 对业务逻辑层进行注解。
@Controller: 表明这个类是Spring MVC里的Controller,将其声明为Spring的一个Bean,Dispatch Servlet会自动扫描注解了此注解的类,并将Web请求映射到注解了@RequestMapping的方法上。
@RequestMapping: 用来映射Web请求(访问路径和参数)、处理类和方法的。它可以注解在类和方法上。注解在方法上的@RequestMapping路径会继承注解在类上的路径。
@RequestBody: 可以将整个返回结果以某种格式返回,如json或xml格式。
@PathVariable: 用来接收路径参数,如/news/001,可接收001作为参数,此注解放置在参数前。
@RequestParam:用于获取传入参数的值。
@RestController:是一个组合注解,组合了@Controller和@ResponseBody,意味着当只开发一个和页面交互数据的控制的时候,需要使用此注解。
我一般都是使用Servlet-Api,在处理请求的方法参数列表中,添加一个HTTPSession对象,之后SpringMVC就可以自动注入进来了。在方法体中调用session.setAttribute就可以了。
依赖注入的三种方式:
控制反转与依赖注入是同一个概念,引入IOC的目的:
具体做法:
AOP是面向切面编程,可以说是面向对象编程的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,但是这些都是纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,但是这与核心的业务代码确没有关系。
AOP利用"横切"的技术,把那些与业务无关,但是却为业务模块所共同调用的逻辑部分封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并提高了系统的维护性。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如日志还有事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
Springmvc的拦截器实现HandlerInterceptor接口后,会有三个抽象方法需要实现,分别为方法前执行preHandle,方法后postHandle,页面渲染后afterCompletion。
总结:
preHandle 按拦截器定义顺序调用
postHandler 按拦截器定义逆序调用
afterCompletion 按拦截器定义逆序调用
postHandler 在拦截器链内所有拦截器返成功调用
afterCompletion 只有preHandle返回true才调用
spring 框架中核心组件有三个:Core、Context 和 Beans。其中最核心的组件就是Beans, Spring提供的最核心的功能就是Bean Factory。
Spring 解决了的最核心的问题就是把对象之间的依赖关系转为用配置文件来管理,也就是Spring的依赖注入机制。这个注入机制是在Ioc 容器中进行管理的。
共同点:两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。
@Autowired注解是按照类型(byType)装配依赖对象。当有且仅有一个匹配的Bean时,Spring将其注入@Autowired标注的变量中。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。
@Resource默认按照ByName自动注入。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。
可以定义多个配置文件,比如开发,测试,上线。 我们可以在SpringBoot中定义多个application.properties。 我一般都用-名字做区别,比如:
application-dev.properties
application-test.properties
application-prod.properties
之后我们需要在默认的配置文件里面声明一下激活哪些配置文件。
spring.profiles.active=test
使用java -jar 方式启动的时候也可以添加参数指定配置文件启动
java -jar mm.jar --spring.profiles.active=dev
MyBatis一级缓存最大的共享范围就是一个SqlSession内部,那么如果多个SqlSession需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用CachingExecutor装饰Executor进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。
当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个共同的 cache,也就是二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。默认二级缓存不开启,需要在MyBatis的全局配置文件中进行配置。
MySQL 支持多种类型的数据库引擎,可分别根据各个引擎的功能和特性为不同的数据库处理任务提供各自不同的适应性和灵活性。在 MySQL 中,可以利用 SHOW ENGINES 语句来显示可用的数据库引擎和默认引擎。
MySQL 提供了多个不同的存储引擎,包括处理事务安全表的引擎和处理非事务安全表的引擎。在 MySQL 中,不需要在整个服务器中使用同一种存储引擎,针对具体的要求,可以对每一个表使用不同的存储引擎。
MySQL 5.7 支持的存储引擎有 InnoDB、MyISAM、Memory、Merge、Archive、Federated、CSV、BLACKHOLE 等。
使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析查询语句或是表结构的性能瓶颈。
Explain + SQL语句
通过Explain,我们可以获取以下信息:
EXPLAIN SELECT * FROM USER;
显示的结果一般不会全部去关注,比较关注的有:
id是查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。
id号每个号码,表示一趟独立的查询。一个sql的查询趟数越少越好。
第二个是type,显示的是访问类型。如果是All就代表是全表扫描。需要进行优化。一般来说,得保证查询至少达到range级别,最好能达到ref。
然后是possible_keys:sql所用到的索引
还有一个就是key,这个就是实际使用的索引。如果为NULL,则没有使用索引。
然后key_len表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。key_len字段能够帮你检查where条件是否充分的利用上了索引。key_len越长,查询效率越高。
rows列显示MySQL认为它执行查询时必须检查的行数。行数越少,效率越高!
可以使用alter添加列,这样原有的数据不会改变,新增的字段值是null。 还可以使用Navicat或者SQLyog这些可视化工具修改表的结构,效果和上面的一样
MyISAM支持表锁,InnoDB支持表锁和行锁,默认行锁。
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端数据库查。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。 解决穿透的一种办法是对接口做校验,然后也可以对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后清理缓存。
缓存雪崩就是当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。我们可以做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。或者我们对不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
Redis为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis的持久化策略有两种:1. RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。 当子进程完成写临时文件后,将原来的RDB替换掉。1. AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。
不会,其实有三种不同的删除策略:
先更新数据库,再删缓存。数据库的读操作的速度远快于写操作的,所以脏数据很难出现。可以对异步延时删除策略,保证读请求完成以后,再进行删除操作。
可以使用Java连接Redis,获得指定hash的所有值,然后做正则验证。
key
keys * 获取所有的key
select 0 选择第一个库
move myString 1 将当前的数据库key移动到某个数据库,目标库有,则不能移动
flush db 清除指定库
randomkey 随机key
type key 类型
set key1 value1 设置key
get key1 获取key
mset key1 value1 key2 value2 key3 value3
mget key1 key2 key3
del key1 删除key
exists key 判断是否存在key
expire key 10 10过期
pexpire key 1000 毫秒
persist key 删除过期时间
string
set name cxx
get name
getrange name 0 -1 字符串分段
getset name new_cxx 设置值,返回旧值
mset key1 key2 批量设置
mget key1 key2 批量获取
setnx key value 不存在就插入(not exists)
setex key time value 过期时间(expire)
setrange key index value 从index开始替换value
incr age 递增
incrby age 10 递增
decr age 递减
decrby age 10 递减
incrbyfloat 增减浮点数
append 追加
strlen 长度
getbit/setbit/bitcount/bitop 位操作
hash
hset myhash name cxx
hget myhash name
hmset myhash name cxx age 25 note "i am notes"
hmget myhash name age note
hgetall myhash 获取所有的
hexists myhash name 是否存在
hsetnx myhash score 100 设置不存在的
hincrby myhash id 1 递增
hdel myhash name 删除
hkeys myhash 只取key
hvals myhash 只取value
hlen myhash 长度
list
lpush mylist a b c 左插入
rpush mylist x y z 右插入
lrange mylist 0 -1 数据集合
lpop mylist 弹出元素
rpop mylist 弹出元素
llen mylist 长度
lrem mylist count value 删除
lindex mylist 2 指定索引的值
lset mylist 2 n 索引设值
ltrim mylist 0 4 删除key
linsert mylist before a 插入
linsert mylist after a 插入
rpoplpush list list2 转移列表的数据
set
sadd myset redis
smembers myset 数据集合
srem myset set1 删除
sismember myset set1 判断元素是否在集合中
scard key_name 个数
sdiff | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集
srandmember 随机获取集合中的元素
spop 从集合中弹出一个元素
zset
zadd zset 1 one
zadd zset 2 two
zadd zset 3 three
zincrby zset 1 one 增长分数
zscore zset two 获取分数
zrange zset 0 -1 withscores 范围值
zrangebyscore zset 10 25 withscores 指定范围的值
zrangebyscore zset 10 25 withscores limit 1 2 分页
Zrevrangebyscore zset 10 25 withscores 指定范围的值
zcard zset 元素数量
Zcount zset 获得指定分数范围内的元素个数
Zrem zset one two 删除一个或多个元素
Zremrangebyrank zset 0 1 按照排名范围删除元素
Zremrangebyscore zset 0 1 按照分数范围删除元素
Zrank zset 0 -1 分数最小的元素排名为0
Zrevrank zset 0 -1 分数最大的元素排名为0
Zinterstore
zunionstore rank:last_week 7 rank:20150323 rank:20150324 rank:20150325 weights 1 1 1 1 1 1 1
排序:
sort mylist 排序
sort mylist alpha desc limit 0 2 字母排序
sort list by it:* desc by命令
sort list by it:* desc get it:* get参数
sort list by it:* desc get it:* store sorc:result sort命令之store参数:表示把sort查询的结果集保存起来
可能因为各种原因,导致了生产端发送了多条一样的消息给消费端,但是,消费端也只能消费一条,不会多消费。可以使用唯一ID + 指纹码机制防止消息被重复消费。
指纹码(就是时间戳 + 业务的一些规则, 来保证id + 指纹码在同一时刻是唯一的,不会出现重复)。
将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。
通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。同样可以达到解耦的效果。
SpringBoot整合ElasticSearch有一个searchSourceBuilder,通过链式调用一个highlighter方法,传入一个HighlightBuilder对象并设置好查询的列和高亮的标签。
之后调用RestHighLevelClient对象的Search方法之后返回一个SearchResponse对象,之后可以调用response.getHits().getHits();获得击中的结果数组,数组中每一个对象除了包含原始内容还包含了一个高亮结果集,是一个Map集合。
首先需要导入spring-boot-starter-data-elasticsearch,在Spring官网的data项目里面有详细的文档介绍,官方强烈建议使用 High Level REST Client来操作ES。之后需要添加一个配置类,在官方文档有介绍。之后我们就可以通过Spring容器来管理获取HighLevelRESTClient对象了。
Maven有三套生命周期,分别是clean、default、site,每个生命周期都包含了一些阶段(phase)。三套生命周期相互独立,但各个生命周期中的phase却是有顺序的,且后面的phase依赖于前面的phase。执行某个phase时,其前面的phase会依顺序执行,但不会触发另外两套生命周期中的任何phase。
clean的生命周期:
pre-clean:执行清理前的工作;
clean:清理上一次构建生成的所有文件;
post-clean:执行清理后的工作
default的生命周期:default生命周期是最核心的,它包含了构建项目时真正需要执行的所有步骤。
validate
initialize
generate-sources
process-sources
generate-resources
process-resources :复制和处理资源文件到target目录,准备打包;
compile :编译项目的源代码;
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources
test-compile :编译测试源代码;
process-test-classes
test :运行测试代码;
prepare-package
package :打包成jar或者war或者其他格式的分发包;
pre-integration-test
integration-test
post-integration-test
verify
install :将打好的包安装到本地仓库,供其他项目使用;
deploy :将打好的包安装到远程仓库,供其他项目使用;
site的生命周期:
pre-site
site :生成项目的站点文档;
post-site
site-deploy :发布生成的站点文档
握手过程:
至于为什么要发送第三条是因为在发送第一条的时候,可能因为网络原因导致数据报滞留,那么超过一定时间主机A会再次发送请求连接的数据报文。之后主机B返回确认连接报文,如果主机A收到确认报文之后不发送第三条报文告诉主机B自己已经收到了,那么B其实是不知道的,这时候可能A第一次发送的原本滞留的报文突然正常了,B就再次收到了请求连接的报文,但是实际上A已经连接了。
挥手过程:
其实在客户端断开和服务器的单向连接之后,服务器仍然可以往客户端发送数据,需要处理一下事情。
客户端需要最后等一段时间才能进入关闭状态是因为:客户端无法保证最后发送的ACK报文会一定被对方收到,所以有时候需要重发可能丢失的ACK报文。
更多Java进阶学习资料、2022大厂面试真题,关注我,主页自取