Java面试题目总结与回答
歇了几个月,弹尽粮绝,不得不出门为资本家打工卖命挣取养命钱、糊口饭。所以不得不再次看望几个月没碰面的老朋友Java,数日不见,如隔三秋,岁月总是不动声色流逝,而对老朋友Java的记忆却是渐行模糊。经过几天的照面,试图做个面试题目总结,希望可以找个钱多又轻松又可以少被资本家剥削的工作,总是想要的太多,拥有的太少,哎,不说了,说多了都是泪。
本文结合自己对Java面试题目的理解试图做一个回答总结,题目部分来源于某博同学某年某日某司的面试题目,我也做了总结回答,各位看官如果觉得回答的不对或者不好,可以扔砖头,反正面试这段时间我总会带着精钢不坏之头盔,不怕。
最后一段废话,按上次本混混号应该写机器学习的第一个模型:回归模型及机器学习模型评价,回归是很简单的模型,但其涉及到机器学习算法(模型)的建模及评价却是个大的话题,贯穿机器学习始终,作为一种方法论我觉得很有必要做个总结。先做个预告,也是对自己的一个督促,督促自己去探索、去学习,等糊口饭安定下来再写。
最最后附议,由于本文首先写自于微信公众号,本篇是****第一篇,如果喜欢可关注微信公众号,它作为后期更新的首发阵地,见以下二维码,或搜索公众号:正直的小混混。
好了,开始Java面试题目总结及回答。
1.请说说JVM的内存结构?
JVM内存结构也叫JVM运行时数据区。
JVM内存主要包括五大部分,堆区,方法区,虚拟机栈,本地方法栈,程序计数器,前两个是线程共享的,后三个是线程私有的。
堆区:是JVM管理内存最大的一块,负责存储对象实例,也是GC机制管理的主要区域,堆区又分为年轻代和老年代,年轻代包括Eden区,两个幸存区。
方法区:在Java虚拟机规范中,方法区作为堆区的一个逻辑部分对待,但事实上它并属于堆区,逻辑上的永久代。方法区负责存储已经被虚拟机加载的类信息,包括版本,字段,方法、接口、final常量、静态变量、编译器即时编译的代码等。
虚拟机栈:一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。
本地方法栈:基本和虚拟机栈相同,唯一不同的是保存java本地方法的栈帧。
程序计数器:,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。
补充:对于JVM结构,除了以上的JVM内存结构,还包括以下:
类加载器,在JVM启动时或者在类运行时将需要的class加载到JVM中;
执行引擎:负责执行class文件中包含的字节码指令;
2.请说说GC的工作机制?
GC负责回收无任何引用对象的内存空间,而不是回收对象本身。要说明白GC的工作机制必须说Java对象的内存分配。
Java对象内存分配,主要是在Java堆区上的内存分配,采用分代分配机制,GC机制就主要采用分带回收机制。
对象被创建时,首先分配在年轻代的Eden区(大对象可以直接被创建分配到年老代),大多数对象在被创建不久后就不在使用,因此很快变得不可达,当Eden区满时,执行Minor GC将不可达对象清除,并将剩余的对象复制到一个存活区S0中,此时,另一个存活区S1是空白。
到下一次Eden区满,在执行一次Minor GC,将Eden区不可达对象清除,将存活对象复制到S1区,并将刚才S0区中不可达对象清除,将此时Eden区存活对象和S0存活对象一并拷贝到S1区。
当两个幸存区切换几次,达到设定阈值,将任然存活的对象复制到年老区中,此时其实只有很小的一部分。
年轻代采用算法是“停止-复制算法”。
在年轻到经过多次停止-复制法清除不可达对象后,存活的对象都被复制到年老代中,需要更多的内存空间,所以年老代的空间一般比年轻代大,但在老年代发生GC的次数比较少,当年老带内存不足时,执行Major GC(Full GC)清除不可达对象,采用“标记-整理算法”,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
最后一个的方法区,也即使永久代,主要回收两种:常量池中的常量,无用的类信息。常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
1)类的所有实例都已经被回收
2)加载类的ClassLoader已经被回收
3)类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
3.服务器qps量上不去,服务器消耗比较大,排查思路?
QPS每秒的响应请求数,也即最大吞吐能力,可衡量服务器的并行处理能力,并行能力越强,QPS值越高,QPS量上不去说明服务器的并行能力不强;
服务器消耗比较大,说明空闲线程太多。
排查思路:1)查看是否有死锁情况产生;
2)查看是否有线程阻塞情况产生;
3)查看比较消耗CPU的代码,比如循环、字符串拼接\查找\替换、编码\解码、序列化\反序列化、压缩;
4)查看同步锁代码范围。
4.说说常见的网页状态码?
成功2×× 成功处理了请求的状态码。
200 服务器已成功处理了请求并提供了请求的网页。
204 服务器成功处理了请求,但没有返回任何内容。
重定向3×× 每次请求中使用重定向不要超过 5 次。
301 请求的网页已永久移动到新位置。当URLs发生变化时,使用301代码。 搜索引擎索引中保存新的URL。
302 请求的网页临时移动到新位置。搜索引擎索引中保存原来的URL。
304 如果网页自请求者上次请求后没有更新,则用304代码告诉搜索引擎机器 人,可节省带宽和开销。
客户端错误4×× 表示请求可能出错,妨碍了服务器的处理。
400 服务器不理解请求的语法。
403 服务器拒绝请求。
404 服务器找不到请求的网页。服务器上不存在的网页经常会返回此代码。
410 请求的资源永久删除后,服务器返回此响应。该代码与 404(未找到) 代码相似,但在资源以前存在而现在不存在的情况下,有时用来替 代404 代码。如果资源已永久删除,应当使用 301 指定资源的新位 置。
服务器错误5×× 表示服务器在处理请求时发生内部错误。这些错误可能是服务器本身的错 误,而不是请求出错。
500 服务器遇到错误,无法完成请求。
503 服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状 态。
5.Runtime和非RuntimeException?
Java将所有的错误封装为一个对象,其根本父类为Throwable, Throwable有两个子类:Error和Exception。
Error:一般为底层的不可恢复的类;
Exception:分为未检查异常(RuntimeException)和已检查异常(非 RuntimeException)。
RuntimeException:是因为程序员没有进行必需要的检查,因为疏忽和错误而引起的错误。几个经典的RunTimeException如下:
1).java.lang.NullPointerException;
2).java.lang.ArithmaticException;
3).java.lang.ArrayIndexoutofBoundsException
非RuntimeException:定义方法时必须声明所有可能会抛出的exception; 在调用这个方法时,必须捕获它的checked exception,不然就得把它的exception传递下去;exception是从java.lang.Exception类衍生出来的。例如:IOException,SQLException就属于Exception。
6.左连接、右连接区别与写法?
左连接where只影向右表,右连接where只影响左表。
Left Join:
select * from tbl1 Left Join tbl2 where tbl1.ID = tbl.ID2
左连接后的检索结果是显示tbl1的所有数据和tbl2中满足where 条件的数据。
简言之 Left Join影响到的是右边的表
Right Join:
select * from tbl1 Right Join tbl2 where tbl1.ID = tbl2.ID
检索结果是tbl2的所有数据和tbl1中满足where 条件的数据。
简言之 Right Join影响到的是左边的表。
在做表与表的连接查询时,大表在前,小表在后;不使用表别名,通过字段前缀区分不同表中的字段;查询条件中的限制条件要写在表连接条件前;尽量使用索引的字段做为查询条件。
7.乐观锁和悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
8.String,StringBuffer,StringBilder
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且大量浪费有限的内存空间。
StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。 每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量。
StringBuffer和StringBuilder类功能基本相似,主要区别在于StringBuffer类的方法是多线程、安全的,而StringBuilder不是线程安全的,相比而言,StringBuilder类会略微快一点。对于经常要改变值的字符串应该使用StringBuffer和StringBuilder类。
9.抽象类和接口
抽象类和接口都是定义一类对象的一些抽象行为,但接口比起抽象更能体现对象的多态特性,它明确一类对象的行为,而抽象在多态这个特性上显得稍弱,但它在封装特性上表型强劲。
10.单类两种经典写法
10.1.静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.instance;
}
}
10.2.双重检查
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
11.利用反射创建对象A和数组B?
创建对象:
Class classType = Class.forName(“A");
Object a = classType.newInstance();
创建数组使用反射包下的Array类:
Class<?> classType = Class.forName("B");
Object b = Array.newInstance(classType, 10);
12.如何保证系统的高并发?
从两个层面回答:
1)技术:采用负载均衡工具如nginx,限流
2)业务:模块化
13.列举几个高性能队列?
ConcurrentLinkedQueue:无阻塞,无界。
ArrayBlockingQueue:阻塞,有界。
LinkedBlockingQueue:阻塞,无界(指定数量即可成有界队列)。
SynchronousQueue:同步转移队列,一个线程入队列,另一个线程需要立即take出来,不然报错。
PriorityBlockingQueue:排序队列,入队列不排序,每次take队列数据,对剩余数据排序。
14.自定义线程池
public ThreadPoolExecutor(int coreSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
corePoolSize(核心线程池大小),maximumPoolSize(最大线程池大小),workQueue之间关系:
1)当提交任务数小于corePoolSize时,新提交任务将创建新线程执行任务,即使此时线程池中存在空闲线程。
2)当提交任务数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 。
3)当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4)当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5)当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6)当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 。
当使用有界队列时:若有新任务需要执行,如果线程池实际线程数小于corePollSize,则优先创建线程执行任务。若大于corePoolSize,则会将任务加入队列,若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务,若线程数大于maximumPoolSize,则执行拒绝策略。
当使用无界队列时:与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队列失败的情况。当有新任务到来,系统线程数小于corePoolSize,则新建线程执行任务;当大于corePoolSize后,任务进入队列等待。若任务创建和处理速度差异很大,无界队列会快速增长,知道耗尽系统内存。
拒绝策略,JDk提供四种,若要自定义可以实现RejectedExecutionHandler接口即可。
15.解释一下Spring的事物管理?
Spring对事务管理主要有两种方式,编程式和声明式。前者代码编写量大,后者简单实用。
对于编程式事物管理,主要涉及Spring的三个对象:
TransactionDefinition负责事物的属性定义,比如事物的传播特性和隔离级别;
TranscationStatus:代表了当前的事务,可以提交,回滚
PlatformTransactionManager:spring提供的用于管理事务的基础接口,其下有一个实现的抽象类AbstractPlatformTransactionManager,我们使用的事务管理类例如DataSourceTransactionManager等都是这个类的子类。
Spring配置声明式事务:
* 配置DataSource
* 配置事务管理器
* 事务的传播特性
* 那些类那些方法使用事务
Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager和代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分。
DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,
比如使用Hibernate进行数据访问 时,DataSource实际为SessionFactory,TransactionManager的实现为 HibernateTransactionManager。
关于事物:
事务是逻辑处理原子性的保证手段,通过使用事务控制,可以极大的避免出现逻辑处理失败导致的脏数据等问题。事务最重要的两个特性,是事务的传播级别和数据隔离级别。
传播级别定义的是事务的控制范围,事务隔离级别定义的是事务在数据库读写方面的控制范围。事务的7种传播级别,Spring默认采用PROPAGATION_REQUIRED ,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
数据隔离级别分为不同的四种,Spring默认采用READ COMMITTED,也是大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
16.算法,1-1000,随机取500个数不重复
思路:假设现在1-5五个数,取3个不重复的随机数。(字丑慎看,图片旋转不过来了,抱歉,放松一下颈椎吧。)
因为python语法比较简单,我直接上Python代码,对于Java程序员可按以上思路写代码。该算法时间复杂度,假设取随机数为N,即为O(N)。
def gen_random(arr, N):
"""
从数组arr中生成N个不重复的随机数
:param arr: 给定数组
:param N:生成随机数个数
:return:N个不相等的随机数
"""
size = len(arr)
res = list()
for i in range(N):
idx = random.randint(i, N)
val = arr[idx]
if idx != i:
arr[i], arr[idx] = arr[idx], arr[i]
res.append(val)
return res
17.快速排序算法实现
思路:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序算法时间复杂度:
-
最优时间复杂度:O(nlogn)
-
最坏时间复杂度:O(n2)
-
稳定性:不稳定
步骤为:
1)从数列中挑出一个元素,称为"基准"(pivot)
2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3)递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
(图片来源:某课程,抱歉我也不知道他是谁,侵删)
还是Python实现,没办法,就是这么喜欢她。
def quick_sort(alist, start, end):
"""
递归实现快递排序
:param alist:原始乱序序列
:param start:子序列左边游标
:param end:子序列右边游标
:return:升序序列
"""
# 递归终止条件
if start >= end:
return
middle_val = alist[start]
low = start
high = end
while low < high:
while low < high and alist[high] >= middle_val:
high -= 1
alist[low] = alist[high]
while low < high and alist[low] < middle_val:
low += 1
alist[high] = alist[low]
alist[low] = middle_val
# 递归左序列排序
quick_sort(alist, start, low - 1)
# 递归右序列排序
quick_sort(alist, low + 1, end)
def one_time_quick_sort(alist):
"""一次快速排序,完成后列表被选定中间值分成两部分,左边序列小于中间值,右边序列大于中间值"""
n = len(alist)
# 取中间值为列表第一个元素
middle_val = alist[0]
# 左序列游标
low = 0
# 右序列游标
high = n - 1
while low < high:
# 右边序列目的要比中间值大,当大于中间值时,左移动右边游标,取等号保证相同大小的数值全在右边序列
while low < high and alist[high] >= middle_val:
high -= 1
# 将右边序列中小于中间值的数据放到左游标处
alist[low] = alist[high]
# 左边序列目的要比中间值小,当小于中间值时,右移动右边游标
while low < high and alist[low] < middle_val:
low += 1
# 将左边序列中大于中间值的数据放到右游标处
alist[high] = alist[low]
##完成最外层while循环后,就完成了一次快速排序,此时low=high,把中间值放到此位置处
alist[low] = middle_val
if __name__ == "__main__":
li = [54, 26, 93, 67, 77, 31, 44, 55, 20]
print '原始列表:\n', li
one_time_quick_sort(li)
print "一次快速排序后(中间值为第一个元素):\n", li
print "—————————分割线———————————"
li = [54, 26, 93, 67, 77, 31, 44, 55, 20]
quick_sort(li, 0, len(li) - 1)
print "一次快速排序后(中间值为第一个元素):\n", li
写在最后,以后遇到不错的题目,持续更新。