这份面试手稿是自己准备求职期间进行总结的一份面试记录,主要关于
java
的知识点进行记录
面试手稿:
首先自我介绍:
尊敬的面试官您好;我叫aritso,毕业于皇家理工大学物联网工程专业的2020届本科毕业生,面试的是java软件工程师职位.在职期间主要从事的项目有MES系统的开发,北京海底捞项目,和武汉天马项目,使用的技术栈主要Spring,SpringBoot,Mybaits,Springcloud等java相关的技术栈,平时自己也喜欢写写博客,看看github学习新知识.(还有啥你可以自己巴里巴拉哈!!~)
以上是我的自我介绍.
1.面:嗯,请说一下java有哪些容器?在你的项目中你是怎么用的?分别说下特性和应用场景吧!
我:嗯,好的,面试官
java的容器:主要有LIST,set,map这三大类
(1)**list集合:**首先我说下它的特性:有序可以重复的队列
在项目中最常用是它的两个子实现类,ArrayList和Linklist
首先:我说下ArrayLIST在我的项目中的最常应用的:因为arraylist可以动态扩容数组,随机访问的效率高,在项目中主要,对于查询操作业务多的,比如一天的订单量查询
对于单线程环境用arrlist,如果要多线程环境,一般采用vector来进行代替
(若问我怎么动态扩容:通过arrayList的add()源码分析得到:一次扩容默认为10,之后达到阈值,比如为11时,以原先1.5倍进行扩容,也就是15,后面依次采用这样扩容)
其次:LinkList:是一个双向链表实现,在对于需要快速插入删除元素时用LinkedList,在项目经常是对于要进行修改等,比如海底捞订单的优先级变动.
(2)Set
它的特性:不重复,无序的
使用的场景: 举不出例子来
(3)MAP
是一个key-value键值对,key值唯一,value值可以重复,但key_value都不可以为null,若输入重复key,会覆盖原来 value
其实现类主要是HashMap和HashTable,两者有啥区别你呢?
hashmap线程不安全,hashtable线程安全的
hashMap中允许键和值为null,但是hashtable不允许
hashmap是默认容器16,2倍扩容,而hashtable默认是11,是2倍+1扩容
面:那你说一下hashMap的实现原理
好嘞!面试官,哈哈map的实现原理要分为JDK1.8之前和jdk1.8之后的两个版本来说
JDK1.7:
HashMap使用的是桶+链表实现的,采用的是链表处理冲突,在同一hash值的键值对会放到同一个桶中,
当桶的元素较多的时候,key的查找效率较低
JDK1.8
采用的是桶+链表+红黑树实现
也就是说当链表表长超过阈值8的时候,会将链表转换为红黑树,这样就大大减少的查找的时间
面:能具体说下吗?
嗯!好的
在1.7之前,HashMAp底层维护的是一个数组,数组的每一项都是一个Entry,我们向HashMap中所放置的对象实际上是存储在该数组当中的;而Map中的可以,value则是以Entry的形式存放在数组中.当中 的这个Entry应该放在数组的哪一个位置上,通过key的hashcode计算得来最终的位置hash桶(就是采用has的拉链法)
置于其扩容默认是16,当每个槽都满时,每次都是扩大一倍容量
而对于JDK1.8采用是位桶+红黑树,非线程安全,就是说某个桶的链表长度达到阈值时,会转为 红黑树
1)介绍HashMap:
按照特性来说明一下:储存的是键值对,线程不安全,非Synchronied,储存的比较快,能够接受null。
按照工作原理来叙述一下:Map的put(key,value)来储存元素,通过get(key)来得到value值,通过hash算法来计算hascode值,用hashCode标识Entry在bucket中存储的位置,储存结构就算哈希表。
“2)你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”
“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。
当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。
”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。
这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道hashing以及HashMap的工作原理。
3)提问:两个hashcode相同的时候会发生说明?
hashcode相同,bucket的位置会相同,也就是说会发生碰撞,哈希表中的结构其实有链表(LinkedList),这种冲突通过将元素储存到LinkedList中,解决碰撞。储存顺序是放在表头。
4)如果两个键的hashcode相同,如何获取值对象?
如果两个键的hashcode相同,即找到bucket位置之后,我们通过key.equals()找到链表LinkedList中正确的节点,最终找到要找的值对象。
一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。
5)如果HashMap的大小超过了负载因子(load factor)定义的容量?怎么办?
HashMap里面默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
6)重新调整HashMap大小的话会出现什么问题?
多线程情况下会出现竞争问题,因为你在调节的时候,LinkedList储存是按照顺序储存,调节的时候回将原来最先储存的元素(也就是最下面的)遍历,多线程就好试图重新调整,这个时候就会出现死循环。
当多线程的情况下,可能产生条件竞争(race condition)。
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
7)HashMap在并发执行put操作,会引起死循环,为什么?
是因为多线程会导致hashmap的node链表形成环形链表,一旦形成环形链表,node 的next节点永远不为空,就会产生死循环获取node。从而导致CPU利用率接近100%。
8)为什么String, Interger这样的wrapper类适合作为键?
因为他们一般不是不可变的,源码上面final,使用不可变类,而且重写了equals和hashcode方法,避免了键值对改写。提高HashMap性能。
String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
9)使用CocurrentHashMap代替Hashtable?
可以,但是Hashtable提供的线程更加安全。
Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。
10)hashing的概念
散列法(Hashing)或哈希法是一种将字符组成的字符串转换为固定长度(一般是更短长度)的数值或索引值的方法,称为散列法,也叫哈希法。由于通过更短的哈希值比用原始值进行数据库搜索更快,这种方法一般用来在数据库中建立索引并进行搜索,同时还用在各种解密算法中。
11)扩展:为什么equals()方法要重写?
判断两个对象在逻辑上是否相等,如根据类的成员变量来判断两个类的实例是否相等,而继承Object中的equals方法只能判断两个引用变量是否是同一个对象。这样我们往往需要重写equals()方法。
我们向一个没有重复对象的集合中添加元素时,集合中存放的往往是对象,我们需要先判断集合中是否存在已知对象,这样就必须重写equals方法。
2.面:多线程的实现方式
通过thread类继承,实现Runnable接口,通过哦Callable接口通过FutureTask包装器创建Thread线程
3.面:请你聊聊JVM垃圾回收
嗯~jvm的垃圾回收就是识别并且丢弃应用不在使用的对象来释放和重用资源
哪些对象需要回收:
引用计数法:计数器
可达性分析(以GCRoots作为起始点,向下搜索,当一个对象到GCRoots没有任何的引用链相连时,证明不可用)
二次标记回收对象:就是GCRootS不可达的对象,会被标记,然后判断是否要哦执行Finalize的方法
何时进行回收:CPU空闲,堆满,或者调用system.gc()
怎么回收? 标记清除算法,标记整理算法,
分代收集算法:新生代:复制算法 老年代:标记清除,标记整理算法
java中的四种引用:强弱软虚
1.强:垃圾级回收器永远不会回收掉被引用的对象实例
2.软引用:描述一些还有用但不重要的数据,一般在内存溢出之前,会把这些对象列入回收范围,进行第二次回收,实现采用
SoftReference
3.弱::描述非必要对象,弱引用关联的对象只能生存到下一次拉拉筋回收发生之前,也就是说再垃圾回收的时候,不管内存,都会回收弱引用关联的对象
4.虚::设置此目的是为了这个对象收集器回收时收到一个系统通知
4.面:双亲委派说说?
主要是为了安全,防止类重复加载,以及保证java的核心API被篡改
5.谈谈MinorGC 和FullGC
MinorGC:发生在新生代,对于许多java对象会大量死去,锦绣柠快速的回收,只有少量的对象回收,可以采用复制算法
FullGC:发生在老年代,一般是在老年代满了之后进行,速度一般很慢
6.说一下类加载机制:
JVM的类加载机制分为五个部分:加载,验证,准备,解析,初始化
加载
加载过程主要完成三件事情:
1、通过类的全限定名来获取定义此类的二进制字节流
2、将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
3、这个过程主要就是类加载器完成。(对于HotSpot虚拟而言,Class对象较为特殊,其被放置在方法区而不是堆中)
连接
1、验证:目的保证加载的类是否能被JVM执行
2、准备:
仅仅为类变量(static修饰的变量)分配内存空间并且设置该类变量的初始值(这里的初始值指的是数据类型默认的零值),这里不包含用final修饰的static,因为用final修饰的类变量在javac执行编译期间就会分配,同时要注意,这里不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量会在对象实例化是随着对象一起被分配在java堆中3、解析:
解析阶段主要是将常量池内的符号引用替换为直接引用的过程。符号引用是用一组符号来描述目标,可以是任何字面量,而直接引用则是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。通常而言一个符号引用在不同虚拟机实例翻译出来的直接引用一般不会相同。
和C之类的纯编译型语言不同,Java类文件在编译过程中只会生成class文件,并不会进行连接操作,这意味在编译阶段Java类并不知道引用类的实际地址,因此只能用“符号引用”来代表引用类。举个例子来说明,在com.sbbic.Person类中引用了com.sbbic.Animal类,在编译阶段,Person类并不知道Animal的实际内存地址,因此只能用com.sbbic.Animal来代表Animal真实的内存地址。在解析阶段,JVM可以通过解析该符号引用,来确定com.sbbic.Animal类的真实内存地址
初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
1.面:你的GMES系统,使用的SpringCloud,能聊下这个项目的组成吗?
(1)项目SpringCloud主要分为那几个模块:并且各自的作用
**Eureka:**服务注册与发现,设置客户端和 服务发现
客户端:
依赖:引入-
netflix-eureka-server
然后在启动类上添加
@EnableEurekaServer
配置文件:配置将自己注册以及一些相应的要求如服务端间隔多少秒删除,设置许月百分比以及许月的时间间隔,拉取注册fetch-registry
服务端:
配置:引入依赖:
netflix-eureka-client
在配置server中拉取依赖
fetch-registry:
** Ribbon**:负载均衡
实现原理:引入依赖
netflix-ribbon
@EnableEurekaClient
Feign:
用于服务端之间的访问
Hystrix
服务熔断
启动类要加上
@EnableHystrixDashBoard
Zuul
网关
@EnableZuulProxy
看你简历上写了
微服务架构的原理是什么?
面向SOA的理念,更细小的粒度服务拆分,将功能拆解到各个服务中,降低系统间的耦合性,提高 灵活服务的支持
注册中心的原理是什么?
服务向eureka注册中心注册,EurekaServer会将注册信息向其他的EurekaServer进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存到本地,下次再调用时,则直接在调用时,则直接从本地缓存中取,完成一次调用
配置中心的原理
在服务运行之前,将所要的配置信息从配置仓库中拉取到本地服务,从而达到同一配置化管理的服务
配置中心如何实现自动刷新
Ribbon的负载均衡原理
是通过LoadBalanced这个注解根据我们配置的负载均衡策略irule进行选择何时的服务提供者给使用者
并通过iping来检测server是否可用,iLOadBalance通过维护一个Timer每隔10s检测一次server的可用状态
IClientConfig主要定义用于初始化各种客户端和负载均衡器的配置信息
Histrix的熔断机制
正常情况下:熔断机制关闭
一段时间内,失败率达到有一定的阈值,断路器将断开,此时不在请求服务提供者,而是只是快速失败的方法
在断路器刚刚开始一段时间,属于还没有完全关闭的状态,这个时候断路器会允许发送一个 请求给服务提供者,如果调用成功,关闭断路器,否则继续保持打开状态,也就是真正意义的断开
2.面:请介绍下 你最近一个项目有哪些模块?你负责主要是什么模块呢?
好的,面试官,我最近做的项目是Gmes大型制造业信息集中系统,主要的项目模块包括1.各系统车间的资源管理,2.库存管理,3.系统的生产过程管理,4.生产任务管理监控等,5.数据采集功能等.我在起重工主要负责是系统车间的资源管理业务的编码,当中批量数据语句插入,sql编写优化
3.请说下你项目中用到的MYbaits的动态标签有哪些?
if标签 where标签 set trim foreach bind choose when otherwise \
4.Mybaits的注解方式有哪些?
1.配置文件进行配置
2.接口类
mybaits属性名与表中的 字段不一致怎么办?
1.采用别名
2.通过映射字段名和实体类属性名对应
也就是通过resultMap中来配置一一对应如id result中的property和column来映射
Mybaits的分页:
通过自身的RowBounds来进行内存分页
通过pageHelperl来进行分页操作
或者通过SQL语句进行 直节进行分页
拦截器实现分页
Mybaits的二级缓存解释下
首先一级缓存:基于PerpetualCache的HashMap缓存,存储的作用域为session,当sessionflush或者flush后或者close后,该session的所有cache都清空了,默认打开一家缓存
其次:二级缓存与二级缓存一样采用PerpetulCache的hashmap存储,不同点在于存储作用域为Map品牌而就是namespace,是可以自己的定义的存储源,默认是不打开的
当中的缓存更新机制:某个作用域进行了cud的操作,默认作用域下面的select缓存会被清理掉,若开启二级缓存,只根据判断是否更新
mybaits的一对一映射,一对多映射呢
一对一:association
一对多:collection
4.请说下那你在项目中怎么运用设计模式的?
5.dickerfile和核心配置有哪些
Dockerfile 分为四部分 : 基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。
主要包括from命令:指定基础镜像
Label:维护者的姓名是什么
run:指定要做的事
add:用来加一些本地文件到
copy:与add相似,但不能解压和访问网络资源
EnterPoint:配置容器
## Dockerfile文件格式 # 1、第一行必须指定 基础镜像信息 FROM java:8 # 2、维护者信息 MAINTAINER baizhan baizhan@163.com # 3、镜像操作指令 RUN echo "wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.55/bin/apachetomcat-9.0.55.tar.gz" run tar -zxvf apache-tomcat-9.0.55.tar.gz -C /usr/local # 4、容器启动执行指令 CMD /usr/local/tomcat/bin/catalina.sh
SpringBoot常用的注解有哪些?
@Transactional:声明事务
编程式事务:TransaactionTemplate
声明式事务:建立在Aop的基础上,其本质对方法前后进行拦截,在目标方法开始之前创建或加入一个事务,在 执行完成目标方法后根据执行情况提交回滚事务,通过@Transaaction级就可进行事务的操作
@EnableEurekaServer
用在springboot启动类上,表示这是一个eureka服务注册中心;
2、@EnableDiscoveryClient
用在springboot启动类上,表示这是一个服务,可以被注册中心找到;
3、@LoadBalanced
开启负载均衡能力;
4、@EnableCircuitBreaker
用在启动类上,开启断路器功能;
5、@HystrixCommand(fallbackMethod=”backMethod”)
用在方法上,fallbackMethod指定断路回调方法;
6、@EnableConfigServer
用在启动类上,表示这是一个配置中心,开启Config Server;
7、@EnableZuulProxy
开启zuul路由,用在启动类上;
8、@SpringCloudApplication
请说下SpringBoot的生命周期:
好的,springboot的是生命周期主要分为四大阶段:bean实例化,设置对象属性,处理aware接口,BeanPOSTProcessor,InitialBean,DispsableBean或者调用destroy-method方法
1.实例化bean对象
通过反射的方式进行对象的创建,此时创建的只是在堆空间中申请空间,属性都是默认值
2.设置对象属性,给对象属性进行值的设置工作
3.处理aware接口
如果对象需要引用容器内部的对象,需要调用aware接口子类方式进行同一的操作进行同一的设置
4.BeanPOSTProcessor的前置处理
对生成的bean对象进行前置的处理工作
5.检查当前的bean对象是设置了initializingBean接口,然后进行属性的设置等基本工作.
6.检查是否配置有自定义的iniit_method方法,若有就调用
7.BeanPOSTProcesor后置处理
对生成的bean对象进行后置的处理工作
8.注册必要的Destruction相关的回调接口,方便对象的销毁操作
9.获取并使用bean对象
10:销毁:看是否调用了DisposableBean接口或者destroy_methood的方法
有进行销毁
SringBean的几种作用域:
主要有四种作用域:
singleton
prototype:容器可以创建多个bean实例,每次返回都是一个新的实例
request:为每一个网络请求创建一个实例,在请求完成后,bean会失效并被垃圾回收器回收
session: 与request类似,只是以session为单位,在session失效后,也会随之失效
global-session: 全局作用域 仅仅作用于HTTP Session,所有的session值共享一个Bean实例
Spring的事务说明下:
spring的事务主要有两种:一种是编程式事务,另一种是声明式事务@Transaction
手动实现的的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bcmwaEsf-1680420318395)(image-20230204164529522.png)]
spring本身是没有事务的,严格来说,事务只有在数据库才有.我们说的spring事务其实事为方便我们开发对事务的一种扩展实现
Spring的事务是AOP的一种体现,当一个方法添加@Transaction注解时,spring会基于这个类生成一个代理对象,会将这个代理对象作为 bean,当使用这个代理对象的方法 时候,如果有事务处理,会自动提交事务给关系,然后去执行具体的业务逻辑,若与异常,直接回滚操作,否则,正常执行
spring的事务隔离级别
主要是分为四种:
读未提交:导致脏读
读已提交:避免脏读,
可重复读避免脏读,不可重复读,
串行化:不推荐,在并发性能上,会导致代价高,
Spring事务在哪些情况会失效呢?
bean对象没有被spring容器管理,
访问修饰符不是public
自身调用问题
数据库不支持事务
数据源没有配置事务管理器
异常被捕获或者配置错误
Spring事务的传播机制有哪些?
七种传播机制:
1.required:默认的传播机制,没有事务就新建一个事务,存在当前事务,加入当前事务
2.supports:没有当前事务,就新建一个事务,若有了,就以非事务的方式执行
3.Mandatory:,当前 事务存在,就加入当前事务,没有当前 事务,直接抛出异常
4.Required_new:新建一个事务执行,如果存在当前事务,就挂起事务
5.Nested:没有新建一个事务,有就嵌套到其中当自事务执行
6.Not_Supported:以非事务的方式执行,存在当前事务,挂起
7.never:不使用事务,如果存在,抛出异常
注意:
nested和required_new的区别:required市创建一个新事务,当原事务回滚不会影响 新事务,但你nested是嵌套的,父事务回滚,子事务也会回滚
nested和required:required存在事务,调用方和别被调用方用同一个事务,出现异常,都会回滚,但是nested若是被调用方出现异常,子事务的回滚不会影响调用方的回滚操作.
说说你对spring事务的理解
SpringBoot如何就解决跨域的问题
1.@CROSSRigin
2.配置文件跨域
3.CorsFilter
4.通过response来实现跨域
5.ResponseBodyAdvice
Springboot的启动流程说下?
好的
1.首先是要从main方法中找到run() 方法,在执行run()方法之前new一个SpringApplication对象
2.进入run方法,创建应用监听器SpringApplicationRunListener开始监听
3.加载SpringBoot的配置环境(ConfigurableEnvironment),然后把配置环境加入 监听对象中
4.然后加载**应用上下文
ConfigurableApplicationContext
,**当做run方法的返回对象5.最后创建Spring容器,**
refreshContext(context)
,**实现starer自动化配置和bean的实例化等工作
请解释下Springboot自动配置的原理
@SpringbootApplication
他是一个组合注解里面包含了三个核心注解
@Configuration
:表名他是一个注解类,会配置spring所有的bean事务,提供一个spring的上下文环境
EnableAutoConfiguration
:这个注解主要用来根据Spring声明的依赖对Spring框架进行自动的配置主要包含如下两个注解:
1.
@AutoConfiguratiionPackage
:这个注解里面 有一个@Import(Register.class)注解,其中Register类就是将启动类中所在包下的所有子类扫哦秒注入到Spring容器中2.
@Import(AutoConfigurationImportSelector.class)
,在这类中的有一个getCandidateConfiguration
方法会去查找位于meta-inf/下面的spring.factories文件中所有自动配置 类3.
@ComponentSCAN
:组件扫描,通过run方法的ExammangerApplication.class
包路径下面的文件,同时也会扫描再有@Component注解的类,并为其创建bean
Jvm的内存模型主要有哪些?
内存模型主要有:栈,方法区,堆,本地方法栈和PC程序计数器
其中:
堆:用来存放的对象的实例
栈:方法栈,线程私有,值方法执行时,来存储方法用到的局部变量,操作栈,动态链接等信息,调用方法时,执行出栈
方法区:又叫非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器优化后代码等数据
本地方法栈:与栈其实很相似,只是他是在执行native方法时采用到
程序计数器:用来处理哪些转换成字节码的文件
类加载过程:加载,验证,准备,解析,初始化
类的加载器:引导类加载器,拓展类加载器,应用加载器,自定义加载器
双亲委派模式
原理:就是一个类接受到加载的请求,你会立即去加载,而是委托给他的父类去进行加载,如果这个父类加载器上面还有父类,就继续向上委托直到顶级父类启动类加载器,看是否已经加载过,若加载过,直接成功返回,否则再交给子类去加载
好处:其实就是安全
第一个防止类被重复加载
第二个是保证java的核心api被篡改,导致恶意的类被加载,比如我们自定义了一个object类,在双亲委派模式下是自动加载系统的object类,不会我们手写的
JVM的垃圾回收机制说下
1)新产生的对象优先分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入老年代);
2)当Eden区满了或放不下了,这时候其中存活的对象会复制到from区。
这里,需要注意的是,如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入老年代。之后Eden区的内存全部回收掉;
3)之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和from区存活下来的对象复制到to区(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入老年代,之后回收掉Eden区和from区的所有内存;
4)如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了;
5)当老年代满了或者存放不下将要进入老年代的存活对象的时候,就会发生一次Full GC(这个是最需要减少的,因为耗时较为严重)。
内存溢出的原因有哪些?
堆栈溢出:
GC频繁回收导致内存溢出
ABc循环依赖
缓存穿透
查询一个数据库不存在的数据,由于redis的默认容错机制是不缓存数据库没有的数据,这就导致每次访问不存在的数据时候会导致都去存储层去查询,缓存失去了意义,如果这个时候流量大,DB可能就挂掉
采用布隆过滤器
或者返回一个数据为空,但过期时间会短
缓存击穿
这个数据存在,且是热点数据,这个key缓存刚过期,这时候又有大量的请求来访问,导致db宕机
对读写的操作进行加锁,从而避免大力来那个请求落到db上
缓存雪崩
就是缓存集体失效,大量请求直接落到DB上,导致DB宕机
采用互斥锁,STNX实现锁
提前设置超时值
设置永不过期
REdis事务怎么实现的
redis的事务的主要包括:multi,exec,discard,watch
Exec:命令负责触发并执行事务中的所以有命令
Multi:敞开业务,这个是位置的开端,一切指令的开端跟mysql的begin差不多
Discard:撤销业务,回滚操作
watch和unWatch:监督一至多个key
nameserver,broker,producer,consumer
nameServer:: 担任路由消息的提供者.生产者和消费者通过Nameserver查找个topic相应的BrokerIP列表进行发送消息和消费资源,nameserver由多个无状态的节点构成,节点键无信息的同步
broker: 定期向Nameserver发送心跳包方式,轮询所有的Nameserver注册元数据
主要作用:消息中转角色,负责存储消息,转发消息
broker基本信息
主体topic地址信息
broker集群信息
存活broker信息
filter过滤器
由于我们项目中主要使用rocketMq做链路跟踪功能,因此需要比较高的性能,并且偶尔丢失几条消息也关系不大,所以我们就选择多Master多Slave模式,异步复制方式进行部署
部署过程简单说一下:
我部署的是双master和双slave模式集群,并部署了两个nameserver节点
1)服务器分配
分配是两台服务器,A和B,其中A服务器部署nameserv1,master1,slave2;B服务器部署nameserv2,master2和slave1节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cOZeusKD-1680420318396)(b06c86436d9c4b69969ac76f4a037cbb.png)]
2)broker的配置
分别配置rocketmq安装目录下四个配置文件:
master1:/conf/2m-2s-async/broker-a.properties
slave2:/conf/2m-2s-async/broker-b-s.properties
master2:/conf/2m-2s-async/broker-b.properties
slave1:/conf/2m-2s-async/broker-a-s.properties
1234
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52z2weCH-1680420318397)(956e66046923455aad4691b722c28caf.png)]
总的思路是:
a.master节点的brokerId为0,slave节点的brokerId为1(大于0即可);
b.同一组broker的broker-Name相同,如master1和slave1都为broker-a;
c.每个broker节点配置相同的NameServer;
d.复制方式配置:master节点配置为ASYNC-MASTER,slave节点配置为SLAVE即可;
e.刷盘方式分为同步刷盘和异步刷盘,为了保证性能而不去考虑少量消息的丢失,因此统一配置为异步刷盘
3)启动集群
a 检查修改参数
启动前分别检查修改runbroker.sh和runserver.sh两个文件中的JVM参数,默认的JAVA_OPT参数的值比较大,若直接启动可能会失败,需要根据实际情况重新配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cvGeJIIU-1680420318397)(5af5cfce91414124a75d09a36955c5ed.png)]
b 分别启动两个namerser节点
nohup sh bin/mqnamesrv > /dev/null 2>&1 &
查看日志
tail -f ~/logs/rocketmqlogs/namesrv.log
c 分别启动4个broker节点
maste1
nohup sh bin/mqbroker -c
/usr/local/rocketmq/conf/2m-2s-async/broker-a.properties &
slave1
nohup sh bin/mqbroker -c
/usr/local/rocketmq/conf/2m-2s-async/broker-a-s.properties &
maste2
nohup sh bin/mqbroker -c
/usr/local/rocketmq/conf/2m-2s-async/broker-b.properties &
slave2
nohup sh bin/mqbroker -c
/usr/local/rocketmq/conf/2m-2s-async/broker-b-s.properties &
查看日志:
tail -f ~/logs/rocketmqlogs/broker.log
总结:集群环境部署,主要就是以上三个步骤,需要注意的是过程中broker配置文件的配置正确性,还需要注意一下启动前对jvm参数的检查
RocketMq的工作流程如下:
1)首先启动NameServer
。NameServer启动后监听端口,等待Broker、Producer以及Consumer连上来
2)启动Broker
。启动之后,会跟所有的NameServer建立并保持一个长连接,定时发送心跳包。心跳包中包含当前Broker信息(ip、port等)、Topic信息以及Borker与Topic的映射关系
3)创建Topic
。创建时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic
4)Producer发送消息
。启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic所在的Broker;然后从队列列表中轮询选择一个队列,与队列所在的Broker建立长连接,进行消息的发送
5)Consumer消费消息
。跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,进行消息的消费
RocketMq采用文件系统进行消息的存储,相对于ActiveMq采用关系型数据库进行存储的方式就更直接,性能更高了
RocketMq与Kafka在写消息与发送消息上,继续沿用了Kafka的这两个方面:顺序写和零拷贝
1)顺序写
我们知道,操作系统每次从磁盘读写数据的时候,都需要找到数据在磁盘上的地址,再进行读写。而如果是机械硬盘,寻址需要的时间往往会比较长而一般来说,如果把数据存储在内存上面,少了寻址的过程,性能会好很多;
但Kafka 的数据存储在磁盘上面,依然性能很好,这是为什么呢?
这是因为,Kafka采用的是顺序写,直接追加数据到末尾。实际上,磁盘顺序写的性能极高,在磁盘个数一定,转数一定的情况下,基本和内存速度一致
因此,磁盘的顺序写这一机制,极大地保证了Kafka本身的性能
2)零拷贝
比如:读取文件,再用socket发送出去这一过程
buffer = File.read
Socket.send(buffer)
传统方式实现:
先读取、再发送,实际会经过以下四次复制
1、将磁盘文件,读取到操作系统内核缓冲区Read Buffer
2、将内核缓冲区的数据,复制到应用程序缓冲区Application Buffer
3、将应用程序缓冲区Application Buffer中的数据,复制到socket网络发送缓冲区
4、将Socket buffer的数据,复制到网卡,由网卡进行网络传输
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sBFD8HCv-1680420318397)(b4862e11dba34179be8e4f1a11c1826e.png)]
传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的
重新思考传统IO方式,会注意到在读取磁盘文件后,不需要做其他处理,直接用网络发送出去的这种场景下,第二次和第三次数据的复制过程,不仅没有任何帮助,反而带来了巨大的开销。那么这里使用了零拷贝,也就是说,直接由内核缓冲区Read Buffer将数据复制到网卡,省去第二步和第三步的复制。
那么采用零拷贝的方式发送消息,必定会大大减少读取的开销,使得RocketMq读取消息的性能有一个质的提升
此外,还需要再提一点,零拷贝技术采用了MappedByteBuffer内存映射技术,采用这种技术有一些限制,其中有一条就是传输的文件不能超过2G,这也就是为什么RocketMq的存储消息的文件CommitLog的大小规定为1G的原因
小结:RocketMq采用文件系统存储消息,并采用顺序写写入消息,使用零拷贝发送消息,极大得保证了RocketMq的性能
如图所示,消息生产者发送消息到broker,都是会按照顺序存储在CommitLog文件中,每个commitLog文件的大小为1G
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HrZDI9Ux-1680420318398)(2b6b83ee84a7487b80e988b81f5f0cb5.png)]
CommitLog-存储所有的消息元数据,包括Topic、QueueId以及message
CosumerQueue-消费逻辑队列:存储消息在CommitLog的offset
IndexFile-索引文件:存储消息的key和时间戳等信息,使得RocketMq可以采用key和时间区间来查询消息
也就是说,rocketMq将消息均存储在CommitLog中,并分别提供了CosumerQueue和IndexFile两个索引,来快速检索消息