美团后端一面面经
说一下AOP?
面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的技术。可以减少程序中相同代码的编写,简化开发,使得接口更加专注于业务
相关概念
Aspect
(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
Joint point
(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
Pointcut
(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
Advice
(增强):Advice 定义了在Pointcut
里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
如何实现
1. AspectJ
AspectJ主要作用于编译时增强, 也称之为静态代理, 我们在写完一段独立的业务方法saveData()时, 可以使用aspectJ将切面逻辑织入到saveData()中. 比如日志记录.
在使用aspectJ编译代码之后, 我们的class文件中会多出一段代码, 这段代码是aspectJ在编译时增加的aop代码. AspectJ的这种做法可以被称为静态代理
Aspect在编译期, 为被代理方法织入我们在aspect中定义好的切面逻辑, 以添加字节码的方式(强行添加代码)
2. JDK动态代理
jdk动态代理使用jdk自带的反射机制来完成aop的动态代理, 使用jdk自带的动态代理有如下要求:
1.被代理类(我们的业务类)需要实现统一接口
2.代理类要实现reflect包里面的接口InvocationHandler
3.通过jdkProxy
提供的静态方法newProxyInstance(xxx)
来动态创建代理类
代理类和被代理类使用同样的对象引用,因此我们可以神不知鬼不觉地使用我们的真实业务类, 而无需关注在它周围的切面逻辑(独立性),
说一下IOC?
loC——Inversion of Control 即“控制反转”,是一种设计思想。在java开发中,loc意味着将你设计好的对象交给容器控制,而不是传统的由对象内部控制。loc通常是由DI实现的,下面我们介绍一下DI。
DI------Dependency Injection,即依赖注入,组件之间的依赖关系由容器在运行期决定,即由容器动态地将某个依赖关系注入到组件当中。依赖注入的目的是为了提升组件重用的频率,并为系统搭建一个灵活可扩展的平台。
核心要点:
1.对象依赖于IOC/DI容器,因为对象需要IOC/DI容器来提供对象需要的外部资源。
2.IOC/DI容器注入对象,注入的是某个需要的东西那就是注入对象所需要的资源
3.IOC/DI容器控制对象,主要是控制对象实例的创建
案例:常规情况下的应用程序,如果要在A里面使用C,会去直接去创建C的对象,也就是说,是在A类中主动去获取所需要的外部资源C,这种情况被称为正向的。反向就是A类不再主动去获取C,而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向的注入到A类中。
说一下垃圾回收机制?
GC 的作用区域:
频繁在新生区收集,很少在养老区收集,几乎不在方法区(永久区/元空间)收集,其中,Java堆是垃圾收集器的工作重点
判断对存活的方法:
1.引用计数法
1、引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
2、对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
存在一个严重的问题:无法处理循环引用的情况
2.可达性分析算法
该算法的基本思路就是通过一些被称为 引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点, 更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
垃圾清除算法
1、标记清除算法(Mark-Sweep)
标记阶段是把所有活动对象(可达对象,reachable)都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。
2、标记复制算法(Copying)
将活着的 内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
3、标记压缩算法(Mark-Compact)
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
分代策略
1、年轻代(Young Gen)
1、年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
2、这种情况 复制算法的回收整理, 速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
2、老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由 标记-清除或者是标记-清除与标记-清除-整理的混合实现。
垃圾收集器
新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(停止-复制算法)
并行多线程收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。
Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先,使用多线程。
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
无法清理浮动垃圾,容易产生碎片。
清理过程分为4个步骤,包括:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
G1收集器
并行与并发执行,分代收集,空间整合,分为不同的regin区域进行垃圾回收
G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
Minor GC(新生代GC):
指发生在新生代的垃圾收集动作,Java对象大多存活时间不长,所以Minor GC的发生会比较频繁,回收速度也比较快。触发条件:在新生代的Eedn区满了会触发。
Full GC/Major GC(老年代GC):
指发生在老年代的GC,出现了Full GC,经常会伴随至少一次的Minor GC(不是必然的),Major GC的速度一般会比Minor GC慢10倍以上。
触发条件:
System.gc() 方法的调用,此方法会建议JVM进行Full GC,但JVM可能不接受这个建议,所以不一定会执行。
老年代空间不足,创建的大对象的内存大于老年代空间,导致老年代空间不足,则会发生Full GC。
JDK1.7及以前的永久代空间满了,在JDK1.7以前,HotSpot虚拟机的方法区是永久代实现都得,在永久代中会存放一些Class的信息、常量、静态变量等数据,在永久代满了,并且没有配置CMS GC的情况下就会触发Full GC,在JDK1.8开始移除永久代也是为了减少Full GC的频率。
空间分配担保失败,通过Minor GC后进入老年代的平均大小大于老年代的可用空间,会触发Full GC
内存回收机制
对象先在Eden区分配,当Eden区没有足够的空间去分配时,虚拟机会发起一次Minor GC,将存活的对象放到From Survivor区(对象年龄为1)。
当再次发生Minor GC,会将Eden区和From Survivor区一起清理,存活的对象会被移动到To Survivor区(年龄加1)。
这时From Survivor区会和To Survivor区进行交换,然后重复第一步,不过这次第一步中的From Survivor区其实是上一轮中的To Survivor区。
每次移动,对象的年龄就会加1,当年龄到达15时(默认是15,对象晋升老年代的年龄阈值可以通过参数 -XX: MaxTenuringThreshold 设置),会从新生代进入老年代。
总结:
1.对象优先在Eden区分配。
2.大对象直接进入老年代(大对象指需要大量连续内存空间的Java对象)。
3.长期存活的对象进入老年代。
讲一下布隆过滤器?
它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。
布隆过滤器可以告诉我们 “某样东西一定不存在或者可能存在”
业务场景:
解决Redis缓存穿透问题(面试重点)
邮件过滤,使用布隆过滤器来做邮件黑名单过滤
对爬虫网址进行过滤,爬过的不再爬
解决新闻推荐过的不再推荐(类似抖音刷过的往下滑动不再刷到)
如何解决缓存击穿问题?
方案一、定时任务主动刷新缓存设计
先将所有可能查询到的数据存入redis,对redis中的数据库定时更新,保证redis永远都会有数据存在,来请求只查redis
方案二、使用redis的分布式锁
具体步骤:
1.如果缓存命中直接返回数据集
2.如果缓存没有,则尝试获取分布式锁(有超时设置)如果没有拿到锁,则阻塞当前线程,n秒之后再次尝试获取分布式锁
3.拿到锁之后检查数据是否已经被其他线程放到redis缓存中,如果redis缓存已有,直接返回redis中的数据,释放分布式锁;如果缓存没有被刷新,则查数据库将数据库查询的结果保存到redis缓存中并返回查询结果
方案三、普通加jvm的锁查询缓存
如果缓存命中直接返回数据集
如果缓存没有,则尝试JVM锁,
其他线程阻塞拿到锁之后,检查redis是否有数据,以免其他线程已经刷过缓存
如果redis已经有数据,直接返回,并释放锁,返回数据库结束
如果redis没有数据,则查询数据库,并保存到redis缓存中返回数据,释放锁
比如:
有s台服务器,用户请求数为n;那么同一时间参数相同的请求最多只会有s次查询打到数据库上,这里s这个常量相当于原来对于数据库来说一个O(n)的操作时间下降到了O(s)
这里可以看出,查询数据库操作的耗时与n的增长无关,只与s有关
方案四、jvm缓存+redis缓存的多级缓存
这种设计,服务器只会在jvm缓存失效,且redis缓存也失效的情况下才会查询数据库,而多个服务器的jvm缓存失效时间是随机值,所以很大程度上避免的同时失效去查库的情况,由于所有服务器jvm缓存同时失效redis缓存也失效的可能性极低,所以数据库上重复的查询会很少
如何优化sql查询速度
1.添加索引
经常需要搜索的列上,可以加快搜索的速度;
在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
在经常需要排序的列上创 建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引
在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;
避免 where 子句中对宇段施加函数,这会造成无法命中索引。
2.实现细节
1.SQL语句中IN包含的值不应过多;
2.SELECT语句务必指明字段名称,不用*;
3.只查询一条数据的时候,使用limit 1;
4.避免在where子句中对字段进行表达式操作,这样做可能导致索引失效;
5.对于联合索引来说,要遵守最左前缀法则,防止其失效;
6.尽量使用inner join,这样在没有其他过滤条件的情况下MySQL会自动选择小表作为驱动表;
7.对于联合索引来说,如果存在范围查询,比如between、>、<等条件时,会造成后面的索引字段失效。
解决办法: 业务允许的情况下,使用 >= 或者<= 这样不影响索引的使用.;
8.在 where 子句中使用 or 来连接条件,如果or连接的条件有一方没有索引,将导致引擎放弃使用索引而进行全表扫描
解决办法: 将or连接的双方都建立索引,就可以使用.
9.count 优化 速度:count(*)>count(1)>count(字段)
10.指定查询的索引
use index(索引): 推荐使用指定的索引 (最终用不用该索引,还需要mysql自己判断)
ignore index(索引) : 忽略掉这个索引
force index(索引): 强制使用该索引
分享面试的一些经验帖子 希望可以帮助到你 你的关注是我持续更新的动力 陌生人,加油!