赞
踩
今天看到网易社招Java岗位的面试题,大致浏览了下,发现还没有答案出来,所以自己就搜索整理下,将答案分享出来,由于水平有限,如发现错误或者疑问,欢迎斧正和讨论,大家一起进步
考察对redis的数据结构的了解,以及是否在工作中是否能熟练的运用这些数据结构来解决优化问题
Redis是一个内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件, 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询,在Redis5版本中,新增了流数据类型(Stream data type);
对于存储的key用什么数据结构,这个需要根据业务的需要来设计,选择适合的数据结构对于开发和性能的帮助都是巨大的,比如说以前开发过系统签到功能,在满足高并发的场景要求下,还需要考虑操作时间复杂度,我们这里可以选择hashes类型,并且这里我们还可以根据业务的需求继续优化我们的数据结构,在代码设计复杂度简单的情况下,尽可能减少key对应的个数,比如这里的签到,如果我们为每一个员工都创建一个hash类型的key,则key的数量非常多,但是我们可以选择为每个部门创建一个key,对应的value我们这里还是可以选择使用hashes来存储部门下的员工的相关信息,其中key是员工的信息,value则是签到的状态,这样做有什么好处呢?第一就是减少了key的数据,也就是减少了内存的消耗,第二:由于我们可以根据部门来查询对应的员工,这样的话就减少了循环遍历的次数,性能上面也有了不小的提升。具体的可以参考我的博文《Redis场景应用实例》
重点是考察大数据量的情况下,如果查询所有的key,以及一些常用的管理操作
一般情况下,我们使用keys *
来查询所有的key,也可以匹配查询keys apple*
来查询,dbsize
来查看当前数据库的key的数量;使用flushall
来清空所有数据库的key;
但是当redis中的数据量越来越大的时候,keys命令执行越慢,而且最重要的会阻塞服务器,对单线程的redis来说,简直是灾难,这是我们可以使用scan
命令来查询,scan
是增量式的检索所有的key,同时提供查询一定数量的key(scan 0 count 100
)、匹配查询一定数量的key(scan 0 match CMD* count 100
)等等;
redis默认有16个库,下标从0到15,如果我们切换到指定的数据库,则需要执行select x
x是指定数据库的的下标;也可以通过修改redis.conf来设置数据库的数量database 32
:设置redis的数据库的数量为32个
考察对淘汰策略的了解,是否在项目中去思考如何配置淘汰策略
首先,为什么redis需要淘汰策略,或者redis淘汰策略出现的原因,我们知道redis是基于内存的,如果一个key存储到了redis中,使用的频率很少,但是一直没有释放,会占据一定的内存,很容易造成内存空间存储瓶颈,此时淘汰无用数据来释放空间,存储新数据的就变得尤为重要了。
那么redis如何需要淘汰数据,采用何种方式来淘汰数据呢?
首先Redis中配置文件中设置一个参数maxmemory
来限制内存大小,当实际存储的内容超过这个大小,redis来开始淘汰数据了,redis采用了以下几种淘汰策略,(这里以redis4为例,redis4目前广泛使用),淘汰的策略可以在redis.conf文件中为maxmemory-policy
赋值:值为几下几种
策略名称 | 说明 |
---|---|
volatile-lru | 根据最近最少使用算法,淘汰带有 有效期 属性的key及其数据。是4.0版本之前最常选用的策略 |
allkeys-lru | 同样根据最近最少使用算法,但是淘汰范围的key是所有的key |
volatile-lfu | 根据最不经常使用算法,淘汰带有 有效期 属性的key及其数据。是4.0版本新增的淘汰机制,个人觉得这种策略会与第1种策略成为两种最佳的选择 |
allkeys-lfu | 与第二种的淘汰范围相同,不过使用的算法是最不经常使用算法。同样是4.0版本新增的淘汰机制 |
volatile-random | 随机淘汰带有 有效期 属性的key及其数据 (不推荐使用) |
allkeys-random | 所有key都随机淘汰 (不推荐使用) |
volatile-ttl | 淘汰有效期属性最少的key及其数据,ttl是 Time To Live的缩写 |
如果没有数据可以淘汰,或者没有配置淘汰策略,则只要内存中可用内存还有的话,请求依旧是可以执行的,直到内存全部被占用,但是相应的当Redis内存超出物理内存限制时,内存数据就会与磁盘产生频繁交换,使Redis性能急剧下降。
考察redis的复制,以及复制的主要的步骤和原理
一般上线的数据都不是使用单节点的,主要是redis是基于内存的,如果服务器发送不可预测的因素导致重启,则redis中的数据会发生丢失(采用持久化可以尽量的避免这点),而且系统上线时,如果单节点的redis服务不能正常提供服务,这样会导致整个系统不可用,风险太大,所以一般都是redis集群来搭建高可用环境,将数据备份到其他服务节点上,这样当该节点服务不可用时,其他节点可以继续提供服务;
redis一般采用主从配置的模式来搭建集群,master配置来写数据,slave配置来读数据,redis内置提供复制功能来实现各个节点间数据的同步;
一旦redis各节点配置了主从关系时,便开始进行数据的同步,从库向主库发送psync
命令,主库接收到命令后会判断是否进行增量同步还是全量同步,然后再根据同步策略来同步数据;
当主库有消息操作时,主库会根据心跳来检查从库是否在线,从库则提供自己的复制偏移量,主库根据偏移量来发送未同步的数据
redis采用量乐观复制策略,容忍在一定时间内主从数据内容是不同的,但是两者的数据最终会同步
考察实际项目中是否有使用redis,观察是否有独立思考为什么要用redis,有什么优点和缺点
redis的官网中已经给出了redis的应用场景:
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
一般情况下很少作为数据库,常见的是作为缓存使用,也可以用作分布式锁;除了这些,一些小的功能场景也可以使用redis很方便的解决:
利用Redis的SortSet(有序集合)数据结构能够简单的搞定
利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;
这个在分布式架构中应该是应用最广泛的,无论用户落在那台机器上都能够获取到对应的Session信息。
当然redis也有缺点:
redis和memcache存在同样的特点:
但是两者之间又有差异:
考察对Spring的了解,对于框架,我们不仅仅需要会用,还需要知道内部的联系和原理
这里需要说明下@Autowired
和@Resource
的区别
@Autowired
是Spring自定义的注解,而@Resource
是Java再带的注解;@Autowired
与@Resource
都可以用来装配bean. 都可以写在字段上,或写在setter方法上。@Autowired
默认是按照类型装配的,如果想按照名称来装配可以结合注解@Qualifier
来使用@Resource
默认是按照名称来装配的,也支持按照类型来装配,有两个重要的属性name
和type
BeanFactory是最顶层的接口 ,提供容器功能,只提供实例化对象和获取对象的功能;ApplicationContext是应用上下文,该接口继承BeanFactory接口,是更高一层的容器,提供更多有用的功能:国际化、访问资源、AOP拦截等
两者都可以装载Bean,但是还是有区别的:
lazy-init=true
)我们一般使用ApplicationContext,他有如下的优点:
ApplicationContextAware
来获取应用上下文,然后对Bean进行获取操作等完整的生命周期分为以下步骤:
所谓AOP是面向切面编程,通过预编译方式和运行时动态代理实现程序功能的统一维护的一种技术,我们利用AOP结束可以对程序进行解耦,从而使业务逻辑的各部分之间的耦合度降低,提高程序的可重用性,提升开发效率
常见的应用场景有:权限控制、异常处理、缓存、事务管理、日志记录以及数据校验等
Spring框架提供了@AspectJ 注解方法和基于XML架构的方法来实现AOP;主要的原理如下:
通过配置切入点来拦截关注点方法,运用Java动态代理的方法来生成代理类,增强对目标方法的处理。
需要了解几个概念:
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理
上面说到的SpringAOP是基于动态代理实现的,那么什么是动态代理呢?我们编写程序知道代理模式:
为其他对象提供一个代理以控制对某个对象的访问。代理类主要负责为委托了(真实对象)预处理消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理
这个其实就是代理(代理分为静态代理和动态代理),而这里的动态代理就是利用反射机制在运行时创建代理类,由代理类为被代理类预处理消息、过滤消息并在此之后将消息转发给被代理类,之后还能进行消息的后置处理。代理类和被代理类通常会存在关联关系(即上面提到的持有的被带离对象的引用),代理类本身不实现服务,而是通过调用被代理类中的方法来提供服务;
动态代理有两种方式:
主要是通过Proxy
的静态方法newProxyInstance
返回一个接口的代理实例。针对不同的代理类,传入相应的代理程序控制器InvocationHandler,其底层实现如下:
1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
是一个开源项目,强大的高性能的代码生成包,CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类;
cglib是通过拦截被代理类生成代理类,被代理类直接继承代理类,就可以反射出委托类接口中的所有方法,父类中的所有方法,自身定义的所有方法,完成这些方法的代理就完成了对委托类所有方法的代理
cglib无法代理final修饰的方法
主要原理如下:
Enhance
对象,并设置了所需要的参数然后enhancer.create()
成功创建出来了代理对象intercept()
方法,在这个方法中会调用proxy.invokeSuper(obj, args)
方法invokeSuper
中,通过FastClass机制调用目标类的方法考察Spring的IOC和AOP
Inversion of Control
所谓 IOC ,就是由 Spring IOC 容器来负责对象的生命周期和对象之间的关系
抛除Spring框架,如果我们使用一个对象的话,势必是需要我们自己动手来实例化这个对象的,这样的话就会造成我们的对象和所依赖的对象耦合在一起,我们需要的是所依赖对象提供的服务,但是不是这个对象;
最好是我们需要的依赖对象提供服务的时候,他能够及时提供服务即可,至于是谁实例化这个对象,其实我们并不关心的,Spring正是基于这点,通过Spring容器来实例化Bean对象,然后存储在Bean容器中,那个组件需要这个Bean,只需要将这个Bean注入到对象内,即可调用这个Bean提供的服务。
这样的话,原来需要我们手动来实例化的操作交给了Spring容器来实现,控制Bean实例化的操作权交给了Spring容器,这就是控制翻转
一般注入Bean的方式有三种:setter方法注入、构造器注入、接口注入
见面试题6中的关于AOP的讲解
考察网络请求协议相关知识
提到Http1.1版本,我们不得不提下 HTTP1.0版本,WEB站点收到大量的请求,为了提高效率,HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。但是,这也造成了一些性能上的缺陷,例如,一个包含有许多图像的网页文件中并没有包含真正的图像数据内容,而只是指明了这些图像的URL地址,当WEB浏览器访问这个网页文件时,浏览器首先要发出针对该网页文件的请求,当浏览器解析WEB服务器返回的该网页文档中的HTML内容时,发现其中的图像标签后,浏览器将根据标签中的src属性所指定的URL地址再次向服务器发出下载图像数据的请求,这样的话,如果一个网页中包含多个图片文件时,就会多次请求和响应,这样对性能造成很大的影响,
HTTP1.1版本解决了上传的问题 ,同时新增了如下的特性:
在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟
客户端可以同时发送多个HTTP请求,而不用一个个等待响应
客户端记录本次需要续传的片断,并在需要续传时通知服务器本次需要下载的内容片断,实现断点续传功能
说了HTTP1.0和HTTP1.1,我们在来说下HTTP2.0有了哪些优化
二进制解析起来更高效,更紧凑
不采用有序阻塞,主要是为了提高请求和相应的效率,能同时处理多个消息的请求和响应; 甚至可以在传输过程中将一个消息跟另外一个掺杂在一起。所以客户端只需要一个连接就能加载一个页面
节省数据的开销,提升请求和响应的效率
当浏览器请求一个网页时,服务器将会发回HTML,在服务器开始发送JavaScript、图片和CSS前,服务器需要等待浏览器解析HTML和发送所有内嵌资源的请求。服务器推送服务通过“推送”那些它认为客户端将会需要的内容到客户端的缓存中,以此来避免往返的延迟
请求方式有如下:
请求方式 | 备注说明 |
---|---|
GET | 请求指定的页面信息,并返回实体主体 |
HEAD | 类似GET请求,只是返回的信息中没有请求的主体内容,用于获取报头 |
POST | 向指定资源提交数据进行处理请求 |
PUT | 从客户端向服务端传送的数据取代指定的文档的内容 |
DELETE | 请求服务器删除指定界面 |
CONNECT | HTTP1.1协议中预留给能够将连接改为管理方式的代理服务器 |
OPTIONS | 允许客户端查看服务器的性能 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断 |
何为三次握手:
三此握手有两个目的:
建立连接,需要三次握手,但是释放链接则需要四次握手
为什么TIME_WAIT状态需要经过两个最大报文段生存时间才能到close状态?
原因有以下几点:
如上图所示:假如最后一个ACK由于网络原因导致无法到达B机器,处于LAST_ACK状态的B机器会以为对方没有收到自己的FIN+ACK报文,所以会重发,A机器收到第二次的FIN+ACK报文,会重发一次ACK,并且重新计时,如果A机器收到B机器的FIN+ACK报文后, 发送一个ACK给B机器,就进入CLOSED状态,可能会导致B机器无法去报收到最后的ACK命令,也无法进入CLOSED状态
2. 防止失效请求
防止已经失效链接的请求数据包与正常链接的请求数据包混淆而发生异常
考察网络相关知识,数据传输的流程
考察Spring task的原理
Spring Task的作用是处理定时任务。Spring中为定时任务提供TaskExecutor
、TaskScheduler
两个接口。
TaskExecutor
继承了jdk的Executor
,为定时任务的执行提供线程池的支持:
public interface TaskExecutor extends Executor {
void execute(Runnable var1);
}
TaskScheduler提供定时器支持,即定时滴执行任务:
scheduler.schedule(task, new CronTrigger("30 * * * * ?"));
TaskScheduler需要传入一个Runnable的任务做为参数,并指定需要周期执行的时间或者触发器,这样Runnable任务就可以周期性执行了
Spring中实现定时任务有两种方式:
<!-- 配置注解扫描 -->
<context:component-scan base-package="需要扫描的包路径"/>
<task:scheduler id="taskScheduler" pool-size="100" />
<task:scheduled-tasks scheduler="taskScheduler">
<!-- 每半分钟触发任务 -->
<task:scheduled ref="bean组件名" method="方法名" cron="30 * * * * ?"/>
</task:scheduled-tasks>
@EnableScheduling
@Scheduled(cron="* * * * * ?")
public void test(){}
考察Spring的事务
Spring有基于XML和注解的两种事务配置方式
首先定义好数据源的bean,其次实例化sessionFactory,最后配置切面来管理事务,拦截指定路径下的类
<aop:config proxy-target-class="true">
<!-- expression(*)执行的实现类 -->
<aop:pointcut id="serviceMethods" expression="execution(* com.spring.jdbc.service.impl.UserServiceImpl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="SUPPORTS"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
@Transactional
注解Spring事务的本质是对数据库事务的封装支持,没有数据库对事务的支持,Spring本身无法提供事务管理功能。对于用JDBC操作数据库想要用到事务,必须经过获取连接——》开启事务——》执行CRUD操作——》提交/回滚事务——》关闭连接几部分操作。使用Spring管理事务后,可以省掉自己写代码开启、提交/回滚事务的操作
Spring事务通过AOP动态代理实现,使用上通常要先在配置文件中开启事务,然后通过xml文件或注解配置要执行注解的类方法,然后在调用对应类实例方法时,Spring会自动生成代理,在调用前设置事务操作、调用方法后进行事务回滚或提交操作
下面以SpringBoot为例说明,Spring的事务是如何工作的
在SpringBoot中,如果想要添加@Transactional
并且开启事务功能,则需要添加注解@EnableTransactionManagement
,该注解的主要作用是开启基于注解的事务管理功能,自动配置TransactionManager
,Spring容器在启动时,会将所有的bean加载到ApplicationContext中,在执行含有@Transactional
的类的方法时,会使用AOP来拦截该方法,在该方法调用前开启事务,执行完该方法后,没有异常则提交事务,存在异常则回滚事务。
我们
考察nginx相关知识
事务隔离级别:
事务具有四性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
而事务的隔离性就是指,多个并发的事务同时访问一个数据库时,一个事务不应该被另一个事务所干扰,每个并发的事务间要相互进行隔离
事务有四种隔离级别,由低到高:
就是一个事务可以读取另一个未提交事务的数据,容易造成脏读
2. Read committed
就是一个事务要等另一个事务提交后才能读取数据
一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读
3. Repeatable read
重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作
4. Serializable
Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用
大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read
Spring中存在事务传播,所谓的传播属性就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为;
嵌套事务提交的情况:
// ServiceA @Autowired private ServiceB serviceB; @Override @Transactional(propagation = Propagation.REQUIRED, readOnly = false) public void methodA() { this.methodA(); serviceB.methodB(); } // ServiceB @Override @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false) public void methodB() { System.out.println("methodB"); }
这种情况下, 因为 ServiceB#methodB 的事务属性为 PROPAGATION_REQUIRES_NEW,ServiceB是一个独立的事务,与外层事务没有任何关系。如果ServiceB执行失败,ServiceA的调用出会抛出异常,导致ServiceA的事务回滚
//ServiceA
@Autowired
ServiceB serviceB;
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodA() {
serviceB.methodB();
}
//ServiceB
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodB() {
}
B 如果发生异常导致事务回滚,则A的事务也会回滚
//ServiceA
@Autowired
ServiceB serviceB;
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void methodA() {
serviceB.methodB();
}
//ServiceB
@Override
public void methodB() {
}
B 如果发生异常导致事务回滚,则A的事务也会回滚
//ServiceA @Autowired ServiceB serviceB; @Override @Transactional(propagation = Propagation.REQUIRED, readOnly = false) public void methodA() { try { serviceB.methodB(id); } catch (Exception e) { System.out.println("内层事务出错啦。"); } } //ServiceB @Override @Transactional(propagation = Propagation.REQUIRED, readOnly = false) public void methodB(String id) { }
事务设置为Propagation.REQUIRED时,如果内层方法抛出Exception,外层方法中捕获Exception但是并没有继续向外抛出,最后出现“Transaction rolled back because it has been marked as rollback-only”的错误。外层的方法也将会回滚。
其原因是:内层方法抛异常返回时,transacation被设置为rollback-only了,但是外层方法将异常消化掉,没有继续向外抛,那么外层方法正常结束时,transaction会执行commit操作,但是transaction已经被设置为rollback-only了。所以,出现“Transaction rolled back because it has been marked as rollback-only”错误。
2. trycatch+Propagation.REQUIRED+Propagation.NESTED
//ServiceA @Autowired ServiceB serviceB; @Override @Transactional(propagation = Propagation.REQUIRED, readOnly = false) public void methodA() { try { serviceB.methodB(id); } catch (Exception e) { System.out.println("内层事务出错啦。"); } } //ServiceB @Override @Transactional(propagation = Propagation.NESTED, readOnly = false) public void methodB(String id) { }
当内层配置成 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? 从 Juergen Hoeller 的原话中我们可以找到答案, ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint(注意, 这是本文中第一次提到它, 潜套事务中最核心的概念), 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:
考察SpringMVC和Spring的区别
Spring是IOC和AOP的容器框架,SpringMVC是基于Spring功能之上添加的Web框架,想用SpringMVC必须先依赖Spring。
Spring框架有如下的特征:
SpringMVC是一个MVC模式的WEB开发框架;Spring有的优点他都有,他们之间的区别SpringMVC是web框架,是基于Spring框架演化而来的,主要用来简化web开发的,而Spring是一个轻量级的通用解决方案。
Java操作数据库,底层还是使用JDBC技术来进行的,而Spring也针对JDBC进行了封装,提供了许多使用JDBC的模板和驱动模块,为Spring应用操作关系数据库提供了更大的便利。
我们首先配置数据源,然后将数据源注入到jdbcTemplate中,这样jdbcTemplate就能操作数据库了,jdbcTemplate中封装了操作数据的常用方法execute
、query
、update
等方法,方便直接调用
考察SpringMVC的原理,以及@ResponseBody注解的使用
后端返回json的话,则需要使用的@ResponseBody
注解,
@RestController
注解则 该类中的所有的含有@RequestMapping
注解的方法返回的都是json格式的@ResponseBody
的话,也是返回json格式,但是每个方法都必须添加,比较麻烦,对于前后端分离的项目来说,使用第一种方案最方面考察常用的jvm的命令
jps -lvm 用于查看当前机器上运行的java进程。
可以查看某个进程的线程信息
查看此进程下线程的堆栈信息
考察linux命令
批量替换当前目录下所有文件中oldstring为newstring
grep old_string -rl /home
该命令批量将/home下的所有文件里面包含old_string的替换成new_string
grep和/home旁边的符号为反引号
考察jvm命令的使用方法
java提供的一个显示当前所有java进程pid的命令
常用的命令参数有
# 查看当前所有java进程
jps
# 输出应用程序main class的完整package名或者应用程序的jar文件完整路径名
jps -l
命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列
常用的命令参数有:
# 查看进程的内存映像信息
jmap pid
# 显示Java堆详细信息
jmap heap pid
# 产生核心的dump的java可执行文件
jmap executable
# 显示堆中对象的统计信息
jmap -histo:live pid
# 打印类加载器信息
jmap -clstats pid
# 生成堆转储快照dump文件
jmap -dump:format=b file=heapdump.phrof pid
jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息
jstack用于生成java虚拟机当前时刻的线程快照
常用的命令参数有:
# 长列表. 打印关于锁的附加信息
jstack -l pid
利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控
常用的命令参数有:
# 显示加载class的数量,及所占空间等信息。
jstat -class pid
# 显示VM实时编译(JIT)的数量等信息
jstat -compiler pid
# 显示gc相关的堆信息,查看gc的次数,及时间
jstat -gc pid
# VM内存中三代(young,old,perm)对象的使用和占用大小
jstat -gccapacity pid
# 统计gc信息
jstat -gcutil pid
考察自动装箱等基础知识
首先我们需要明确一点,Integer是一个Object对象类型,但是Java8中针对Integer进行了一些处理,我们先来看下代码:
int i1=20;
Integer i2 = 20;
Integer i3 = 128;
Integer i4 = 128;
int i5 = 128;
Integer i6 = 100;
Integer i7 = 100;
System.out.println(i1==i2); // true
System.out.println(i3==i4); // false
System.out.println(i4==i5); // true
System.out.println(i6==i7); // true
Integer i8 = new Integer(10);
Integer i9 = new Integer(10);
System.out.println(i8==i9); // false
其中 ,为什么i6i7就返回ture,而i3i4返回false呢,这里其实需要说明下:
我们使用Integer i=100
,其实调用的是Integer.valueO()
方法,我们先来扒下源码
/**
* 如果i在 -128~127之间,则对象从缓存中获取
* 否则就创建一个新的对象
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
上面的注释说的很明白了,如果一个数字不在-128~127之间,则就会重新创建一个对象,对象与对象使用 ==
比较的话,则肯定是返回false的,而 new Integer()
肯定是创建一个对象的,所以返回false
考察 volatile
volatile常用于保持内存可见性和防止指令重排序;
什么是内存可见性呢?
所有线程都能看到共享内存的最新状态就是内存可见性
我们都知道Java的内存模型,分为主内存和工作内存,其中主内存就是主线程的内存,而工作内存就是每个子线程的内存,每当创建一个子线程执行,则会将主内存中的数据备份到工作内存中,在子线程中操作的也是对应工作内存中的数据,然后等操作的结果同步到主线程中的内存中,这个过程很容易出现多个子线程同时同步数据到主内存中,造成数据混乱,出现线程安全问题;
volatile还能防止指令重排,我们的JVM编译器在编译程序时,为了性能考虑, 编译器和CPU可能会对指令重新排序,很容易造成执行的结果并不是我们想要的,
此volatile关键字就显现他的作用了,对了votaile修饰的变量,线程1中对变量V的修改,在线程2中是可见的,这样就能避免线程同步时,线程安全的问题
volatile能防止指令重排,主要是通过内存屏障来实现的
这里需要明确一点,volatile是变量对内存可见,同时也能防止指令重排,但是不能保证原子性,主要体现在:
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题
对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS(compare-and-swap)技术。
所谓CAS,表现为一组指令,当利用CAS执行试图进行一些更新操作时。会首先比较当前数值,如果数值未变,代表没有其它线程进行并发修改,则成功更新。如果数值改变,则可能出现不同的选择,要么进行重试,要么就返回是否成功。也就是所谓的“乐观锁”。
从AtomicInteger的内部属性可以看出,它依赖于Unsafe提供的一些底层能力,进行底层操作;以volatile的value字段,记录数值,以保证可见性
经典面试题,如果具体的话能用一个长篇博客来表达,此处给出关于面试题的解答
JDK1.8之前:
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表,查找的时间复杂度为O(N),N为链表的长度;
数组和链表中的每个元素和节点都是嵌套类Entry的实例,Entry包括四个属性:key,value,hash,和用于单向链表的next;
static class Entry<K,V> implements Map.Entry<K,V>{
Final K key;
V value;
Entry<K,V>next;
int hash;
}
JDK 1.8之后
HashMap结构:数组+链表+红黑树,查找的时间复杂度降低为O(logN).
Java7 中使用Entry来代表每个HashMap中的数据节点,Java8中使用Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的。
另外,和 Java7 稍微有点不一样的地方就是,Java7 是先扩容后插入新值的,Java8 先插值再扩容
他们之间的区别:
HashMap在多线程的情况下会出现循环链表
为什么会出现循环链表呢? 这个得从HashMap的扩容来说,扩容的过程如下:
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
这个也是我们的代码规范中,为什么规范在创建HashMap对象时需要制定Map的大小。
当多个线程同时对这个HashMap进行put操作,而察觉到内存容量不够,需要进行扩容时,多个线程会同时执行resize操作,而这就出现问题了:
以下模拟2两个线程同时扩容:
当前hashmap的空间为2(临界值为1),hashcode分别为0和1,在散列地址0处有元素A和B,这时候要添加元素C,C经过hash运算,得到散列地址为1,这时候由于超过了临界值,空间不够,需要调用resize方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:
线程一:读取到当前的hashmap情况,在准备扩容时,线程二介入
线程二:读取hashmap,进行扩容
线程一:继续执行
这个过程为,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将B.next=A,所以,这里继续复制A,让A.next=B,由此,环形链表出现:B.next=A; A.next=B
ConncurrentHash写的时候,使用的是分段锁:
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁
ReentrantLock与Synchronized相似,都是加锁方式同步,而且都是阻塞式的同步;不同的是:它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
其内部实现线程安全的原理可以使用一句话总结:内部使用了自旋锁的方式来解决多线程下的线程安全问题
什么是自旋锁?通过循环调用CAS操作来实现加锁;
它的性能比较好也是因为避免了使线程进入内核态的阻塞状态
考察索引原理相关知识
索引(Index)是帮助MySQL高效获取数据的数据结构,索引实际上也是一张表,保存了主键和索引字段,并指向实体表的记录;
我们知道数据库的数据持久化到了磁盘中,每次的数据查询,其实就是扫描磁盘,而磁盘IO的操作是非常耗费资源的;
那么我们就需要尽量减少磁盘的IO次数,我们可以时间分区思想来思考问题,如果总共有数据1000条,我们将50条数据分为一块,那么查询序号为300的数据,我们需要定位到第6分区的数据就能查到,这样就极大的减少了磁盘IO的次数。
其实上面的思想就是运用到搜索树的功能,其平均复杂度是lgN,具有不错的查询性能;
这里我们引入B+Tree的数据结构,那么什么是B+Tree?
在了解B+Tree之间,我们还得介绍下BTree;
BTree即二叉搜索树:
其特点如下:
BTree数据结构通过节点的横向扩展,从而压低整个Btree的高度,减少了节点io读取的次数。通常百万级别的数据会被压到3-5层的高度。
但是BTree也有缺点:
BTree里面的节点采用了key-value的基本存储结构,key是索引的数值,value是存储的data数值。由于我们对于BTree进行节点 比较的时候是基于内存进行数据比较,先从磁盘进行io读取数据,读取到cpu缓存中进行比对。
如果我们将节点的度设置到极致,例如说将度设置到100W,那么BTree的高度就会降低,查询的次数就会大大减少,是否可行?
这种想法是不可行的,节点会变得过大。每次进行节点数据读取的时候都需要将磁盘的数据加载到操作系统自身的缓存中。假设将度值设置过大,io一次读取的大小还是有限,过大的节点还是需要进行多次的io读取。
此时我们就需要B+Tree了,那么什么是B+Tree呢?
具有如下的特点:
仔细观察图中树结构的同学不知道是否有发现,B+Tree里面对于索引数据进行了适当的冗余存储,但是这一点相比于度大小的增加而言,并不会带来太多的性能影响。由于非叶子结点只存储key,并没有存储data数据,因此所有的非叶子结点的度可以增加地更大, 使得一次磁盘IO读取的数据更多,从磁盘读取到操作系统内存中的数据也大大增加。
仔细观察B+Tree里面的数据结构,会发现叶子节点里面有相应的顺序访问指针。
B+Tree的叶子节点之间的顺序访问指针的作用可以提高范围查询的效率。
具体的可以阅读如下的博文:
Mysql索引原理
考察索引使用的相关知识
我们通常在以下几种情况时创建索引:
索引的底层是一颗B+树,那么联合索引当然还是一颗B+树,只不过联合索引的健值数量不是一个,而是多个。构建一颗B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建B+树;
最左匹配原则:最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配;
举个例子:
建立联合索引(a,b,c),其索引匹配有以下几种情况:
select * from table_name where a = '1' and b = '2' and c = '3'
select * from table_name where b = '2' and a = '1' and c = '3'
select * from table_name where c = '3' and b = '2' and a = '1'
用到了索引,where子句几个搜索条件顺序调换不影响查询结果,因为Mysql中有查询优化器,会自动优化查询顺序
select * from table_name where a = '1'
select * from table_name where a = '1' and b = '2'
select * from table_name where a = '1' and b = '2' and c = '3'
都从最左边开始连续匹配,用到了索引,如果查询调价那种没有a的确定值匹配(不是范围匹配)则没有用到索引,是全表扫描的
如果不连续的话,比如:
select * from table_name where a = '1' and c = '3'
如果不连续时,只用到了a列的索引,b列和c列都没有用到
如果a是字符类型,那么前缀匹配用的是索引,后缀和中缀只能全表扫描了
select * from table_name where a like 'As%'; //前缀都是排好序的,走索引查询
select * from table_name where a like '%As'//全表查询
select * from table_name where a like '%As%'//全表查询
select * from table_name where a > 1 and a < 3
可以对最左边的列进行范围查询
select * from table_name where a > 1 and a < 3 and b > 1;
多个列同时进行范围查找时,只有对索引最左边的那个列进行范围查找才用到B+树索引,也就是只有a用到索引,在1<a<3的范围内b是无序的,不能用索引,找到1<a<3的记录后,只能根据条件 b > 1继续逐条过滤
select * from table_name where a = 1 and b > 3;
如果左边的列是精确查找的,右边的列可以进行范围查找, a=1的情况下b是有序的,进行范围查找走的是联合索引
一般情况下,我们只能把记录加载到内存中,再用一些排序算法,比如快速排序,归并排序等在内存中对这些记录进行排序,有时候查询的结果集太大不能在内存中进行排序的话,还可能暂时借助磁盘空间存放中间结果,排序操作完成后再把排好序的结果返回客户端。Mysql中把这种再内存中或磁盘上进行排序的方式统称为文件排序。文件排序非常慢,但如果order子句用到了索引列,就有可能省去文件排序的步骤
select * from table_name order by a,b,c limit 10;
因为b+树索引本身就是按照上述规则排序的,所以可以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了
order by的子句后面的顺序也必须按照索引列的顺序给出,比如
select * from table_name order by b,c,a limit 10;
这种颠倒顺序的没有用到索引
select * from table_name order by a limit 10;
select * from table_name order by a,b limit 10;
这种用到部分索引
select * from table_name where a =1 order by b,c limit 10;
联合索引左边列为常量,后边的列排序可以用到索引
那么题目中的 现在一个表有三列a b c,组合索引(a,b,c)查询的时候where a like ? and b=? and c=?能用到这个组合索引吗?
我们就有了答案了:
这里要区分下: a的范围匹配 是否是前缀匹配,如果是前缀匹配则使用了联合索引,如果不是前缀匹配,则走的是全表扫描
考察sql执行计划
当我定位查询缓慢的sql语句时,通过我们会在sql的执行语句之前添加explain,查询的结果会出现10列:
column | remark |
---|---|
id | 选择标识符 |
select_type | 表示查询的类型 |
table | 输出结果集的表 |
partitions | 匹配的分区 |
type | 表示表的连接类型 |
possible_keys | 表示查询时,可能使用的索引 |
ref | 列与索引的比较 |
key | 表示实际使用的索引 |
filtered | 按表条件过滤的行百分比 |
Extra | 执行情况的描述和说明 |
下面我们来重点说明下:select_type
和 type
值的含义
其中select_type
如下:
value | remark |
---|---|
SIMPLE | 简单SELECT,不使用UNION或子查询等 |
PRIMARY | 子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY |
UNION | UNION中的第二个或后面的SELECT语句 |
DEPENDENT UNION | UNION中的第二个或后面的SELECT语句,取决于外面的查询 |
UNION RESULT | UNION的结果,union语句中第二个select开始后面所有select |
SUBQUERY | 子查询中的第一个SELECT,结果不依赖于外部查询 |
DEPENDENT SUBQUERY | 子查询中的第一个SELECT,依赖于外部查询 |
DERIVED | 派生表的SELECT, FROM子句的子查询 |
UNCACHEABLE SUBQUERY | 一个子查询的结果不能被缓存,必须重新评估外链接的第一行 |
type
对应的列值:
value | remark |
---|---|
ALL | Full Table Scan, MySQL将遍历全表以找到匹配的行 |
index | Full Index Scan,index与ALL区别为index类型只遍历索引树 |
range | 只检索给定范围的行,使用一个索引来选择行 |
ref | 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 |
eq_ref | 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件 |
const/system | 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system |
NULL | MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成 |
考察实际的项目经验
结合实际情况说明下,一般就以下几种情况:
考察反射相关知识,以及单例模式的特性
反射可以获取父类的私有方法,代码如下:
public class MockData extends Parent{
private String name;
public static void main(String[] args) throws Exception{
Class parentClass = MockData.class.getSuperclass();
Method method = parentClass.getDeclaredMethod("saySomething", null);
method.setAccessible(true);
method.invoke(parentClass.newInstance(), null);
}
}
class Parent{
private void saySomething() {
System.out.println("parent's method");
}
}
首先单例模式的常用实现方式是将构造器私有化,这样外部就不能通过new的方式来创建对象,但是这个方法可以通过反射来创建新的对象,让实例不唯一,单例模式就遭到了破坏。
解决的方法其实很简单,就是在构造器中添加判断,如果对象已经存在,就直接抛出异常
//volatile防止指令重排序,内存可见(缓存中的变化及时刷到主存,并且其他的内存失效,必须从主存获取)
public static volatile DoubleLock doubleLock = null;
private DoubleLock(){
//构造器必须私有 不然直接new就可以创建
//构造器判断,防止反射攻击
if(doubleLock != null){
throw new IllegalStateException();
}
}
考察JVM内存方面的相关知识,
首先这里的内存模型描述有点问题,其实考官主要是考察内存结构方面的知识点:
每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记
本地方法栈则是为Native方法服务
用于存储虚拟机加载的:静态变量+常量+类信息+运行时常量池(类信息:类的版本、字段、方法、接口、构造函数等描述信息 ),默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小
所有的对象实例以及数组都要在堆上分配,此内存区域的唯一目的就是存放对象实例
堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身)
栈是java 方法执行的内存模型
每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息
每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的
由于堆是JavaGC的主要区域,所以我们这里讲解下堆内部的结构:新生代(Eden区+2个Survivor区) 老年代 永久代(HotSpot有)
新创建的对象——>Eden区
GC之后,存活的对象由Eden区 Survivor区0进入Survivor区1
再次GC,存活的对象由Eden区 Survivor区1进入Survivor区0
这里为什么需要两个S区呢?
我们假设一下,如果没有Survivor区,新生代只有Eden区。
当Eden区装满后,Minor GC进行垃圾回收,幸存的对象会直接放入老年代,可以想到,要不了多久老年代就会装满,便会进行Major GC且连带Minor GC也就是Full GC,每次Full GC都会消耗大量的时间。
Survivor具有预筛选保证,只有对象到一定岁数才会送往老年代,Survivor区可以减少被送到老年代的对象,进而减少Full GC发生
至于为什么使用两个S区?
我们知道新生代使用复制回收算法,我们设想一下只有一个Survivor区会发生什么情况:
当Eden区填满后,Minor GC进行垃圾回收,幸存的对象会移动到Survivor区,这样循环往复。此时,Survivor区被装满了,也会进行Minor GC,将一些对象kill掉,幸存的对象只能保存在原来的位置,这样就会出现大量的内存碎片(被占用内存不连续)
内存碎片化是严重影响性能的,可以设想当有一个稍大一点的对象从Eden区存活转入Survivor区,发现空闲内存断断续续,没有他能落脚的地方,就只能直接存到老年代了,如此反复,老年代会出现我们第一部分的问题。
如果有两个Survivor区,便可以保证一个为空,另一个是非空且无碎片保存的
2. 老年代
对象如果在新生代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到老年代
如果新创建对象比较大(比如长字符串或大数组),新生代空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)
老年代的空间一般比新生代大,能存放更多的对象,在老年代上发生的GC次数也比年轻代少
我们常用的垃圾回收算法有:
而老年代使用的就是 标记整理算法
可以简单理解为方法区(本质上两者并不等价)
Jdk1.6及之前:常量池分配在永久代
Jdk1.7:有,但已经逐步“去永久代”
Jdk1.8及之后:没有永久代(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
ThreadLocal是如何为每个线程创建变量的副本的:
如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null,不重写的话,如果查询不到会报空指针异常
常用的创建线程有两种方法:extends Thread和 implements Runnable,start方法是开启一个线程,run方法是执行类中的方法,此时没有开启线程。
创建线程和销毁线程的花销是很大的,如果在业务逻辑中创建和销毁线程,这样很消耗系统资源,降低系统性能,使用线程池可以减少线程创建和销毁的过程,线程池有如下的优点:
常见的线程池
创建单线程的线程池,主要是通过一个线程池来实现执行的顺序化
缺点:堆积的请求处理队列,可能会消耗大量的内存,引起OOM异常
创建定长线程,控制线程最大并发数,超出的线程会在队列中等待
缺点:堆积的请求处理队列,可能会消耗大量的内存,引起OOM异常
创建可缓存线程池,如果线程池长度超过处理的需求,可灵活的回收空闲线程池,若不可回收,则创建新的线程
缺点:线程池最大数是Integer.MAX_VALUE,创建很多的线程导致OOM
创建定长线程池,支持周期性任务执行
缺点:线程池最大数是Integer.MAX_VALUE,创建很多的线程导致OOM
线程池几个重要参数说明
核心池的大小,线程池刚创建时,默认是没有线程的,只有当任务来临时才创建线程,如果创建的线程多于corePoolSize的大小,则再次创建的线程就会存储在缓存队列中等待执行
线程池最大线程数,线程池中最多能创建多个线程
线程没有执行任务时,最终保持多长时间终止,默认情况下,只有当线程池中的线程树大于corePoolSize才会生效
keepAliveTime的单位
阻塞队列,用来存储等待执行的任务
用于创建线程的工厂,对创建出来的线程进行管理
表示拒绝处理任务时的策略
线程池有四种拒绝策略:
是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃
丢弃队列最前面的任务,然后重新提交被拒绝的任务
由调用线程处理该任务
我们也可以自己实现RejectedExecutionHandler
接口,来自定义拒绝策略
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。