八股文


目录

1 操作系统

1.1 进程和线程的区别

  • 进程是资源分配的最小单位,线程是任务执行的最小单位。
  • 进程有自己独立的地址空间,每启动一个线程,都会为它分配地址空间,建立数据表来维护代码段、堆栈端和数据段,这种操作非常昂贵。而线程是共享进程数据的,使用相同的地址空间,因此 CPU 切换一个线程的开销远比进程要小得多,同时创建一个线程的开销也比进程小得多。
  • 线程间的通信更加方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以进程间通信进行。不过如何处理同步和互斥是多线程编程的难点。
  • 多进程更加健壮,多线程程序只要有一个线程死掉,整个进程也就死掉了,而一个进程死掉,并不会对其他进程产生影响,因为进程有独立的地址空间。

1.2 进程的调度算法有哪些?

  • 先来先去服务

  • 时间片轮转

  • 短作业优先

  • 多级反馈队列调度算法

  • 优先级调度

1.3 常用的 IO 模型

  • 同步阻塞 IO:应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。
  • 同步非阻塞 IO:进程发起 IO 系统调用后,内核返回一个错误码而不会被阻塞;应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成。如果内核缓冲区有数据,内核就会把数据返回进程。一个输入操作通常包含两个过程:
    • 等待数据准备好,对于一个 socket 上的操作,这一步骤涉及到数据从网络送达,并将其复制到内核的某个缓冲区。
    • 将数据从内核缓冲区复制到进程缓冲区。
  • 异步 IO:进程发起一个 IO 操作,进程返回不阻塞,但也不能返回结果;内核把 IO 处理完后,通知进程结果。
  • IO 复用:使用 select 、poll 等待数据,可以等待多个 socket 中的任何一个变为可读。这一过程会被阻塞,当某一个 socket 可读时返回,之后把数据从内核复制到进程中。

1.4 select、poll 和 epoll 的区别?

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

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

  • select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监视少于 1024 个描述符。如果要监听更多描述符,需要修改 FD_SETSIZE 后重新编译。
  • poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态,但 poll 没有描述符的限制,原因是它基于链表来存储。

epoll 可以理解为 event poll,不同于 select、poll,epoll 会将哪个流发生了怎样的变化通知我们,epoll 是基于事件驱动的。已注册的描述符在内核中会被维护在一颗红黑树中,调用 callback 的描述符会被加入一个链表中去管理;epoll 的触发模式有两种:

  • LT(水平触发):当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait()会再次通知进程。
  • ET(垂直触发):和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。

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

1.5.1 进程间的通信方式
  • 匿名管道
  • 有名管道
  • 消息队列
  • 信号
  • 信号量
  • 共享内存
  • 套接字
1.5.2 线程间通信方式
  • 互斥量
  • 信号量
  • 事件

1.6 fork 函数的作用?

fork 函数的作用是在已经存在的线程中创建一个子进程,原进程为父进程。

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

  • 分配新的内存块和内核数据结构给子进程。
  • 将父进程部分数据结构内容拷贝至子进程。
  • 将子进程添加到系统进程列表。
  • fork 返回开始调用器进程调度。

fork 调用一次返回两次,返回给父进程子进程的 pid,返回给子进程 0;父进程和子进程的代码段是共享的,其他都是得到父进程一个副本,所以父进程和子进程是两个进程,两者并不会共享内存地址。

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

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

  • 用户态:用户运行的进程可以读取用户程序的数据。
  • 内核态:内核态运行的程序几乎可以访问计算机的任何资源。

运行的程序基本上都是运行在用户态,如果我们调用操作系统提供的内核级子功能就需要进程系统调用来切换到内核态去执行。

系统调用:与系统级别的资源有关的操作(文件管理、进程管理、内存管理等),都必须通过系统调用的方式向操作系统发起请求,由操作系统代为执行。

用户态切换到内核态的几种方法:

  • 系统调用:系统调用是用户态主动切换到内核态的一种方式,用户应用程序通过操作系统调用来完成上层应用程序开放的接口。
  • 异常:当 cpu 执行用户应用程序发生异常后,发生了某些不可知的异常,于是当前用户应用程序切换到处理此异常的内核程序。
  • 硬件设备的中断:当硬件设备完成用户请求后,会向 cpu 发送相应的中断信号,此时 cpu 会暂停执行下一步需要执行的指令,转而去执行与中断信号对应的应用程序,如果之前下一步要执行的指令是用户态,那么这个操作也可以将用户态切换成内核态。

2 计算机网络

2.1 为什么网络要分层?

  • 各层之间相互独立:各层之间相互独立,不需要关心其他层如何工作的,只需要调用其他层提供的接口。
  • 提高整体灵活性:每一层都可以使用最合适的技术来实现。
  • 大问题化小问题:将功能进行分解。

2.2 TCP/IP 4 层模型了解么?

  • 应用层
  • 传输层
  • 网络层
  • 网络接口层

2.3 HTTP 是哪一层的协议?http 常见的状态码?

HTTP 是应用层的协议。

  • 200:请求成功
  • 301:永久重定向
  • 302:临时重定向
  • 400:请求参数错误
  • 401:请求为授权
  • 403:没有访问权限
  • 404:找不到对应资源
  • 405:请求方式不支持
  • 500:服务器错误
  • 502:网关错误
  • 504:代理服务无法及时从上游服务器获得响应

2.4 HTTP 和 HTTPS 什么区别?

  1. HTTP 默认采用 80 端口,HTTPS 默认采用 443 端口;
  2. HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器都无法鉴别对方的身份;HTTPS 是运行在 SSL 之上的 HTTP 协议,所有传输过程都经过了对称加密,但对称加密的密钥用服务器的证书进行了非堆成加密;
  3. HTTP 的安全性没有 HTTPS 高,但是 HTTPS 需要消耗更多的服务器资源。

2.5 讲一下对称加密算法和非对称加密算法?

对称加密:密钥只有一个,加密解密为同一个密码,加解密速度快,常见的加密算法有 DES、AES 等。

非对称加密:加密和解密采用不同的密钥。通信发送方使用接收方的公开密钥加密,接收方使用私钥解密。运算速度慢,常见的非对称加密有 RSA、DSA 等。

2.6 HTTP报文详解?详细说一下请求报文,以及HTTP和TCP的区别

HTTP 有两种报文,分别是请求报文和响应报文。

请求报文包括请求行、请求头、空行、请求体。

响应报文包括状态行、响应头、响应体。

2.7 TCP三次握手的过程,以及三次握手的原因?

三次握手

三次握手的目的是为了确定双方的发送和接受都是正常的;第三次握手是为了防止失效的连接请求到达服务器,让服务器错误的打开连接。

2.8 TCP四次挥手的过程,以及四次挥手的原因?

四次挥手CLOSE-WAIT 状态问题:

客户端发送了 FIN 连接释放报文后,服务端收到这个报文后,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器发送未发送完毕的数据,传送完毕后,服务端会发送 FIN 连接释放报文。

TIME-WAIT 状态问题

客户端收到服务器发来的 FIN 连接释放报文后进入此状态,此时不是直接进入 CLOSED 状态,还需要等待 2MSL,有两个原因:

确保最后一个报文能到达服务端,如果服务端没有收到客户端发来的确认报文,就会重新向客户端发送 FIN 连接释放报文;当重新收到服务端发来的 FIN 连接释放报文后,会向服务端发送确认报文并重置计时器。等待一段时间是为了让本连接产生的所有报文都在网络上消失,使得下一个连接不会出现旧的连接请求报文。

2.9 TCP 滑动窗口是干什么的?TCP 的可靠性体现在哪里?拥塞控制如何实现的?

滑动窗口:

窗口是缓存的一部分,用来暂存字节流。发送方和接收方各有一个窗口,接收方通过报文中的窗口字段告知发送方自己的窗口大小。发送方根据这个值和其他信息设置自己的窗口大小。发送窗口内的字节都被允许发送,接受窗口内的字节都被允许接收。接收方只会对窗口内的最后一个字节确认,如果发送窗口内的字节已经发送并且已经得到确认,那么就将发送窗口向右滑动一定距离;接收窗口类似,接收窗口左部字节已经发送确定并交付主机,就向左滑动一定距离。

流量控制实现:

流量控制是为了控制发送发的发送速率以保证接收方来得及接收。接收方发送的确认报文中的窗口字段告知发送方自己的窗口的大小,从而影响发送方的发送速率。将窗口字段设为 0,则发送方不能发送数据。

拥塞控制:

如果网络出现拥塞,则分组就会丢失,此时发送方会继续重传,从而导致拥塞更多严重。因此发生网络拥塞时,应该控制发送发的发送速率。TCP 通过四个算法来控制发送方的发送速率:慢开始、拥塞避免、快重传、快恢复。发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量。

  • 慢开始与拥塞避免:发送方最初执行慢开始,此时将 cwnd 设为 1,发送方只能发送一个报文段,收到确认后,将 cwnd 加倍。设置一个慢开始门限,超过门限后进入拥塞避免,每次将 cwnd + 1,如果出现了超时,则将慢开始门限设为 cwnd / 2,然后重新执行慢开始。
  • 在接收方,要求每次接收到报文段都应该对最后一个报文段进行确认,如果收到三个重复的确认,那么就可以认为下一报文丢失,此时执行快重传,立即重传下一报文段。在这种情况下,只是丢失个别报文段,并不是网络拥塞,所以将慢开始门限设为 cwnd / 2,拥塞窗口设为慢开始门限,直接进入拥塞避免。

可靠性如何保证:

TCP 使用超时重传来实现可靠传输。如果一个已经发送的报文段在超时时间没有收到确认,那么就重传这个报文段。

  1. 应用程序被分割成 TCP 认为最适合发送的数据块;

  2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层;

  3. 检验和:TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段;

  4. TCP 的接收端会丢弃重复的报文;

  5. 流量控制;

  6. 拥塞控制;

  7. ARQ 协议:每发送完一个分组就停止发送,等待确认,收到确认后再发送下一个分组;

  8. 超时重传:当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

2.10 TCP 和 UDP 有什么区别?及其适用的场景?

UDP 是无连接的,尽最大可能交付,没有拥塞控制,面向报文,支持一对一、一对多、多对多的交互通信。

TCP 是面向连接的,提供可靠支付,有流量控制、拥塞控制,提供全双工通信,面向字符流,每一个 TCP 是点对点的。

TCP 应用场景:

  • 文件传输
  • 邮件发送
  • 远程登录

UDP 应用场景:

  • QQ 聊天
  • 在线视频
  • 语音聊天

UDP 为什么快?

  • 不需要建立连接
  • 不需要对接收的数据给出确认
  • 没有超时重发
  • 没有流量控制和拥塞控制

2.11 Mac 地址和 IP 地址的区别?既然有了 Mac 地址,为什么还要 IP 地址呢?

Mac 地址是烧录在网卡的物理地址,具有全球唯一性,IP 地址是网络中的设备在局域网的逻辑地址,具有局域网唯一性。

2.12 当你打开一个电商网站,都需要经历哪些过程?分别用到了什么协议?

  1. 浏览器查找域名对应的 IP 地址;
  2. 浏览器向 web 服务器发送 HTTP 请求;
  3. 服务器处理请求;
  4. 服务器返回 HTTP 报文,发送 HTML 响应;
  5. 浏览器解析渲染页面;

使用的协议:

  1. DNS
  2. IP
  3. ARP
  4. OSPF
  5. HTTP

2.13 DNS 解析过程,DNS 劫持了解吗?

  1. 客户机提出域名解析请求,并将该请求发送给本地域名服务器;
  2. 当本地域名服务器收到请求后,首先查询本地缓存,如果有记录,直接返回;
  3. 如果本地缓存中没有记录,则本地域名服务器就直接把请求发给根域名服务器,然后跟域名服务器再返回给本地域名一个所查询域主域名服务器地址;
  4. 本地服务器再向上一步返回的域名服务器发送请求,然后接收请求的服务器查询自己的缓存,如果没有该记录,则返回相关下级域名服务器地址;
  5. 重复第四步,直到找到记录;
  6. 本地域名服务器将查询结果保存,以备下次使用。

DNS 劫持:在 DNS 服务器中,将 www.xx.com 域名对应的 IP 地址进行了变化,解析出来的 IP 地址不是正确的地址。

HTTP劫持:在网站交互过程中劫持了请求,在网站返回请求之前向你返回了伪造的请求。

2.14 GET 和 POST 有什么不一样?

  • GET 在浏览器回退是无害的,而 POST 会再次发送请求。
  • GET 会被浏览器主动缓存。
  • GET 有长度限制,POST 没有长度限制。
  • GET 相对于 POST 不安全,因为参数暴露在 url 中。
  • GET 请求参数会完整的保存在历史记录中,而 POST 不会。
  • GET 会将 header 和 body 同时发送给服务器,而 POST 第一次先将 header 发送给服务器,收到浏览器返回 100 contine,再将 body 请求给服务器。

2.15 session 和 cookie的问题?

session 和 cookie 都是用来保存浏览器用户身份的会话方式。

cookie 一般用来保存用户信息,Session 主要作用是通过服务端记录用户的状态。

cookie 数据保存再客户端,session 数据保存在服务端。

session 比较安全,session 依赖于 cookie 实现,如果禁用 cookie,session 也不可使用。

2.16 HTTP 是不保存状态的协议,如何保存用户状态?

session、cookie、内存数据库

cookie 被禁用 session 怎么用?

url 携带、矩阵变量

2.17 Arp 协议?

  1. 如果发送端和接收端在同一个网段,发送端发送数据帧前先检查是否有接收端的 mac 地址;
  2. 如果没有,启动 arp,检查缓存 ip-mac 表中是否有接收端的 mac 地址,如果有,拿来就用;
  3. 如果没有则在本网段广播,本网段各计算机都收到 arp 请求,从发送来的数据检查接收方 ip 是否和自己相同,如果相同,则将本机的 mac 地址单播给发送端,如果不相同,直接丢弃。
  4. 如果发送端和接收端不在同一个网段,请求端拿到的 mac 地址是网关的 mac 地址。

2.18 DDos 攻击了解吗?

分布式拒绝攻击,一般来说指攻击者利用控制的设备对目标网站短时间发送大量的请求,消耗目标网站的服务器资源,让它无法正常服务。

2.19 什么是跨域,怎么解决?

跨域访问基于同源策略,它要求通信的双方必须在同一域中,当一个请求的协议、域名、端口 不完全相同就产生了跨域问题。

解决:

  • JSONP

  • CORS

2.20 常见的 WEB 攻击手段及解决方案?

CSRF

攻击者欺骗客户端去访问之前认证的网站

解决:

  • Referer
  • 添加校验 token

XSS

XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。

解决:

  • 后台加过滤器

3 Java 基础

3.1 StringBuilder 和 StringBuffer?

StringBuilder 和 StringBuffer 都是可变的字符串类型,但是 StringBuffer 类中的方法大量使用了 synchronization 修饰,所以 StringBuffer 是线程安全的,而 StringBuilder 是线程不安全的。

3.2 Java实现连续空间的内存分配?

3.3 创建对象的方式有哪几种?

  • new Obj()

  • clone:调用对象的 clone 方法

  • 反射

    • 无参构造,需要提供对象的无参构造,如果没有会抛异常

      Object o = clazz.newInstance();
      
    • 有参构造,需要先获得对象的构造器,通过构造器调用 newInstance 函数创建对象

      Constroctor constroctor = User.class.getConstructor(String.class);
      Object obj =  constroctor.newInstance("name");
      
  • 通过反序列化创建对象,需要对象实现 Serializable 接口

3.4 接口和抽象类有什么区别?

  • 接口和抽象类都不能直接实例化。
  • 抽象类通过 extends 继承,接口通过 implments 实现。
  • 抽象类可以有非抽象方法,接口只能对方法声明。
  • 抽象类可以 public、protected、default 修饰符,接口只能存在 public。
  • 抽象类可以有构造器,接口不能有构造器。

3.5 深拷贝和浅拷贝区别?

  • 深拷贝:对于基本类型,拷贝的是基本类型的指;对于引用类型,创建一个新的对象,把值赋值进去。

  • 浅拷贝:对于基本类型,拷贝的基本类型的指;对于引用类型,拷贝的是引用类型的内存地址。

3.6 讲一讲封装,继承,多态(重要)?

编译期多态

方法重载是编译期多态,根据参数类型、个数,在编译期就可以确定执行重载方法中的哪一个。

方法重写表现两种多态,当对象引用本类实例时,表现编译器多态,否则时运行时多态。

运行时多态

通过父类对象引用变量引用子类对象。当父类对象引用子类实例时,通过接口类型变量引用实现接口的类的对象来实现。运行时多态主要通过继承和接口来实现。

3.7 泛型是什么?类型擦除?

将类型当作参数传递给类或者方法。

Java 中的泛型是伪泛型,它是在编译器层面实现的。在 Java 的编译期间,所有的泛型信息都会被擦除掉,最后的在字节码中不包含泛型中的任何信息的。

Java 编译器先检查泛型类型,然后在进行泛型擦除,再进行编译。

3.8 如何实现静态代理?有啥缺陷?

为现有的每个类编写一个特定的代理对象,实现相同的接口。

在代理类的构造方法中传入目标对象,然后在代理类方法中调用目标对象的同命方法,在这行代码前后做一些操作。

3.9 动态代理的作用?在哪些地方用到了?(AOP、RPC 框架中都有用到,面试笔试中经常要求手写一个动态代理)

为其它对象提供一种代理以控制对这个对象的访问控制,在程序运行时,通过反射机制动态生成。JDK动态代理的调用处理程序必须实现 InvocationHandler 接口,及使用 Proxy 类中的 newProxyInstance 方法动态的创建代理类。

public interface HelloInterface {
    void sayHello();
}

public class Hello implements HelloInterface {
    @Override
    public void sayHello() {
        System.out.println("Hello!");
    }
}

public class ProxyHandler implements InvocationHandler {
    
    private Object object;
    
    public ProxyHandler(Object object){
        this.object = object;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke "  + method.getName());
        method.invoke(object, args);
        System.out.println("After invoke " + method.getName());
        return null;
    }
    
    public static void main(String[] args) {
        System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        HelloInterface hello = new Hello();
        
        InvocationHandler handler = new ProxyHandler(hello);

        HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);

        proxyHello.sayHello();
    }
}

3.10 JDK 的动态代理和 CGLIB 有什么区别?

JDK 可以代理实现了接口的类,CGLIB 可以代理未实现任何接口的类,CGLIB 的原理是通常生成一个被代理类的子类来拦截被代理类的方法调用,因此不能将被代理类声明成 final 类型。

3.11 谈谈对 Java 注解的理解,解决了什么问题?

Java 语言中的类、方法、变量、参数和包等都可以注解标记,程序运行期间我们可以获取到相应的注解以及注解中定义的内容,这样可以帮助我们做一些事情。比如说 Spring 中如果检测到说你的类被 @Component 注解标记的话,Spring 容器在启动的时候就会把这个类归为自己管理,这样你就可以通过 @Autowired 注解注入这个对象了。

3.12 Java 反射?反射有什么缺点?你是怎么理解反射的(为什么框架需要反射)?

反射是指在程序运行过程中,我们可以知道任何一个类的所有属性和方法,对于任何一个对象,可以调用它所有的属性和方法;这种动态获取信息和动态调用对象方法的技术称为反射。

反射的优缺点:

  • 运行期类型的判断、动态加载类、提高代码灵活度。
  • 性能问题:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多
  • 安全问题

3.13 为什么框架需要反射技术?

我认为可以动态的创建和编译对象,体现出语言强大的灵活性和扩展性。

3.14 获取 Class 对象的两种方式

// 第一种
Class alunbarClass = TargetObject.class;        
// 第二种
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");

3.15 内存泄露和内存溢出的场景?

内存泄漏:内存泄漏是指无用对象持有占有内存或无用对象的内存得不到释放,从而造成内存空间的浪费称为内存泄漏。内存泄漏的根本原因是长生命周期的对象持有短生命周期的引用就很可能发生内存泄漏,尽管短生命周期的对象已经不再需要,但因为长生命周期的对象持有它的引用,导致不能回收。

内存溢出:内存溢出是指程序运行过程中无法申请到足够的内存。

3.16 讲一下,强引用,弱引用,软引用,虚引用?

  • 强引用:被强制关联的对象不会被回收。
  • 软引用:被软引用关联的对象会在内存不足的情况下回收。
  • 弱引用:被弱引用关联的对象的生命周期到下一次 GC。
  • 虚引用:弱引用必须和引用队列配合使用,当 GC 时发现还关联着虚引用,会在执行完 finalize() 将虚引用加入到引用队列中,在其关联的虚引用没有出队钱,并不会彻底的清除这个对象,此时该对象为虚可达,只要显示的在引用队列中将虚引用出队,才会彻底回收掉该对象,可以用来控制对象的清除时机。

3.17 讲一下 Java 的 NIO,AIO, BIO?

BIO:同步阻塞 IO 模型,读取写入阻塞在一个线程中进行

NIO:同步非阻塞 IO 模型,提供了 Channel、Seletor、Buffer 对象,我们可以通过 NIO 进行非阻塞 IO 操作;单线程从通道读取数据到 Buffer,同时可以继续做别的事情,等到读取完毕,线程在继续处理数据,NIO 底层依赖于 epoll 实现。

AIO:异步非阻塞模型,基于事件和异步回调机制实现。

3.18 Java 中 finalize()方法的使用?

GC 回收对象前会调用此方法,一般用户非 Java 资源的回收,一般不推荐调用此方法,因为 finalize() 方法调用时间不确定,从一个对象不可达到 finalize() 方法的调用之间的时间不任意长的。我们并不能依赖 finalize() 方法能 及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc 却仍未触发,因而通常的做法是提供显示的 close() 方法供客户端手动调用。另外,重写 finalize() 方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。

3.19 GC Root 对象有哪些?

  • 方法区静态变量和常量引用的对象
  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象

3.20 Java 中 Class.forName 和 ClassLoader 的区别?

类的加载过程:

  1. 装载:通过类的全限定名获取二进制字节流,将二进制字节流转换成方法区运行时数据结构,在内存中生成 Java.lang.Class 对象;
  2. 链接:执行校验、准备和解析;
  3. 校验:检查类或者接口的二进制数据的正确性;包括文件格式验证、元数据验证、字节码验证、符号引用验证;
  4. 准备:给类的静态变量分配内存并初始化内存空间;
  5. 解析:将常量池中的符号引用转化成直接引用;
  6. 初始化:执行 方法;包括静态变量初始化语句和静态块的执行。

ClassLoader 遵循双亲委派机制,最终调用启动类加载器通过某个类的全限定名去获取类的二进制字节流,然后将二进制字节流放到虚拟机中,不会执行 static 内容,Class.forName() 会执行 static 内容。

4 集合框架

4.1 ArrayList 的扩容机制?

ArrayList 的扩容发生于 add() 方法中,add() 方法添加元素先通过 ensureCapacityInternal() 方法判断是否需要扩容;

流程:

  1. 获取原数组的容量;
  2. 原数组长度的 1.5 倍;
  3. 如果新长度小于最小需要的容量,将 minCapacity 赋值给 newCapacity;
  4. 如果新长度大于数组预置的最大长度,将 newCapacity 设置为 Integer 最大值;
  5. 然后通过 copyOf 将原数组元素复制到新数组中。

4.2 HashMap 的底层实现、JDK 1.8 的时候为啥将链表转换成红黑树?HashMap 的负载因子?

HashMap 底层是数组+链表+红黑树来实现的,当向 map 中添加数据时,首先计算 key 的 hash 值,并根据 hash 值确定元素存储在哪个 bucket 中,但是容易产生 hash 碰撞,hashmap 是采用拉链法的方式来解决 hash 碰撞的,当链表长度大于等于 8 时,这时候对于这个 bucket 中查询效率会退化成 O(N),会将底层数据结构变成红黑树,这种考虑是因为红黑树的查询效率更加稳定,当链表的长度小于等于 6 时,会重新将红黑树转换成链表,这种考虑是因为红黑树比链表的存储代价要大。

转换成红黑树还有一个必要条件就是数组长度要大于 64,因为作者认为在数组长度低于 64,产生 hash 碰撞的几率非常高。

hashmap 的负载因子是 0.75。

4.3 为什么 HashMap 长度是 2 的幂?

首先看 hash 方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

获得 key 被存放在数组的哪个 bucket 中

i = (n - 1) & hash

这两个需要连起来看,这也是 1.8 做出的优化之一,在 hash() 方法把 hashCode 的高 16 和低 16 都参与运算,这样产生 hash 碰撞的几率大大降低,也会让数据更加平均。

经实际经验,h % (length-1) == (length - 1) & hash 的前提是这个 length 必须是 2 的幂次方,& 的效率比

4.4 HashMap 的 key 需要注意哪些问题

key 必须是不可变对象,如 String、包装类和采用 final 修饰的类。

4.5 HashMap hash 算法可以采用随机数吗?

不可以,因为下次不能定位到存储的 bucket。

4.6 HashMap 和 Hashtable 的区别?

  • HashMap 允许一个 key 为 null,多个 value 为 null,Hashtable 不允许 key/value 为 null。
  • HashMap 是线程非安全的,Hashtable 是线程安全的。
  • HashMap 的默认长度是 16, Hashtable 的默认长度是 11。
  • HashMap 扩容两倍,Hashtable 扩容两倍 + 1。
  • HashMap 需要调用 hash() 方法计算 hash 值,Hashtable 直接调用 hashCode 值。

4.7 HashMap 和 HashSet 的区别?

HashSet 的构造方法中创建了 一个 HashMap,并且提供了一个空对象 PRESENT,不允许重复的原理实际上使用了 HashMap key 不能重复的特性。

4.8 ConcurrentHashMap 的底层实现?

ConcurrentHashMap 在 1.7 采用了分段的数组+链表实现,1.8 采用的数据结构和 HashMap 一致。

在 1.7 时,ConcurrentHashMap 采用了分段锁,每一把锁只锁定他所管辖的那部分数据,多线程访问不同数据段的数据就不会产生锁的竞争,提交并发率;1.8 摒弃了这种臃肿的设计,并发控制采用 CAS+Synchronized 来实现,因为 1.7 最多同时有 16 个线程进行读写操作,Synchronized 只锁定当前链表或者红黑树的首节点,只要 hash 不冲突,就不会产生并发。

4.9 为什么 ConcurrentHashMap 的读操作不需要加锁?

因为无论是 Node 还是 HashEntity,value 都已经被 volatile 修饰了,volatile 可以保证工作内存中共享变量的可见性,所以在 get() 方法中拿到的值永远是最新值。

4.10 HashMap,LinkedHashMap,TreeMap 有什么区别?

LinkedHashMap 记录元素的插入顺序,在使用 iterator 遍历时,先取到的数据肯定是先插入的;

TreeMap 实现了 SortMap 接口,所以它可以对 key 按照顺序排列;

4.11 有哪些集合是线程不安全的,又有哪些集合是线程不安全的?怎么解决呢? 线程安全的集合类.

HashMap -> ConcurrentHashMap

HashSet -> CopyOnWriteHashSet

ArrayList -> CopyOnWriteArrayList

4.12 什么是快速失败?什么是安全失败?

快速失败是集合的一种错误检查机制,在使用迭代器遍历时,如果在多线程下操作非安全的集合类,就会产成 ConcurrentModificationException 异常,另外,在单线程遍历时对元素进行修改也会触发快速失败。

安全失败,安全失败的集合在遍历时不是在原来的对象上进行操作的,而是在拷贝出的集合进行操作。

public static void main(String[] args) {
    ArrayList<Object> objects = new ArrayList<>();
    objects.add(1);
    objects.add(1);

    Iterator<Object> iterator = objects.iterator();

    // 多线程操作
    new Thread(() -> {
        while (iterator.hasNext()) {
            iterator.next();
        }
    }).start();

    new Thread(() -> {
        while (iterator.hasNext()) {
            objects.remove(iterator.next());
        }
    }).start();

	// 单线程遍历修改
    for (Object object : objects) {
        objects.remove(object);
    }
}

为什么会产生安全失败问题?

当调用 ArrayList.iterator() 方法时,首先会将 modCount 赋值给 expectedModCount,当调用 next 方法时,会判断 modCount 是否 等于 modCount,我们调用 ArrayList.remove() 时,会修改 modCount 的值,但是不会修改 expectedModCount 的值,所以只要在遍历时修改列表的元素,都会造成 modCount 和 expectedModCount 不一致。

4.13 HashMap 多线程操作导致死循环问题异常

主要原因在于并发下的 rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。

4.14 讲一下 CopyOnWriteArrayList 和 CopyOnWriteArraySet?

写时复制的容器,当我们向容器中添加一个元素时,不直接向容器中添加,而是将当前容器进行 copy,在 copy 出来的副本上进行添加,添加完成之后,再将原容器的引用指向新容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读。在读时并不需要加锁。适合读多写少的场景。

缺点:

  • 资源浪费
  • 能保证数据的最终一致性,不能保证数据实时一致性

5 多线程

5.1 写一个死锁

public static void main(String[] args) {
    Object o1 = new Object();
    Object o2 = new Object();
    new Thread(() -> {
        synchronized (o1) {
            try {
                System.out.println(Thread.currentThread().getName() + " 获取第一把锁成功");
                TimeUnit.SECONDS.sleep(1);
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + " 尝试获取第二把锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "A").start();
    new Thread(() -> {
        synchronized (o2) {
            try {
                System.out.println(Thread.currentThread().getName() + " 获取第二把锁成功");
                TimeUnit.SECONDS.sleep(1);
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + " 尝试获取第一把锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "B").start();
}

5.2 讲一下 volatile 关键字的作用?

voliatile 特性:

  • 可见性

  • 禁止指令重排

  • 不保证原子性

volatile 是怎么保证可见性和禁止指令重排的?

如果对 vlolatile 修饰的变量写,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在的缓存行的数据强制写回到主内存。但是即使我这边将共享变量写会了主内存,其他线程工作内存中的值还是旧的,所以在多处理器,为了保证各个处理器的缓存是一致的,就要依赖于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,当处理器发送自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设为无效状态,当处理器要对这个数据做修改操作时,强制要求处理器必须从主内存重新读取。

happend-before 原则

  • 程序顺序原则:一个线程中每个操作,happend-before 于该线程中的任意后续操作。
  • 监视器原则:一个监视器的解锁 happend-before 后续对这个监视器的加锁。
  • voliate 原则:对一个 volatile 的写 happend-before 于对这个 volatile 的读。
  • 传递性原则:A happend-before B,B happend-before C,则 A happend-before C。
  • start 原则:如果线程 A 执行 线程 B.start,那么线程 A 中的 线程 B.start() happend-before 于线程 B 的任意操作。
  • join 原则:如果线程 A 执行 线程 B.join(),那么线程 B 的任意操作 happend-before 于线程 A 执行完线程 B.join() 成功返回的后续操作
  • 程序中断原则:对一个线程的中断 happend-before 与被中断线程检测到中断时间的发生。
  • 对象的析构原则:一个对象的初始化 happend-before 于这个对象的析构。

5.3 synchronized 作用,讲一讲底层实现。

synchronized主要有 3 种使用方式:

  • 修饰实例方法,锁对象为调用这个方法的实例。
  • 修饰静态方法,锁对象为该类的 class。
  • 修饰代码块,需要指定锁对象。

synchronized 底层语义原理:

JVM 中的同步基于进入和退出管程对象来实现的,无论是显示同步还是隐式都是如此。

Java 对象头和 monitor

在 JVM 中,一个对象由三部分构成:

  • 实例数据:存放类的的属性数据信息,包括父类的属性信息,如果是数组的话实例数据部分还包含数组的长度,这部分内存按 4 字节对齐。

  • 对其填充:由于虚拟机要求对象起始地址必须是 8 字节的整数倍,对于不满 8 字节整数倍的对象对齐。

  • 对象头:由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到虚拟机的空间成本,所以对象头被设计成一个非固定的结构,以便存储更多有效的数据。

    img

    在虚拟机中,monitor 是由 ObjectMonitor 实现的,其主要数据结构如下所示:

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;
        _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
    }
    

    ObjectMonitor 中有两个队列,_waitSet 和 _EntityList,用来保存 ObjectWaiter 对象列表(每个等待的线程都会被封装成一个 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntityList 集合,当获取到 ObjectWaiter,将 monitor 中的 _owner 设为当前线程,同时 _count++,若线程调用 wait(),将释放持有 monitor,_owner 重新为 null,_count--,同时进入该线程进入 _WaitSet,如果当前线程执行完毕也会释放 monitor,复位 _owner,以便其他线程获取 monitor。

5.4 ReetrantLock 和 synchronized的区别

5.5 说说 synchronized关键字和 volatile关键字的区别

5.6 ReetrantLock实现方式

5.7 AQS 实现原理

5.8 interrupt、interrupted 与 isInterrupted方法的区别? 如何停止一个正在运行的线程

  • interrupt:将当前线程的状态设为中断状态

  • interrupted:返回当前线程的中断状态并清除中断态

  • isInterrupted:返回当前线程的中断状态

如何停止一个正在运行的线程

  • 利用标志位
  • 调用 interrupted,根据标志位由用户处理
  • 调用 stop() 方法

5.9 线程池作用?Java 线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?线程池的工作机制?

线程池作用

  • 减少创建线程的开销

  • 提高线程的可管理性

七大参数:

  • corePoolSize:核心线程数
  • maximumPoolSize:最下线程数
  • keepAliveTime:空闲线程保活时间
  • unit:keepAliveTime 的单位
  • workQueue:工作队列
  • threadFactory:线程工厂
  • handler:拒绝策略

阻塞队列有哪几种?

  • ArrayBlockingQueue:基于数组实现的有界阻塞队列,此队列按照先进先出原则对元素进行排序

  • LinkedBlockingQueue:基于链表实现的有界阻塞队列,此队列按照先进先出原则对元素进行排序

  • SynchronousQueue:不存储元素的阻塞队列,每次插入操作必须等到另一个线程做移除操作,否则插入操作一直处于阻塞状态

  • PriorityBlockingQueue:优先级队列,进入队列的元素按照优先级进行排序

线程池常见的拒绝策略:

  • AbortPolicy:直接抛弃,并抛出异常
  • CallerRunsPolicy:让调用者去处理
  • DiscardOldestPolicy:丢弃队列中最老的任务,然后执行当前提交的任务
  • DiscardPolicy:直接抛弃

线程池的工作机制?

img

常见的线程池有哪几种?

  • newFixedThreadPool:最大线程数和核心线程数一致,工作队列采用 LinkedBlockingQueue。
  • newSingleThreadExecutor:最大线程数和核心线程数一致,都是 1,工作队列采用 LinkedBlockingQueue。
  • newCachedThreadPool:没有核心线程,直接向 SynchronousQueue 提交任务,如果有空闲线程就执行,否则就新建一个线程,执行完任务的线程有 60s 的存活时间
  • newScheduledThreadPool:最大线程数是 Integer.MAX_VALUE,工作队列采用 DelayedWorkQueue。

5.10 线程池拒绝策略分别使用在什么场景?

AbortPolicy:没有特别的场景,默认

DiscardPolicy:提交的任务无关紧要

DiscardOldestPolicy:发布消息和修改消息,如果发布消息任务还未执行,修改消息就过来了,抛弃发布消息,直接执行修改消息

CallerRunsPolicy:不允许失败,并发量小的场景

5.11 线程死锁,解除线程死锁有哪几种方式?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某 个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

解决死锁的策略

死锁预防:

  • 破坏保持和等待条件:一次性申请所有资源
  • 破坏不可剥夺条件:当一个进程获取了某个不可剥夺的条件时,若要提出新的资源申请,申请不到释放所有资源
  • 破坏循环等待条件:按某一顺序申请资源,释放倒序

死锁避免:进程每次申请资源时判断这些操作是否安全

死锁检测: 判断系统是否属于死锁的状态,如果是,则执行死锁解除策略

死锁解除:将某进程所占资源进行强制回收,然后分配给其他进程

5.12 ThreadLocal 是什么,应用场景是什么,原理是怎样的?

ThreadLocal 可以让每个线程都有自己的本地变量,如果创建了一个 ThreadLocal,那么访问这个变量的每个线程都会有这个变量的本地副本。可以使用 get()、set() 来获取值和设置值,从而避免线程安全问题。

ThreadLocal 的值最终是存放到 ThreadLocalMap 中,并不是存到 ThreadLocal。在 ThreadLocal 中存在一个 ThreadLocalMap 内部类,在执行 get() 方法流程就根据当前的 Thread 获取 ThreadLocalMap,在通过 put 往 ThreadLocal 存放数据的时候,是调用 ThreadLocalMap 的 put() 方法把 ThreadLocal 作为 key,每个线程可以有多个 ThreadLocal 方便存储多个变量。

ThreadLocalMap 采用二次探测法处理 hash 碰撞。

5.13 ThreadLocal类为什么要加上private static修饰?

5.14 ThreadLocal有什么缺陷?如果线程池的线程使用ThreadLocal会有什么问题?

5.15 介绍一下 Java 有哪些锁

5.16 乐观锁和悲观锁讲一下,哪些地方用到。

6 虚拟机

6.1 说一下 JVM 的主要组成部分及其作用?

6.1.1 程序计数器

程序计时器是一块很小的内存空间,可以简单的认为它是当前线程所执行的字节码的行号指示器。字节码解释器工作时改变这个计时器的值来获取下一条需要执行的指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计时器来完成。

为了线程切换后能恢复到正确的位置,每条线程都需一个独立的线程计数器,各线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有区域。

注意:程序计数器是唯一一个不会产生 OutOfMemoryError 的内存区域,它的生命周期随着线程创建而创建,随着线程消亡而消亡。

6.1.2 Java 虚拟机栈

与程序计数器一样,虚拟机栈也是线程私有的内存区域,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

Java 内存区域可以粗略的分为堆和栈,其中栈就是现在说的 Java 虚拟机栈(实际上栈是由一个个的栈帧组成,栈帧包括:局部变量表、操作数栈、动态链接、方法出口等信息)。

局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(refence 类型,它不同于对象本身,可能是一个指向对象起始位置的引用指针,也可能是指向一个代表对象的句柄)。

Java 虚拟机会出现的两种错误:StackOverFlowError 和 OutOfMemoryError

  • StackOverFlowError:若虚拟机栈的内存大小不允许动态扩展,那么当前线程请求深度超过当前虚拟机栈的最大深度的时候,就会抛出 StackOverFlowError
  • OutOfMemoryError:若虚拟机栈中没有空闲内存,并且垃圾回收期也无法提供更多内存的话,就会抛出 OutOfMemoryError。

那么方法、函数如何调用呢?

虚拟机栈中保存的主要内容是栈帧,每一次函数调用都会对应着栈帧的被压入栈,每一个函数调用结束,都会有一个栈帧被弹出。

Java 方法有两种返回方式:return 和 抛出异常。

不管哪种方式都会导致栈帧被弹出。

6.1.3 本地方法栈

和虚拟机栈发挥的作用相同,只不过两者服务的对象的不同,虚拟机栈服务的对象是 Java 方法,本地方法栈服务的对象是虚拟机使用到的 Native 方法。

6.1.4 堆

Java 虚拟机所管理的内存中最大的一块,堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。但是,随着 JIT 编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致所有对象都分配到堆上也渐渐变得不那么绝对了。从 JDK1.7 开始默认开启逃逸分析,如果某些方法的对象引用没有被返回到外部,那么对象可以直接从栈上分配。

Java 堆是垃圾收集器管理的主要区域,因此也被称为 GC 堆。现代收集器都基本采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致还可以分为:Eden、From Survivor、To Survivor 等。进一步划分的目的是更好的进行垃圾回收,或者更快的分配内存。

JVM堆内存结构-JDK8

对象分配的流程:

大部分情况,对象首先从 Eden 分配,在一次垃圾回收后,如果对象还存活,会进入 s0 或 s1,而且对象的年龄还会增加 1,当它的年龄增加到一定程度(默认是 15,Hotspot 遍历所有对象时,按照年龄分别对其所占用的大小进行统计,如果某个年龄段的对象总和超过了 survivor 的一半,取这个年龄和 MaxTenuringThreshold 对比,取较小的那个作为新的晋升年龄阈值),就会晋升到老年代。

6.1.5 方法区

方法区和 Java 堆一样,是各个线程共享的区域,它同于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是 Java 虚拟机规范描述的一个逻辑部分。

方法区和永久代的关系:

方法区是 Java 虚拟机规范中的定义,永久代是 Hotspot 虚拟机对虚拟机规范中方法区的一个具体实现。

常用参数:

JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小

  • -XX:PermSize=N:方法区 (永久代) 初始大小
  • -XX:MaxPermSize=N:方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了,取而代之是元空间,元空间使用的是直接内存

  • -XX:MetaspaceSize=N:设置 Metaspace 的初始和最小大小
  • -XX:MaxMetaspaceSize=N:设置 Metaspace 的最大大小

为什么要讲永久代替换为元空间?

  1. 整个永久代有一个 JVM 设置固定的大小上限,无法进行调整,而元空间使用的是直接内存,受本机内存的限制,虽然还可以发生溢出但是几率会更小;
  2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不会受 PermSize 的限制了,能加载更多类的元数据。
6.1.6 运行时常量池

运行时常量池也是方法区的一部分,用来存放运行期间产生的新的常量。JDK 1.7 之前的运行时常量池包括字符串常量池存放在方法区,此时 Hotspot 虚拟机对方法区的实现为永久代;JDK 1.7 字符串常量池被从方法区拿到了堆中,JDK 1.8 取消了永久代,这时字符串常量池还在堆中,运行时常量池被拿到了元空间中。

6.1.7 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也有可能导致 OOM。

JDK 1.4 中新加入的 NIO 类,引入了一种基于通道与缓冲区的 I/O 方式,它可以直接使用 Native 函数库分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的的引用进行操作。这样就能在一些场景中显著提高性能,避免了在 Java 堆和 Native 堆之间来回复制数据。

6.2 说一下堆和栈的区别?

6.3 HotSpot虚拟机对象探秘

6.3.1 对象的创建

Step1:类加载检查

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

Step2:分配内存

在类加载检查通过后,虚拟机就可以为新生对象分配内存。对象所需大小在类加载通过即可确定,为对象分配内存等同于把一块确定大小的内存空间从堆中划分出来。分配方式有指针碰撞和空闲列表两种,选择哪种方式由堆是否规整决定,堆是否规整由所采用的垃圾收集器是否带有压缩整理决定。

内存分配的两种方式

内存分配的并发问题?

在创建对象的时候有一个很重要的问题,就是线程安全,在实际开发中,创建对象是很繁琐的问题,作为虚拟机来说,必须保证线程安全,通常虚拟机会采用以下两种方式来保证线程安全:

  • CAS+失败重试:CAS 是乐观锁的一种实现。所谓乐观锁就是,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS+失败重试来保证更新操作的原子性。
  • TLAB:为每个线程在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 的剩余空间或者 TLAB 的空间用尽后,再采用 CAS+失败重试的方式。

Step3:初始化零值

内存分配完毕后,虚拟机需要将分配好的内存空间都初始化为零值,这一步操作保证了对象的实例字段在 Java 代码中可以不赋值直接使用。

Step4:设置对象头

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

Step5:执行 init 方法

在上面的工作都完成后,从虚拟机来说这个对象已经创建完成了,但从 Java 程序的角度来说,对象创建才刚刚开始,init 方法还没有执行,所有的字段都还是零,所有一般来说执行完 new 指令后执行 init 方法,按照程序的设置进行对象的初始化操作。

6.3.2 对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充:

Hotspot 虚拟机的对象头包括两部分信息,第一部分是用户存储对象自身运行时数据(哈希码、GC 分代年龄、锁标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据是对象真正存储的有效信息,是程序中定义的各种类型的字段内容。

对齐填充部分不是必须存在的,也没用什么特殊含义,仅仅起占位的作用。因为 Hotspot 虚拟机自动内存管理系统要求对象的起始地址必须是 8 字节的整数倍,也就是对象的大小必须是 8 字节的整数倍,因此对象的大小不够 8 字节的整数倍时,通过这部分来补齐。

6.3.3 对象的访问定位

Java 通过栈的引用来操作堆中的对象。对象的访问方式由虚拟机的实现方式来确定,目前主流的访问方式有使用句柄和直接指针:

6.4 说一下类装载的执行过程?

6.5 什么是双亲委派机制?

6.6 垃圾回收时如何确定垃圾?

6.7 什么是 GC Roots?

6.8 谈谈对 OOM 的理解?

6.9 说一下 JVM 有哪些垃圾回收算法?

6.10 说一下 JVM 有哪些垃圾回收器?

6.11 详细介绍一下 CMS 垃圾回收器?

6.12 详细介绍一下 CMS 垃圾回收器?

6.13 谈谈内存分配策略?

6.14 为什么要采用分代收集?

7 MySQL

7.1 数据库三大范式?

逻辑架构

7.2 MySQL存储引擎MyISAM与InnoDB区别

什么是数据库事务

数据库的四大特性

原子 一致 隔离 持久

什么是脏读?幻读?不可重复读?

什么是事务的隔离级别?默认的隔离级别是什么?

读未提交 读已提交 可重复读 串行化

多版本并发控制

trx_id

roll_pointer 老版本写入 undo 日志

readview

什么是最左前缀原则?什么是最左匹配原则?

B 和 B+ 区别?

为什么 InnoDB 选用 B+ 树?

B 树的每个节点都存储数据,而 B+ 树只有叶子节点才存储数据,所以在查询相同数据量的情况下,B 树的高度更高,IO 更频繁。数据索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每个磁盘页。

什么是聚簇索引?什么是非聚簇索引?

聚簇索引的叶子节点就是数据节点,非聚簇索引的叶子节点还是索引节点。

什么是覆盖索引?

如果一个索引包含了查询语句中的条件和字段的数据叫做覆盖索引,具有以下优点:

  • 索引通常远远小于数据行,只读取索引能减少数据访问量
  • 一些存储引擎在内存中只缓存索引,而数据依赖于操作系统来缓存
  • 对于 InnoDB 引擎,若辅助索引能覆盖查询,则无需访问主键索引

什么是索引回表?

什么是索引下堆?

在索引遍历过程中,对索引中包含的字段先做判断,过滤不符合记录的记录,减少回表的次数

主从复制的原理?

8 Redis

Redis 是单进程的吗?

Redis 为什么这么快?

常用的数据结构和应用场景?

Redis6.0 之后为何引入了多线程?

Redis是如何判断数据是否过期的呢?

过期的数据的删除策略了解么?

Redis 内存淘汰机制了解么?

Redis 持久化机制

Redis 事务

缓存穿透、缓存击穿、缓存雪崩的区别及解决方案?

布隆过滤器用过吗?讲讲?

9 Spring

谈谈自己对于 IOC 和 AOP 的理解

Bean 的生命周期?

BeanFactory、FactoryBean 和 ApplicationContext 的区别?

Spring 事务中哪几种事务传播行为?

@Transactional(rollbackFor = Exception.class)注解了解吗?

Spring MVC 的处理流程?

Spring Boot 自动配置原理?

10 MyBatis

#{} 和 ${} 的区别是什么?

通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

MyBatis 是如何进行分页的?分页插件的原理是什么?

一级缓存和二级缓存?


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM