JAVA开发面试题_网络_操作系统_JAVA基础_JVM虚拟机

 

目录

 

网络篇

OSI七层模型与TCP/IP 五层模型

常见应用层协议和运输层、网络层协议,以及硬件如路由器之类在哪一层

TCP与UDP区别和应用场景,基于TCP的协议有哪些,基于UDP的有哪些

TCP可靠传输的保证,拥塞控制目的和过程

TCP粘包现象原因和解决方法

TCP三次握手过程以及每次握手后的状态改变,为什么三次? 为什么两次不行?

TCP四次挥手过程以及状态改变,为什么四次?CLOSE-WAIT和TIME-WAIT存在的意义?如何查看TIME-WAIT状态的链接数量?为什么会TIME-WAIT过多?解决方法是怎样的?

浏览器输入URL并回车的过程以及相关协议,DNS查询过程。

HTTP1.0、1.1、2.0之间的区别

HTTP与HTTPS之间的区别,HTTPS链接建立的过程,了解对称加密算法和非对称加密算法不?​ HTTP与HTTPS之间的区别

HTTP请求有哪些。get和Post区别

HTTP常见响应状态码,从1xx到5xx

重定向和转发区别

cookie和session区别

操作系统

进程和线程的区别

协程

进程间通信方式IPC

用户态和核心态

操作系统分配的进程空间是怎样的?线程能共享哪些?

操作系统内存管理方式,分页分段以及段页式的优缺点

页面置换算法有哪些,FIFO为什么不好?如何改进?LRU思想,手写LRU

死锁条件,解决方式

Java基础篇

Java面向对象特性介绍、与C++区别

多态实现原理

抽象类和接口区别,以及各自的使用场景

泛型以及泛型擦除。List类型的list,可以加入无继承关系的B类型对象吗?如何加入?

Java异常体系

反射原理以及使用场景

ThreadLocal原理,如何使用?

ThreadLocal内存泄漏的场景

static关键字和final关键字使用情况,一个类不能被继承,除了final关键字之外,还有什么方法(从构造函数考虑)?

序列化和反序列化。反序列化失败的场景

ArrayList和LinkedList的区别和底层实现?如何实现线程安全?

List遍历时如何删除元素?fail—fast是什么?fail—safe是什么?

详细介绍HashMap

JDK1.7与1.8的区别

HashMap如何实现线程安全?ConcurrentHashMap的底层实现?JDK1.7与JDK1.8的区别

linux指令知道哪些?

JVM虚拟机

JVM运行时内存划分?

堆内存分配策略

Full GC触发条件

如何判断对象是否存活?回收对象的两次标记过程。

垃圾回收算法

垃圾收集器

CMS收集器

G1收集器

创建一个对象的步骤

详细介绍类加载过程

双亲委派机制,使用这个机制的好处?如何破坏?

了解下tomcat的类加载机制


网络篇

OSI七层模型与TCP/IP 五层模型

  • ​ OSI七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
  • ​ TCP/IP五层:物理层、数据链路层、网络层、传输层、应用层

常见应用层协议和运输层、网络层协议,以及硬件如路由器之类在哪一层

  •  应用层:HTTP、SMTP、DNS、FTP
  • ​ 传输层:TCP 、UDP
  • ​ 网络层:ICMP 、IP、路由器、防火墙
  • ​ 数据链路层:网卡、网桥、交换机
  • ​ 物理层:中继器、集线器

TCP与UDP区别和应用场景,基于TCP的协议有哪些,基于UDP的有哪些

类型 特点 性能 应用过场景 首部字节

TCP 面向连接、可靠、字节流 传输效率慢、所需资源多 文件、邮件传输 20-60

UDP 无连接、不可靠、数据报文段 传输效率快、所需资源少 语音、视频、直播 8个字节

​ 基于TCP的协议:HTTP、FTP、SMTP

​ 基于UDP的协议:RIP、DNS、SNMP

TCP可靠传输的保证,拥塞控制目的和过程

 TCP通过:应用数据分割、对数据包进行编号、校验和、流量控制、拥塞控制、ARP协议、超时重传等措施保证数据的可靠传输;

​ 拥塞控制目的:为了防止过多的数据注入到网络中,避免网络中的路由器、链路过载

​ 拥塞控制过程:TCP发送发将维护一个拥塞窗口的状态变量,该变量随着网络拥塞程度动态变化,通过慢开始、拥塞避免等算法减少网络拥塞的发生。

TCP粘包现象原因和解决方法

 TCP粘包是指:发送方发送的若干包数据到接收方接收时粘成一包

​ 发送方原因:

​ TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:

​ 只有上一个分组得到确认,才会发送下一个分组

​ 收集多个小分组,在一个确认到来时一起发送

​ Nagle算法造成了发送方可能会出现粘包问题

​ 接收方原因:

​ TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上, TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果 TCP 接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程 序就有可能读取到多个首尾相接粘到一起的包。

解决粘包问题:

​ 最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪,通过使用某种方案给出边界,例如:

发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。

包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。

包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。

TCP三次握手过程以及每次握手后的状态改变,为什么三次? 为什么两次不行?

三次握手过程:

​ 客户端——发送带有SYN标志的数据包——服务端 一次握手 Client进入syn_sent状态

​ 服务端——发送带有SYN/ACK标志的数据包——客户端 二次握手 服务端进入syn_rcvd

​ 客户端——发送带有ACK标志的数据包——服务端 三次握手 连接就进入Established状态

​ 为什么三次:

​ 主要是为了建立可靠的通信信道,保证客户端与服务端同时具备发送、接收数据的能力

​ 为什么两次不行

​ 1、防止已失效的请求报文又传送到了服务端,建立了多余的链接,浪费资源

​ 2、 两次握手只能保证单向连接是畅通的。(为了实现可靠数据传输, TCP 协议的通信双方, 都必须维 护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方 相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤;如果只是两次握手, 至多只 有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认)

TCP四次挥手过程以及状态改变,为什么四次?CLOSE-WAIT和TIME-WAIT存在的意义?如何查看TIME-WAIT状态的链接数量?为什么会TIME-WAIT过多?解决方法是怎样的?

四次挥手过程:

​ 客户端——发送带有FIN标志的数据包——服务端,关闭与服务端的连接 ,客户端进入FIN-WAIT-1状态

​ 服务端收到这个 FIN,它发回⼀ 个 ACK,确认序号为收到的序号加1,服务端就进入了CLOSE-WAIT状态

​ 服务端——发送⼀个FIN数据包——客户端,关闭与客户端的连接,客户端就进入FIN-WAIT-2状态

​ 客户端收到这个 FIN,发回 ACK 报⽂确认,并将确认序号设置为收到序号加1,TIME-WAIT状态

为什么四次:

​ 因为需要确保客户端与服务端的数据能够完成传输。

CLOSE-WAIT:

​ 这种状态的含义其实是表示在等待关闭

TIME-WAIT

​ 为了解决网络的丢包和网络不稳定所带来的其他问题,确保连接方能在时间范围内,关闭自己的连接

如何查看TIME-WAIT状态的链接数量?

​ netstat -an |grep TIME_WAIT|wc -l 查看连接数等待time_wait状态连接数

为什么会TIME-WAIT过多?解决方法是怎样的?

​ 可能原因: 高并发短连接的TCP服务器上,当服务器处理完请求后立刻按照主动正常关闭连接

​ 解决:负载均衡服务器;Web服务器首先关闭来自负载均衡服务器的连接

浏览器输入URL并回车的过程以及相关协议,DNS查询过程。

过程:DNS解析、TCP连接、发送HTTP请求、服务器处理请求并返回HTTP报文、浏览器渲染、结束

过程 使用的协议

1、浏览器查找域名DNS的IP地址

DNS查找过程(浏览器缓存、路由器缓存、DNS缓存) DNS:获取域名对应的ip

2、根据ip建立TCP连接 TCP:与服务器建立连接

3、浏览器向服务器发送HTTP请求 HTTP:发送请求

4、服务器响应HTTP响应 HTTP

5、浏览器进行渲染

HTTP1.0、1.1、2.0之间的区别

HTTP1.0:默认使用Connection:cloose,浏览器每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。

​ HTTP1.1:默认使用Connection:keep-alive(长连接),避免了连接建立和释放的开销;通过Content-Length字段来判断当前请求的数据是否已经全部接受。不允许同时存在两个并行的响应。

HTTP2.0:引入二进制数据帧和流的概念,其中帧对数据进行顺序标识;因为有了序列,服务器可以并行的传输数据。

http1.0和http1.1的主要区别如下:

  •  1、缓存处理:1.1添加更多的缓存控制策略(如:Entity tag,If-Match)
  • ​ 2、网络连接的优化:1.1支持断点续传
  • ​ 3、错误状态码的增多:1.1新增了24个错误状态响应码,丰富的错误码更加明确各个状态
  • ​ 4、Host头处理:支持Host头域,不在以IP为请求方标志
  • ​ 5、长连接:减少了建立和关闭连接的消耗和延迟。

​ http1.1和http2.0的主要区别:

  •  1、新的传输格式:2.0使用二进制格式,1.0依然使用基于文本格式
  • ​ 2、多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成 正常的请求)
  • ​ 3、header压缩:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的 hearder大小
  • ​ 4、服务端推送:同google的SPDUY(1.0的一种升级)一样
  •  

HTTP与HTTPS之间的区别,HTTPS链接建立的过程,了解对称加密算法和非对称加密算法不?​ HTTP与HTTPS之间的区别

HTTP

 HTTPS

默认端口80

HTTPS默认使用端口443

明文传输、数据未加密、安全性差

传输过程ssl加密、安全性较好

响应速度快、消耗资源少

响应速度较慢、消耗资源多、需要用到CA证书

​ HTTPS链接建立的过程

  • ​ 1.首先客户端先给服务器发送一个请求
  • ​ 2.服务器发送一个SSL证书给客户端,内容包括:证书的发布机构、有效期、所有者、签名以及公钥
  • ​ 3.客户端对发来的公钥进行真伪校验,校验为真则使用公钥对对称加密算法以及对称密钥进行加密
  • ​ 4.服务器端使用私钥进行解密并使用对称密钥加密确认信息发送给客户端
  • ​ 5.随后客户端和服务端就使用对称密钥进行信息传输

对称加密算法

​ 双方持有相同的密钥,且加密速度快,典型对称加密算法:DES、AES

非对称加密算法

​ 密钥成对出现(私钥、公钥),私钥只有自己知道,不在网络中传输;而公钥可以公开。相比对称加密速度较慢,典型的非对称加密算法有:RSA、DSA

HTTP请求有哪些。get和Post区别

方法 描述

  • GET 向特定资源发送请求,查询数据,并返回实体
  • POST 向指定资源提交数据进行处理请求,可能会导致新的资源建立、已有资源修改
  • PUT 向服务器上传新的内容
  • HEAD 类似GET请求,返回的响应中没有具体的内容,用于获取报头
  • DELETE 请求服务器删除指定标识的资源
  • OPTIONS 可以用来向服务器发送请求来测试服务器的功能性
  • TRACE 回显服务器收到的请求,用于测试或诊断
  • CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器

get和Post区别:

方面 GET POST
可见性 数据在URL中对所有人可见 数据不会显示在URL中
安全性

与post相比,get的安全性较差,因为所

发送的数据是URL的一部分

安全,因为参数不会被保存在浏览器历史或web服务器日志中
数据长度 受限制,最长2kb 无限制
编码类型 application/x-www-form-urlencoded multipart/form-data
缓存 能被缓存 不能被缓存

HTTP常见响应状态码,从1xx到5xx

  • ​ 100:Continue --- 继续。客户端应继续其请求。
  • ​ 200:OK --- 请求成功。一般用于GET与POST请求。
  • ​ 301:Moved Permanently --- 永久重定向。
  • ​ 302:Found --- 暂时重定向。
  • ​ 400:Bad Request --- 客户端请求的语法错误,服务器无法理解。
  • ​ 403:Forbideen --- 服务器理解请求客户端的请求,但是拒绝执行此请求。
  • ​ 404:Not Found --- 服务器无法根据客户端的请求找到资源(网页)。
  • ​ 500:Internal Server Error --- 服务器内部错误,无法完成请求。
  • ​ 502:Bad Gateway --- 作为网关或者代理服务器尝试执行请求时,从远程服务器接收到了无效的响应。
  • 重定向和转发区别

  • 重定向:redirect

    转发:forward

    ​ 地址栏发生变化

    转发地址栏路径不变

    重定向可以访问其他站点(服务器)的资源

    转发只能访问当前服务器下的资源

    重定向是两次请求。不能使用request对象来共享数据

    转发是一次请求,可以使用request对象共享数据

    cookie和session区别

  •  Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但两者有所区别:
  • ​ Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
  • ​ cookie不是很安全,别人可以分析存放在本地的COOKIE并进行欺骗,考虑到安全应当使用session。
  • ​ Cookie ⼀般⽤来保存⽤户信息,Session 的主要作⽤就是通过服务端记录⽤户的状态
  •  

操作系统

进程和线程的区别

 进程:是资源分配的最小单位,是程序的执行过程,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,但每个线程又有属于自己的本地方法栈、虚拟机栈、程序计数器

​ 线程:是任务调度和执行的最小单位,线程间可能存在相互影响,执行开销较小,不利于资源的管理和保护,线程间是共享进程中的资源的

协程

是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。

进程间通信方式IPC

匿名管道pipe

​ 匿名管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。

命名管道FIFO

不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。

信号

 信号是一种比较复杂的通信方式,信号产生的条件:按键、硬件异常、进程调用kill函数将信号发送给另一个进程、用户调用kill命令将信号发送给其他进程,信号传递的消息比较少,主要用于通知接收进程某个时间已经发生。

消息队列

​ 消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。消息队列起信箱作用,到了就挂在那里,需要的时候去取。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。与命名管道相比:消息队列的优势在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。而且,每个数据块被认为含有一个类型,接收进程可以独立地接收含有不同类型值的数据块。

优点:

​ A. 我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。

​ B. 我们可以用一些方法来提前查看紧急消息。

缺点:

​ A. 与管道一样,每个数据块有一个最大长度的限制。

​ B. 系统中所有队列所包含的全部数据块的总长度也有一个上限。

共享内存(share memory)

使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。

为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。

由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。

信号量(Semaphores)

信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。

套接字(Sockets)

此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。

用户态和核心态

 在计算机系统中,分两种程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态——用户态、核心态

用户态:只能受限的访问内存,运行所有的应用程序

核心态:运行操作系统程序,cpu可以访问内存的所有数据,包括外围设备

为什么要有用户态和内核态:

​ 由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络

用户态切换到内核态的3种方式:

 a. 系统调用

​ 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

 b. 异常

​ 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

c. 外围设备的中断

​ 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

​ 这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

操作系统分配的进程空间是怎样的?线程能共享哪些?

 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。

​ 堆区(heap)— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。

​ 静态区(static)—存放全局变量和静态变量的存储

​ 代码区(text)—存放函数体的二进制代码。

​ 线程共享堆区、静态区

操作系统内存管理方式,分页分段以及段页式的优缺点

存管理方式:块式管理、页式管理、段式管理、段页式管理

分段管理:

​ 在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)

分页管理:

​ 在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的页框,程序加载时,可以将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)

段页式管理:

​ 段⻚式管理机制结合了段式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲ 段,每个段⼜分成若⼲⻚,也就是说 段⻚式管理机制 中段与段之间以及段的内部的都是离散的。

页面置换算法有哪些,FIFO为什么不好?如何改进?LRU思想,手写LRU

置换算法:先进先出FIFO、最近最久未使用LRU、最佳置换算法OPT

先进先出FIFO:

​ 原理:把内存中驻留时间最久的页面置换算法予以淘汰

​ 优点:实现简单、直观

​ 缺点:没有考虑到实际的页面使用频率,性能差、与通常页面使用的规则不符合,实际应用较少

​ 改进:给每个页面增加一个R位,每次先从链表头开始查找,如果R置位,清除R位并且把该页面节点放 到链表结尾;如果R是0,那么就是又老又没用到,替换掉。

最近最久未使用LRU:

​ 原理:选择最近且最久未使用的页面进行淘汰

​ 优点:考虑到了程序访问的时间局部性,有较好的性能,实际应用也比较多

​ 缺点:实现需要比较多的硬件支持,会增加一些硬件成本

public class LRUCache {
    private LinkedHashMap<Integer,Integer> cache;
    private int capacity;   //容量大小
 
    /**
     *初始化构造函数
     * @param capacity
     */
    public LRUCache(int capacity) {
        cache = new LinkedHashMap<>(capacity);
        this.capacity = capacity;
    }
 
    public int get(int key) {
        //缓存中不存在此key,直接返回
        if(!cache.containsKey(key)) {
            return -1;
        }
 
        int res = cache.get(key);
        cache.remove(key);   //先从链表中删除
        cache.put(key,res);  //再把该节点放到链表末尾处
        return res;
    }
 
    public void put(int key,int value) {
        if(cache.containsKey(key)) {
            cache.remove(key); //已经存在,在当前链表移除
        }
        if(capacity == cache.size()) {
            //cache已满,删除链表头位置
            Set<Integer> keySet = cache.keySet();
            Iterator<Integer> iterator = keySet.iterator();
            cache.remove(iterator.next());
        }
        cache.put(key,value);  //插入到链表末尾
    }
}
class LRUCache {
    private Map<Integer, Integer> map;
    private int capacity;
 
    /**
     *初始化构造函数
     * @param capacity
     */
    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > capacity;  // 容量大于capacity 时就删除
            }
        };
    }
    public int get(int key) {
        //返回key对应的value值,若不存在,返回-1
        return map.getOrDefault(key, -1);
    }
 
    public void put(int key, int value) {
        map.put(key, value);
    }
}

最佳置换算法OPT:

​ 原理:每次选择当前物理块中的页面在未来长时间不被访问的或未来不再使用的页面进行淘汰

​ 优点:具有较好的性能,可以保证获得最低的缺页率

​ 缺点:过于理想化,但是实际上无法实现(没办法预知未来的页面)

死锁条件,解决方式

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象;

​ 死锁的条件:

​ 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;

​ 请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源

​ 非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放

​ 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源

解决方法

破坏死锁的任意一条件

​ 资源一次性分配,从而剥夺请求和保持条件

​ 可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件

​ 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件

Java基础篇

Java面向对象特性介绍、与C++区别

特性:封装、继承、多态

​ 封装:对抽象的事物抽象化成一个对象,并对其对象的属性私有化,同时提供一些能被外界访问属性的方法,这样一个对象便有存在的意义了;

​ 继承:在已存在类的基础上,建立新类并对其增加新的数据域或功能,同时该类可以复用父类的属性与功能,这种思路可以称为继承;通过使用继承能够方便地复用旧代码,减少不必要的代码量;

​ 多态:指程序中的某个引用变量,它所指向的具体类型以及该引用变量发出的方法调用,在编程时不能确定,要在程序运行并使用时由机器自己判别确定;实现多态的方式有两种方式,可以通过继承(多个⼦类对同⼀⽅法的重写)、也可以通过接⼝(实现接⼝并覆盖接⼝中同⼀⽅法)

Java与C++区别:

​ 相同点:都是面向对象语言,并且都支持封装、继承、多态

​ 不同点:c++支持多继承,并且有指针的概念,由程序员自己管理内存;Java是单继承,可以用接口实现多继承,Java 不提供指针来直接访问内存,程序内存更加安全,并且Java有JVM⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存

多态实现原理

多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。

静态绑定与动态绑定:

​ JVM 的方法调用指令有五个,分别是:

​ invokestatic:调用静态方法;

​ invokespecial:调用实例构造器<init>方法、私有方法和父类方法;</init>

​ invokevirtual:调用虚方法;

​ invokeinterface:调用接口方法,运行时确定具体实现;

​ invokedynamic:运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言。

​ invokestatic 和 invokespecial 用于静态绑定

​ invokevirtual 和 invokeinterface 用于动态绑定

​ 可以看出,动态绑定主要应用于虚方法接口方法

​ 虚方法的方法调用与方法实现的关联(也就是分派)有两种,一种是在编译期确定,被称为静态分派,比如方法的重载;一种是在运行时确定,被称为动态分派,比如方法的覆盖(重写)。对象方法基本上都是虚方法。

多态的实现

​ 虚拟机栈中会存放当前方法调用的栈帧(局部变量表、操作栈、动态连接 、返回地址)。多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。

以 invokevirtual 指令为例,在执行时,大致可以分为以下几步:

先从操作栈中找到对象的实际类型 class;

找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ;

如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作;

如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ;

可以看到,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。

抽象类和接口区别,以及各自的使用场景

抽象类:包含抽象方法的类,即使用abstract修饰的类;不能使用final修饰,final修饰的类不能被继承;抽象类不能被实例化,只能被继承

接口:接口是一个抽象类型,是抽象方法的集合,接口以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法;接口只能继承接口,不能继承类,接口支持多继承;接口中的定义的成员变量,默认是public static final修饰的静态常量;接口中定义的方法,默认是public abstract修饰的抽象方法

相同点:

  • ​ ① 抽象类和接口都不能被实例化
  • ​ ② 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法

不同点:

  • ​ ① 抽象类有构造方法,接口没有构造方法
  • ​ ③抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以)
  • ​ ③ 抽象类只能单继承,接口可以多继承
  • ​ ④ 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量

抽象类的使用场景:

​ 既想约束子类具有共同的行为(但不再乎其如何实现),又想拥有缺省的方法,又能拥有实例变量

接口的应用场景:

​ 约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现;实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系

泛型以及泛型擦除。List类型的list,可以加入无继承关系的B类型对象吗?如何加入?

泛型

​ 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

泛型擦除

​ Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

​ 如在代码中定义的 List<object>和 List<string>等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。</string></object>

Java异常体系

Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 Error 和 Exception

Error 

​ 是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。

Exception 包含:RuntimeException 、CheckedException

  • (1) RuntimeException:运行时异常

​ 如 NullPointerException 、 ClassCastException ;

​ RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类,这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

(2) CheckedException:受检异常

​ 如 I/O 错误导致的 IOException、SQLException;

​ CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类

异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:

​ ①试图在文件尾部读取数据

​ ②试图打开一个错误格式的 URL

​ ③试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在

反射原理以及使用场景

Java反射

是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为 Java 语言的反射机制。

反射原理

​ 反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类

如何得到Class的实例

  • 1.类名.class(就是一份字节码)
  • 2.Class.forName(String className);根据一个类的全限定名来构建Class对象
  • 3.每一个对象多有getClass()方法:obj.getClass();返回对象的真实类型

使用场景

逆向代码 ,例如反编译;

动态生成类框架,如Spring:xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:

  • 1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;
  • 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
  • 3)使用反射机制,根据这个字符串获得某个类的Class实例;
  • 4)动态配置实例的属性

ThreadLocal原理,如何使用?

ThreadLocal简介

​ 通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢? JDK中提供的 ThreadLocal 类正是为了解决这样的问题。

原理

​ 首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。

​ 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
get方法:
public T get() {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null)  
        return (T)map.get(this);  
 
    // 如果不存在,则创建它  
    T value = initialValue();  
    createMap(t, value);  
    return value;  
}
createMap方法:
void createMap(Thread t, T firstValue) {  
    t.threadLocals = new ThreadLocalMap(this, firstValue);  
}
ThreadLocalMap是个静态的内部类

如何使用

1)存储用户Session
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}
​ 2)解决线程安全的问题
public class DateUtil {
    //SimpleDateFormat不是线程安全的,所以每个线程都要有⾃⼰独⽴的副本
    private static ThreadLocal<SimpleDateFormat> format1 = new                     ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
 
    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

ThreadLocal内存泄漏的场景

​ 实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

​ 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。

​ ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

​ 因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。

static关键字和final关键字使用情况,一个类不能被继承,除了final关键字之外,还有什么方法(从构造函数考虑)?

static:可以修饰属性、方法

static修饰属性

​ 所有对象共享一份,一个对象对其修改,其他的调用也会受到影响,类级别;随着类的加载而加载(只加载一次),先于对象的创建;可以使用类名直接调用。

static修饰方法

​ 随着类的加载而加载;可以使用类名直接调用;静态方法中,只能调用静态的成员;非静态的方法中,可以调用静态和非静态的成员;在静态方法中,不会出现this。

final:关键字主要⽤在三个地⽅:变量、⽅法、类。

​ final修饰变量

​ 对于⼀个 final 变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。

​ final修饰方法

​ 把⽅法锁定,以防任何继承类修改它的含义(重写);类中所有的 private ⽅法都隐式地指定为 final。

​ final修饰类

​ final 修饰类时,表明这个类不能被继承。final 类中的所有成员⽅法都会被隐式地指定为 final ⽅法。

序列化和反序列化。反序列化失败的场景

​ 序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。

序列化:将java对象转化为字节序列的过程。

反序列化:将字节序列转化为java对象的过程。

优点

​ a、实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里)

​ b、利用序列化实现远程通信,即在网络上传送对象的字节序列。

反序列化失败的场景

​ 序列化ID:serialVersionUID不一致的时候,导致反序列化失败

ArrayList和LinkedList的区别和底层实现?如何实现线程安全?

ArrayList

​ 底层基于数组实现,支持对元素进行快速随机访问,支持元素重复;默认初始大小为10,当数组容量不够时,会触发扩容机制(扩大到当前的1.5倍),需要将原来数组的数据复制到新的数组中;当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

LinkedList

​ 底层基于双向链表实现,适合数据的动态插入和删除;内部提供了 List 接口中没有定义的方法,用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

ArrayList与LinkedList区别

​ 都是线程不安全的,ArrayList 适用于查找的场景,LinkedList 适用于 增加、删除多的场景

线程安全

​ 可以使用原生的Vector,或者是Collections.synchronizedList(List list)函数返回一个线程安全的ArrayList集合,或者使用concurrent并发包下的CopyOnWriteArrayList的。

​ ①、Vector: 底层通过synchronize修饰保证线程安全,效率较差

​ ② 、Collections.synchronizedList(List list):

//使用Collections.synchronizedList(List list)方法实现线程安全

List<?> list=Collections.synchronizedList(new ArrayList<>());

​ ③、CopyOnWriteArrayList:写时加锁,使用了一种叫写时复制的方法;读操作是可以不用加锁的

List遍历时如何删除元素?fail—fast是什么?fail—safe是什么?

①、普通for循环遍历List删除指定元素
for(int i=0; i < list.size(); i++){
   if(list.get(i) == 5)
       list.remove(i);
}
② 、迭代遍历,用list.remove(i)方法删除元素
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
    Integer value = it.next();
    if(value == 5){
        list.remove(value);
    }
}
③、foreach遍历List删除元素
for(Integer i:list){
    if(i==3) list.remove(i);
}

fail—fast:快速失败

​ 当异常产生时,直接抛出异常,程序终止;

​ fail-fast只要是体现在当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构被改变的话,就会抛出异常ConcurrentModificationException,防止继续遍历。这就是所谓的快速失败机制。这里要注意的这里说的结构被改变,是例如插入和删除这种操作,只是改变集合里的值的话并不会抛出异常。

fail—safe:安全失败

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。

缺点:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

详细介绍HashMap

角度:数据结构+扩容情况+put查找的详细过程+哈希函数+容量为什么始终都是2^N,JDK1.7与1.8的区别。

数据结构

​ HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据

扩容情况

​ 默认的负载因子是0.75,表示的是,如果数组中已经存储的元素个数大于数组长度的75%,将会引发扩容操作。

  • ​ 【1】创建一个长度为原来数组长度两倍的新数组。
  • ​ 【2】重新对原数组中的Entry对象进行哈希运算,以确定他们各自在新数组中的新位置。

put操作步骤

  •  1、判断数组是否为空,为空进行初始化;
  • ​ 2、不为空,则计算 key 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
  • ​ 3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
  • ​ 4、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据;
  • ​ 5、若不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
  • ​ 6、若不是红黑树,创建普通Node加入链表中;判断链表长度是否大于 8,大于则将链表转换为红黑树;
  • ​ 7、插入完成之后判断当前节点数是否大于阈值,若大于,则扩容为原数组的二倍

哈希函数

​ hash函数是先拿到 key 的hashcode,是一个32位的值,然后让hashcode的高16位和低16位进行异或操作。该函数也称为扰动函数,做到尽可能降低hash碰撞。

容量为什么始终都是2^N

​ 为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。

JDK1.7与1.8的区别

JDK1.7 HashMap

​ 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。HashMap 通过 key 的hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

JDK1.8 HashMap

​ HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据;当链表⻓度⼤于阈值(默认为 8),数组的⻓度大于 64时,将链表转化为红⿊树,以减少搜索时间

HashMap如何实现线程安全?ConcurrentHashMap的底层实现?JDK1.7与JDK1.8的区别

可以通过ConcurrentHashMap 和 Hashtable来实现线程安全;Hashtable 是原始API类,通过synchronize同步修饰,效率低下;ConcurrentHashMap 通过分段锁实现,效率较比Hashtable要好;

ConcurrentHashMap的底层实现:

​ JDK1.7的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现;采用 分段锁(Sagment) 对整个桶数组进⾏了分割分段(Segment),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。

​ JDK1.8的 ConcurrentHashMap 采⽤的数据结构跟HashMap1.8的结构⼀样,数组+链表/红⿊⼆叉树;摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,通过并发控制 synchronized CAS来操作保证线程的安全。

linux指令知道哪些?

文件管理:ls、cd、touch创建普通文件、rm删除、mkdir新建目录、mv移动、cp拷贝、chmod修改权限

进程管理:ps显示进程信息、kill杀死进程

系统管理:top、free显示系统运行信息、vmstat输出各资源使用情况

网络通讯:ping测试网络连通性、netstat显示网络相关信息

JVM虚拟机

JVM运行时内存划分?

JVM运行时数据区域:堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器

Heap(堆)

​ 对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,也是垃圾回收(GC)的主要区域;

​ 堆细分:新生代、老年代,对于新生代又分为:Eden区和Surviver1和Surviver2区;

方法区

​ 对于JVM的方法区也可以称之为永久区,它储存的是已经被java虚拟机加载的类信息、常量、静态变量;Jdk1.8以后取消了方法区这个概念,称之为元空间(MetaSpace);

虚拟机栈

​ 虚拟机栈是线程私有的,他的生命周期和线程的生命周期是一致的。里面装的是一个一个的栈帧,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放(局部变量表、操作数栈 、动态链接 、返回地址);在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

  • 局部变量表:局部变量表是一组变量值存储空间,用来存放方法参数、方法内部定义的局部变量。局部变量表的容量是以变量槽(variable slot)为最小的单位。Java虚拟机没有明确规定一个slot所占的空间大小。只是导向性的说了每一个slot能存放8中基本数据类型中的一种(long 和double这种64位的需要两个slot);
  • 操作数栈:是用来记录一个方法在执行的过程中,字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。
  • 动态链接:因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会在运行期间转化为直接引用,称为动态链接。
  • 返回地址(returnAddress):类型(指向了一条字节码指令的地址)

本地方法栈

​ 本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。

PC程序计数器

​ PC,指的是存放下一条指令的位置的这么一个区域。它是一块较小的内存空间,且是线程私有的。由于线程的切换,CPU在执行的过程中,一个线程执行完了,接下来CPU切换到另一个线程去执行,另外一个线程执行完再切回到之前的线程,这时需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的PC。

堆内存分配策略

对象优先分配在Eden区,如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。而那些无需回收的存活对象,将会进到 Survivor 的 From 区(From 区内存不足时,直接进入 Old 区)。

大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。

动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

Full GC触发条件

每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小,则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC

如何判断对象是否存活?回收对象的两次标记过程。

引用计数法

​ 给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

​ 优点:实现简单,判定效率也很高

​ 缺点:他很难解决对象之间相互循环引用的问题。

对象可达性

​ 通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的;

两次标记过程

​ 对象被回收之前,该对象的finalize()方***被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。

垃圾回收算法

垃圾回收算法:复制算法、标记清除、标记整理、分代收集

复制算法

​ 将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收;

​ 优点:实现简单,内存效率高,不易产生碎片

​ 缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低

标记清除

​ 标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象

​ 缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚,可能发生大对象不能找到可利用空间的问题。

标记整理

​ 标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题

分代收集:根据各个年代的特点选择合适的垃圾收集算法。

新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。

垃圾收集器

Serial、Parnew、parallel Scavenge、Serialold 、Parnewold、CMS、G1

Serial:采用复制算法

Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。

Serial old:标记-整理算法

Serial收集器的⽼年代版本,它同样是⼀个单线程收集器,使用标记-整理算法。主要有两个用途:

ParNew:复制算法

 ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程

parallel Scavenge和CMS(老年代收集器,标记清除算法)

Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。

parallel old:标记整理算法

​ Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。

CMS收集器

​ CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

初始标记

只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程

并发标记

进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程

​ 重新标记

为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程

并发清除

清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

优点:并发收集、低停顿

缺点:对CPU资源敏感;⽆法处理浮动垃圾;使⽤“标记清除"算法会导致⼤量空间碎⽚产⽣。

G1收集器

 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是:

​ 【1】基于标记-整理算法,不产生内存碎片。

​ 【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收

​ G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

创建一个对象的步骤

类加载检查、分配内存、初始化零值、设置对象头、执行init方法

①类加载检查

​ 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。

②分配内存

 在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。

③初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。

④设置对象头

初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。

⑤执⾏ init ⽅法

 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始, <init> ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ <init> ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。</init></init>

详细介绍类加载过程

过程:加载、验证、准备、解析、初始化

加载阶段

​ 1.通过一个类的全限定名来获取定义此类的二进制字节流。

​ 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

​ 3.在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。

验证阶段

​ 1.文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)

​ 2.元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)

​ 3.字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)

​ 4.符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)

准备阶段

​ 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值

解析阶段

​ 解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化阶段

​ 初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。

双亲委派机制,使用这个机制的好处?如何破坏?

​ 每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。即在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派该⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。

使用好处

​ 此机制保证JDK核心类的优先加载;使得Java程序的稳定运⾏,可以避免类的重复加载,也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。

破坏双亲委派机制

可以⾃⼰定义⼀个类加载器,重写loadClass方法;

了解下tomcat的类加载机制

先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。

如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然

有效,ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加载。

如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。

加载依然失败,才使用 AppClassLoader 继续加载。

都没有加载成功的话,抛出异常。

总结一下以上步骤,WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。

 

猜你喜欢

转载自blog.csdn.net/weixin_37841366/article/details/109110242