2020春招 / 2021秋招阿里、腾讯、字节、快手、美团 JAVA 开发岗面试高频问题总结

2020春招 / 2021秋招阿里、腾讯、字节、快手、美团 JAVA 开发岗面试高频问题总结

1.项目相关

  • 介绍一下你简历上写的项目?自己主要做了什么?

  • 你觉得项目里给你最大的挑战是什么?遇到了什么问题?如何解决的?从中学到了什么?

项目里面会不断出现各种问题,比如数据量过大造成的内存溢出问题,如何让程序运行效率更高,如何证明我们的算法比别人的算法效率高,如何找到新的观点来支撑我们现有的理论,如何向导师和师兄进行沟通完成接下来的工作。

  • 项目的架构图能画一下不?

  • 觉得项目有哪些地方可以改进完善?(比如:可以加一个 redis 缓存把热点数据缓存起来)

  • 有没有遇到过内存泄漏的场景?

2.基础问题

2.1 进程和线程的区别?

a)进程是资源分配的最小单位,线程是任务执行的最小单位。

b)进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此 CPU 切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

c)线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

d)但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

2.2 进程的调度算法有哪些?(主要)

a)先来先去服务

b)时间片轮转法

c)短作业优先

d)多级反馈队列调度算法

e)优先级调度

2.3 常用 IO 模型?

关注消息通信机制:

a)同步:调用一个功能,在功能结果没有返回之前,一直等待结果返回。

b)异步:调用一个功能,调用立刻返回,但调用者不能立刻得到结果。调用者可以继续后续的操作,其结果一般通过状态,回调函数来通知调用者。

等待调用结果时的状态:

c)阻塞:调用一个函数,当调用结果返回之前,当前线程会被挂起,只有得到结果之后才会返回。

d)非阻塞:调用一个函数,不能立刻得到结果之前,调用不能阻塞当前线程。一个输入操作通常包括两个阶段:

  • 1.等待数据准备好
  • 2.从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

e)阻塞 IO 模型:应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。

f)非阻塞IO模型:进程发起 IO 系统调用后,内核返回一个错误码而不会被阻塞;应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成。如果内核缓冲区有数据,内核就会把数据返回进程。

g)IO 复用模型:使用 select 或者 poll 等待数据,可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后把数据从内核复制到进程中。(在多路复用 IO 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个 socket,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。)

h)信号驱动 IO 模型:当进程发起一个 IO 操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用 IO 读取数据。

i)异步 IO 模型:当进程发起一个 IO 操作,进程返回不阻塞,但也不能返回结果;内核把整个 IO 处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。

2.4 select、poll 和 epoll 的区别?epoll 的底层使用的数据结构。

select,poll 和 epoll 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。

select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。

select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制,poll 中的描述符是 pollfd 类型的数组;

poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。

如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。

select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。

当某个进程调用 epoll_create() 方法时,内核会创建一个 eventpoll 对象。

创建 epoll 对象后,可以用 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。

就绪列表:epoll 使用双向链表来实现就绪队列,是一种能够快速插入和删除的数据结构。索引结构:epoll 使用红黑树去监听并维护所有文件描述符。

epoll 的描述符事件有两种触发模式:LT(水平触发)和 ET(边沿触发)。

当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait()会再次通知进程。

和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。

边沿触发仅触发一次,水平触发会一直触发。

2.5 进程的通信方式有哪些?线程呢?

2.5.1 进程间的通信方式

a) 管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。

b) 有名管道(Names Pipes): 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。

c)消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺。

d) 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;(对于异常情况下的工作模式,就需要用「信号」的方式来通知进程,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。比如,Ctrl+C 产生 SIGINT 信号,表示终止该进程,Ctrl+Z 产生 SIGSTP,表示停止该进程,但还未结束)

e) 信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。(信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。)

f) 共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。(共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中)

h) 套接字(Sockets): 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

 int socket(int domain, int type, int protocal)

2.5.2 线程间的通信方式:

a) 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。

b) 信号量(Semphares):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。

c) 事件(Event):Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

2.6 fork 函数的作用?

在 Linux 中 fork 函数是非常重要的函数,它的作用是从已经存在的进程中创建一个子进程,而原进程称为父进程。

调用 fork(),当控制转移到内核中的 fork 代码后,内核开始做:

  1. 分配新的内存块和内核数据结构给子进程。

  2. 将父进程部分数据结构内容拷贝至子进程。

  3. 将子进程添加到系统进程列表。

  4. fork返回开始调度器,调度。

特点:

1)调用一次,返回两次并发执行

2)相同但是独立的地址空间

3)fork 的返回值:fock 函数调用一次却返回两次;向父进程返回子进程的 ID,向子进程中返回 0,

4)fork 的子进程返回为 0;

5)父进程返回的是子进程的 pid。

fork 调用失败的原因

1)系统中有太多进程。

2)实际用户的进程数超过限制。

2.7 协程的概念?

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

对操作系统而言,线程是最小的执行单元,进程是最小的资源管理单元。无论是进程还是线程,都是由操作系统所管理的。

协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。

协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程它进程和进程不是一个维度的。

一个进程可以包含多个线程,一个线程可以包含多个协程。

一个线程内的多个协程虽然可以切换,但是多个协程是串行执行的,只能在一个线程内运行,没法利用 CPU 多核能力。

协程与进程一样,切换是存在上下文切换问题的。

2.8. linux 进程和线程?

进程通过 fork()创建

线程通过 pthread_create() 函数创建

2.9 通过进程id查看占用的端口,通过端口号查看占用的进程 id?

通过进程id查看占用的端口:

netstat -nap | grep 进程id

通过端口号查看占用的进程id :

netstat -nap | grep 端口号
2.10 如何查看占用内存比较多的进程?
ps aux | sort -k4nr | head -N

head :-N可以指定显示的行数,默认显示10行。

ps :a---指代所有的进程,u---userid---执行该进程的用户id,x---指代显示所有程序,不以终端机来区分。ps -aux的输出格式如下:

sort -k4nr 中:k 代表从根据哪一个关键词排序,后面的数字 4 表示按照第四列排序;n 指代 numberic sort,根据其数值排序;r 指代 reverse,这里是指反向比较结果,输出时默认从小到大,反向后从大到小。%MEM 在第 4 个位置,-k4 按照内存占用排序。%CPU 在第三个位置,-k3 表示按照cpu占用率排序。

2.11 僵尸进程产生的原因?

僵尸进程是指它的父进程没有等待(调用 wait/waitpid)。如果子进程先结束而父进程后结束,即子进程结束后,父进程还在继续运行但是并未调用 wait/waitpid 那子进程就会成为僵尸进程。但如果子进程后结束,即父进程先结束了,但没有调用 wait/waitpid 来等待子进程的结束, 此时子进程还在运行,父进程已经结束。那么并不会产生僵尸进程。应为每个进程结束时, 系统都会扫描当前系统中运行的所有进程,看看有没有哪个进程时刚刚结束的这个进程的子 进程,如果有就有 init 来接管它,成为它的父进程。

进程设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时间获取。要在当前 进程中生成一个子进程,一般需要调用 fork 这个系统调用,fork 这个函数的特别之处在于一次调用,两次返回,一次返回到父进程中,一次返回到子进程中,可以通过返回值来判断其 返回点。如果子进程先于父进程退出, 同时父进程又没有调用 wait/waitpid,则该子进程将成为僵尸进程。

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存。但是 仍然保留了一些信息(如进程号 pid 退出状态 运行时间等)。这些保留的信息直到进程通过调用 wait/waitpid 时才会释放。这样就导致了一个问题,如果没有调用 wait/waitpid 的话,那 么保留的信息就不会释放。比如进程号就会被一直占用了。但系统所能使用的进程号的有限 的,如果产生大量的僵尸进程,将导致系统没有可用的进程号而导致系统不能创建进程。所 以我们应该避免僵尸进程。

如果进程不调用 wait / waitpid 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用 的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

2.13 孤儿进程产生的原因?

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤 儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程是没有父进程的进程,管理孤儿进程这个重任就落到了 init 进程身上,因此孤儿进程并 不会有什么危害。

2.14 讲一下虚拟内存。虚拟内存和物理内存的关系是什么?

虚拟内存使得应用程序认为它拥有一个连续的地址空间,而实际上,它通常是被分隔成多个物理内 存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。

虚拟内存可以让程序可以拥有超过系统物理内存大小的可用内存空间。虚拟内存让每个进程拥有一 片连续完整的内存空间。

局部性原理表现在以下两个方面:

1)时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。

2)空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问。

操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块, 每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都 必须在物理内存中。当程序引用到不在物理内存中的页时,会将缺失的部分从磁盘装入物理内存。

页面置换算法:

OPT 页面置换算法(最佳页面置换算法):所选择的被换出的页面将是最长时间内不再被访问, 通常可以保证获得最低的缺页率。

FIFOFirst In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。

LRULeast Currently Used)页面置换算法(最近最久未使用页面置换算法):将最近最久未使用的页面换出。需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到 链表表头。这样就能保证链表表尾的页面是最近最久未访问的。力扣-实现LRU

LFULeast Frequently Used)页面置换算法(最少使用页面置换算法):该置换算法选择在之前时期使用最少的页面作为淘汰页。力扣-实现LFU

2.15 分段和分页讲一下?以及对应的场景?

操作系统的内存管理机制了解吗?内存管理有哪几种方式?

  1. 块式管理 : 将内存分为几个固定大小的块,每个块中只包含一个进程。

  2. 页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址 和物理地址。

  3. 段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。 段式管理把主存分为一段段的,最重要的是段是有实际意义的,每个段定义了一组逻辑信息。 段式管理通过段表对应逻辑地址和物理地址。例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。

  4. 段页式管理:段页式管理机制结合了段式管理和页式管理的优点。段页式管理机制就是 把主存先分成若干段,每个段又分成若干页。

分段和分页:

  1. 共同点

分页机制和分段机制都是为了提高内存利用率,较少内存碎片。
页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中 的内存是连续的。

  1. 区别

页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的 程序。
分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中 可以体现为代码段,数据段,能够更好满足用户的需要。

2.16 讲一下用户态和内核态?所有的系统调用都会进入到内核态吗?

操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序。根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:

  1. 用户态(user mode) : 用户态运行的进程或可以直接读取用户程序的数据。
  2. 内核态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

m78探索者

谢谢

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

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

打赏作者

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

抵扣说明:

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

余额充值