java常见面试题——适用于2-5年后端开发面试(二)

1、说一说HashMap的实现原理

在JDK8中,HashMap底层是采用“数组+链表+红黑树”来实现的。

HashMap是基于哈希算法来确定元素的位置(槽)的,当我们向集合中存入数据时,它会计算传入的Key的哈希值,并利用哈希值取余来确定槽的位置。如果元素发生碰撞,也就是这个槽已经存在其他的元素了,则HashMap会通过链表将这些元素组织起来。如果碰撞进一步加剧,某个链表的长度达到了8,则HashMap会创建红黑树来代替这个链表,从而提高对这个槽中数据的查找的速度。

HashMap中,数组的默认初始容量为16,这个容量会以2的指数进行扩容。具体来说,当数组中的元素达到一定比例的时候HashMap就会扩容,这个比例叫做负载因子,默认为0.75。自动扩容机制,是为了保证HashMap初始时不必占据太大的内存,而在使用期间又可以实时保证有足够大的空间。采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。

加分回答

HashMap是非线程安全的,所以在多线程环境下,各线程同时触发HashMap的改变时,都有可能会发生冲突。所以,在多线程环境下不建议使用HashMap可以考虑使用Collections将HashMap转为线程安全的HashMap,更为推荐的方式则是使用ConcurrentHashMap。


2、介绍一下Java中的IO流

流是Java对不同输入源输出源的抽象,代表了从起源到接收的有序数据,有了它程序就可以采用统一的方式来访问不同的输入源和输出源了。

    按照数据的流向,可以将流分为输入流和输出流。其中,输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。

    按照数据的类型,可以将流分为字节流和字符流。其中,字节流操作的数据单元是byte(8位的字节),而字符流操作的数据单元是char(16位的字符)。

    按照使用的场景,可以将流分为节点流和处理流。其中,节点流可以直接从/向一个特定的IO设备读/写数据,也称为低级流。而处理流则是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也成为高级流。

    Java中的IO流主要有4个基类:InputStream、OutputStream、Reader、Writer。其中,InputStream代表字节输入流,OutputStream代表字节输出流,Reader代表字符输入流,Writer代表字符输出流。其他的IO流都是从这4个基类派生而来的,并且子类的名字往往以基类的名字结尾,所以通过类名我们很容易识别某个流的作用。

    Java为我们提供了大量的IO流实现,我们没办法逐个介绍,下面举一些较为常用的例子:

1. 用于访问文件的FileInputStream、FileOutputStream、FileReader、FileWriter。

2. 带有缓冲功能的BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。

3. 具有转换功能的InputStreamReader、OutputStreamWriter。

4. 支持打印功能的PrintStream、PrintWriter。


3、说一说 Linux 如何管理内存

Linux 操作系统是采用段页式内存管理方式:

页式存储管理能有效地提高内存利用率(解决内存碎片),而分段存储管理能反映程序的逻辑结构并有利于段的共享。将这两种存储管理方法结合起来,就形成了段页式存储管理方式。

段页式存储管理方式即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,为了实现从逻辑地址到物理地址的转换,系统中需要同时配置段表和页表,利用段表和页表进行从用户地址空间到物理内存空间的映射。

系统为每一个进程建立一张段表,每个分段有一张页表。段表表项中至少包括段号、页表长度和页表始址,页表表项中至少包括页号和块号。在进行地址转换时,首先通过段表查到页表始址,然后通过页表找到页帧号,最终形成物理地址。


4、说一说ConcurrentHashMap的实现原理

在JDK8中,ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树”的形式。同时,它又采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。


5、说一说NIO的实现原理

NIO是基于IO多路复用模型的实现,它包含三个核心组件,分别是Buffer、Channel、Selector。

  1. NIO是面向缓冲区的,在NIO中所有的数据都是通过缓冲区处理的。Buffer就是缓冲区对象,无论读取还是写入,数据都是先进入Buffer的。Buffer的本质是一个数组,通常它是一个字节数组,也可以是其他类型的数组。Buffer是一个接口,它的实现类有ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
  2. Channel是一个通道,可以通过它读取和写入数据。与流不同的是,流是单向的,而Channel是双向的。数据可以通过Channel读到Buffer里,也可以通过Channel写入到Buffer里。为了支持不同的设备,Channel接口有好几种子类,如FileChannel用于访问磁盘文件、SocketChannel和ServerSocketChannel用于TCP协议的网络通信、DatagramChannel用于UDP协议的网络通信。
  3. Selector是多路复用器,可以通过它监听网络IO的状态。它可以不断轮询注册的Channel,如果某Channel上有连接、读取、写入事件发生,则这个Channel就处于就绪状态,就会被Selector轮询出来。所有被轮询出来的Channel集合,我们可以通过SelectionKey获取到,然后进行后续的IO操作。

6、说一说zset类型的底层数据结构

zset底层的存储结构包括ziplist(压缩表)或skiplist(跳表),在同时满足有序集合保存的元素数量小于128个和有序集合保存的所有元素的长度小于64字节的时候使用ziplist(压缩表),其他时候使用skiplist(跳表)。

压缩列表是Redis为了节约内存而开发的,它是由一系列特殊编码的连续内存块组成的顺序型数据结构。

跳跃表是在链表的基础上,通过增加索引来提高查找效率的。


7、说一说你对redo log、undo log、bin log的了解 

bin log是在服务层实现的;redo log和undo log是在引擎层实现的,且是innodb引擎独有的,主要和事务相关。

bin log中记录的是整个mysql数据库的操作内容,对所有的引擎都适用,包括执行DDL、DML,可以用来进行数据库的恢复及控制。

redo log中记录的是要更新的数据,比如一条数据已提交成功,并不会立即同步到磁盘,而是记录到redo log中,等待合适的时机再刷盘,为了实现事务的持久性。

undo log中记录的是当前操作的相反操作,如一条insert语句在undo log中会对应一条delete语句,在任务回滚时会用到undo log,实现事务的原子性,同时会用在MVCC中,undo会有一条记录的多个版本,用在快照读中。


8、说一说HashMap的扩容机制

1.如果你没有指定初始长度的话,默认会为null,这时候往里面添加元素他就会进行扩容,初始数组大小为16.

2.当元素添加到链表上,链表的长度大于8的时候,数组长度小于64时,会将链表转换成红黑树

3.当添加元素达到了他的阈值,默认的加载因子是0.75,比如16的容量,你加到12的时候他就会进行扩容。

扩容是resize方法,每次扩容都是两倍,他是用一个新的数组代替了原来那个小的数组,将原数组的数据迁移到新数组里面。


9、介绍一下Java中的序列化与反序列化

序列化:对象转换为字节(包含对象的类型、数据等信息)。通过序列化流(ObjectOutputStream)把对象以流的方式写入到文件中保存。

反序列化:字节重构为对象。通过反序列化流(ObjectInputStream)把文件中保存的对象以流的方式读取出来使用。


10、什么是MVC

MVC的全名是Model View Controller,是一种使用“模型-视图-控制器”设计创建Web应用程序的模式,同时提供了对HTML、CSS和JavaScript的完全控制,它是一种软件设计典范。

使用MVC的目的在于将M(业务模型)和V(用户界面)的实现代码分离,从而使同一个程序可以使用不同的表现形式。C(控制器)存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。

MVC使用一种业务逻辑、数据与界面显示分离的方法来组织代码,将众多的业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑,达到减少编码的时间。


11、创建线程有哪几种方式

1.继承Thread类

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start(方法是一个native方法,它将启动一个新线程,并执行ru0方法。这种方式实现多线程很简单,通过自己创建的类直接extend Thread,并复写runO方法,就可以启动新线程并执行自己定义的runO方法:优点:代码简单。“

缺点:该类无法继承别的类

2.实现Runnable接口

Java中的类属于单继承如果自己的类已经extends另一个类,就无法直接extends

Thread,,但是一个类继承一个类同时是可以实现多个接口的优点:继承其他类。统一实现该接口的实例可以共享资源。·缺点:代码复杂

3.实现Callable接口

实现Runnable和实现Callable接口的方式基本相同,不过Callable接口中的callo方法有返回值,Runnable接口中的runO方法无返回值

4.线程池方式

线程池,其实就是一个容纳多个线程的容器,其中的线程可以重复使用,省去了频繁创建线程对象的操作,因为反复创建线程是非常消耗资源的·优点:实现自动化装配,易于管理,循环利用资源。


12、内存溢出问题该如何解决

内存溢出的解决方案:

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

第四步,使用内存查看工具动态查看内存使用情况。

1.java堆内存溢出

设置的jvm内存太小,对象所需内存太大,创建对象时分配空间,就会抛出这个异常。

解决方法:

首先,如果代码没有什么问题的情况下,可以适当调整-Xms和-Xmx两个jvm参数,使用压力测试来调整这两个参数达到最优值。

其次,尽量避免大的对象的申请,像文件上传,大批量从数据库中获取,这是需要避免的,尽量分块或者分批处理,有助于系统的正常稳定的执行。

最后,尽量提高一次请求的执行速度,垃圾回收越早越好。

2.java堆内存泄漏

Java中的内存泄漏是一些对象不再被应用程序使用但垃圾收集无法识别的情况。

因此,这些未使用的对象仍然在Java堆空间中无限期地存在。不停的堆积最终会触发javlang.OutOfMemoryError。

解决方法:

重写equals方法即可:

3.垃圾回收超时内存溢出

当应用程序耗尽所有可用内存时,GC开销限制超过了错误,而GC多次未能清除它,这时便会引发java.lang.OutOfMemoryError。

当JVM花费大量的时间执行GC,而收效甚微,而一旦整个GC的过程超过限制便会触发错误。

解决方法:

要减少对象生命周期,尽量能快速的进行垃圾回收。

4.Metaspace内存溢出

元空间的溢出,系统会抛出java.lang.OutOfMemoryError: Metaspace。

出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。

解决方法:

默认情况下,元空间的大小仅受本地内存限制。但是为了整机的性能,尽量还是要对该项进行设置,以免造成整机的服务停机。

5.直接内存内存溢出

在使用ByteBuffer中的allocateDirect()的时候会用到,很多javaNIO(像netty)的框架中被封装为其他的方法,

出现该问题时会抛出java.lang.OutOfMemoryError: Direct buffer memory异常。

如果你在直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而不做clear的时候就会出现类似的问题。

解决办法:

如果经常有类似的操作,可以考虑设置参数:-XX:MaxDirectMemorySize,并及时clear内存。

6.栈内存溢出

当一个线程执行一个Java方法时,JVM将创建一个新的栈帧并且把它push到栈顶。

此时新的栈帧就变成了当前栈帧,方法执行时,使用栈帧来存储参数、局部变量、中间指令以及其他数据。

当一个方法递归调用自己时,新的方法所产生的数据(也可以理解为新的栈帧)将会被push到栈顶,方法每次调用自己时,

会拷贝一份当前方法的数据并push到栈中。因此,递归的每层调用都需要创建一个新的栈帧。这样的结果是,

栈中越来越多的内存将随着递归调用而被消耗,如果递归调用自己一百万次,那么将会产生一百万个栈帧。这样就会造成栈的内存溢出。

解决办法:

如果程序中确实有递归调用,出现栈溢出时,可以调高-Xss大小,就可以解决栈内存溢出的问题了。

递归调用防止形成死循环,否则就会出现栈内存溢出。

7.创建本地线程内存溢出

线程基本只占用heap以外的内存区域,也就是这个错误说明除了heap以外的区域,无法为线程分配一块内存区域了,

这个要么是内存本身就不够,要么heap的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了。

解决方法:

首先检查操作系统是否有线程数的限制,使用shell也无法创建线程,如果是这个问题就需要调整系统的最大可支持的文件数。

日常开发中尽量保证线程最大数的可控制的,不要随意使用线程池。不能无限制的增长下去。

8.超出交换区内存溢出

在Java应用程序启动过程中,可以通过-Xmx和其他类似的启动参数限制指定的所需的内存。

而当JVM所请求的总内存大于可用物理内存的情况下,操作系统开始将内容从内存转换为硬盘。

一般来说JVM会抛出Out of swap space错误,代表应用程序向JVM native heap请求分配内存失败并且native heap也即将耗尽时,

错误消息中包含分配失败的大小(以字节为单位)和请求失败的原因。

解决办法:

增加系统交换区的大小,我个人认为,如果使用了交换区,性能会大大降低,不建议采用这种方式,

生产环境尽量避免最大内存超过系统的物理内存。其次,去掉系统交换区,只使用系统的内存,保证应用的性能。

9.数组超限内存溢出

有的时候会碰到这种内存溢出的描述Requested array size exceeds VM limit,

一般来说java对应用程序所能分配数组最大大小是有限制的,只不过不同的平台限制有所不同,

但通常在1到21亿个元素之间。当Requested array size exceeds VM limit错误出现时,

意味着应用程序试图分配大于Java虚拟机可以支持的数组。JVM在为数组分配内存之前,

会执行特定平台的检查:分配的数据结构是否在此平台是可寻址的。

解决方法:

因此数组长度要在平台允许的长度范围之内。不过这个错误一般少见的,主要是由于Java数组的索引是int类型。

Java中的最大正整数为2 ^ 31 - 1 = 2,147,483,647。 并且平台特定的限制可以非常接近这个数字,

例如:我的环境上(64位macOS,运行Jdk1.8)可以初始化数组的长度高达2,147,483,645(Integer.MAX_VALUE-2)。

若是在将数组的长度再增加1达到nteger.MAX_VALUE-1会出现的OutOfMemoryError。

10.系统杀死进程内存溢出

在描述该问题之前,先熟悉一点操作系统的知识:操作系统是建立在进程的概念之上,这些进程在内核中作业,

其中有一个非常特殊的进程,称为“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,

OOM killer被激活,检查当前谁占用内存最多然后将该进程杀掉。

一般Out of memory:Kill process or sacrifice child错会在当可用虚拟虚拟内存(包括交换空间)消耗到让整个操作系统面临风险时,会被触发。

在这种情况下,OOM Killer会选择“流氓进程”并杀死它。

解决方法:

虽然增加交换空间的方式可以缓解Java heap space异常,还是建议最好的方案就是升级系统内存,

让java应用有足够的内存可用,就不会出现这种问题。


13、说一说Redis的单线程模型

 Redis的网络IO和键值对读写是由一个线程来完成的,但Redis的其他功能,例如持久化、异步删除、集群数据同步等操作依赖于其他线程来执行。

单线程可以简化数据结构和算法的实现,并且可以避免线程切换和竞争造成的消耗。但要注意如果某个命令执行时间过长,会造成其他命令的阻塞。Redis采用了IO多路复用机制,这带给了Redis并发处理大量客户端请求的能力。

加分回答

Redis单线程实现为什么这么快

Redis 是基于内存的操作,CPU 不会成为 Redis 的瓶颈,单线程容易实现,不需要各种锁的性能消耗,平时采用单线程多进程集群方案


14、TCP协议和UDP协议有什么区别?

1.TCP面向连接,UDP无连接

2.TCP可靠传输,UDP尽力交付

3.TCP采用全双工模式传输,UDP单工、半双工、全双工模式均可

4.TCP最小报文20字节,UDP最小报文8字节

5.TCP面向字节流,UDP面向报文


15、说一说hash类型的底层数据结构

 Redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable(字典)。当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时,Redis会使用ziplist作为哈希的内部实现。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而 hashtable的读写时间复杂度为O(1)。

加分回答

    压缩列表是Redis为了节约内存而开发的,它是由一系列特殊编码的连续内存块组成的顺序型数据结构;hashtable(字典)主要涉及三个结构体:字典、哈希表、哈希表节点。每个哈希表节点保留一个键值对,一个哈希表由多个哈希表节点构成,字典是对哈希表的进一步封装。


16、说一说你对双亲委派模型的理解

双亲委派模型,其实就是一种类加载器的层次关系

1. 工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。

因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

在分析类加载的源码时,我们还会再一次细致的提及类加载的过程。

2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
避免了多份同样字节码的加载,内存是宝贵的,没必要保存相同的两份 Class 对象,例如 System.out.println() ,实际我们需要一个 System 的 Class 对象,并且只需要一份,如果不使用委托机制,而是自己加载自己的,那么类 A 打印的时候就会加载一份 System 字节码,类 B 打印的时候又会加载一份 System 字节码。而使用委托机制就可以有效的避免这个问题。


17、说说你对Spring Boot的理解

用于快速构建基于spring的应用程序。可以开箱即用,也可以按需改动。提供各种非功能特性。快速生成代码,没有xml配置,简化开发


18、Serializable接口为什么需要定义serialVersionUID常量

序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException


19、说一说常用的 Linux 命令

  • cd         切换目录
  • pwd         显示当前工作目录
  • touch     创建空文件
  • mkdir     创建一个新目录
    • -p    创建多级目录
  • cp        复制文件或者目录
    • -r    递归处理,将指定目录下的文件与子目录一并拷贝
  • mv        移动文件或目录、文件或目录改名
  • rm         删除文件
    • -r    同时删除该目录下的所有文件
    • -f    强制删除文件或目录
  • cat        显示文本文件内容
  • more        分页显示文本内容,可前后翻页,空格向后翻页,b向前翻页
  • head        查看文本开头部分,默认十行
    • -[num]    查看文本开头部分指定行数
  • tail    查看文本结尾部分,默认十行
    • -[num]    查看文本结尾部分指定行数
    • -f        循环滚动读取文件并动态显示在屏幕上,根据文件属性追踪    
    • -F        循环滚动读取文件并动态显示在屏幕上,文件文件名追踪
  • wc        统计文本行数,字数,字符数
    • -m    字符数
    • -w    文本字数
    • -l    文本行数
  • find    / -name 在文件系统的指定目录下查找指定的文件
  • grep        在指定的文件中查找指定内容的行
  • ln        建立链接文件
  • scp  远程传输文件

20、说一说线程的生命周期

线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。

  • 新建:就是刚使用new方法,new出来的线程;

  • 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;

  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;

  • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;

  • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

91科技

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值