java高频面试题

java基础

1、== 和 equals的区别

== 和 equals的区别

2、String、StringBuffer与StringBuilder之间区别
String、StringBuffer与StringBuilder之间区别

3、List和Set的区别
List:有序,按添加的顺序来保存数据;数据可以重复。可以使用Iterator进行遍历数组元素;还可通过get(i)方法进行遍历
Set:无序,数据不可重复;只能使用Iterator进行遍历数组元素

面向对象

面试题:谈谈你对面向对象的理解?

对比面向过程,面向对象是两种不同的处理问题的角度。
面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象),及各自需要做些什么

举例说明:洗衣机洗衣服

面向过程:会将任务拆解成一系列的步骤(函数),1、打开洗衣机—》2、放衣服—》3、放洗衣粉—》4、清洗—》5、烘干

面向对象:会拆分出人和洗衣机两个对象。

人:1、打开洗衣机—》2、放衣服—》3、放洗衣粉

洗衣机:1、清洗—》2、烘干

面向对象三大基本特性:封装、继承、多态

谈谈你对面向对象的理解?

面向对象的简单理解

封装

封装(encapsulation)有时也称数据隐藏,是处理对象的一个重要概念。

从表面看,封装就是把数据和行为组合到一个类中,并对外只暴露它的功能,但隐藏具体的实现方式。相当于给调用类的对象赋予了“黑盒子”特征。

在类中:数据称之为属性,操作数据的过程称为方法。作为类的一个实例,特定对象都有特定的属性。这些集合就是当前对象的状态(state)。无论何时,只要在对象上调用一个方法,它的状态就可能发生改变。

实现封装的关键在于,绝对不能让类中的方法直接访问其它类的数据。程序只能通过对象方法和对象数据进行交互。封装给予对象“黑盒子”的特征,是提高重用性和安全性的关键。在类里面完全可以改变数据存储方式,例如给数据加上static修饰,数据存储的位置就会移到元空间,但只要使用同样的方法操作数据,对象就不会知道也不用关心这个类发生的变化。

举例:使用洗衣机时,我们不用关心洗衣机的内部结构及实现原理,只需要通过它提供的按钮来操作。

继承

类的另一个特性能让Java看起来更简单,就是可以通过扩展其他类来构建新类,这称之为继承(inheritance)。在扩展一个类时,扩展出的新类继承被扩展的类的全部属性和方法。你只需要在新类中提供合适这个新类的新方法和新属性就够了。

事实上,在Java中所有的类,都继承于一个超级父类,它就是Object类。所有类都扩展自这个Object类。对于继承关系,被继承的称为父类,继承的称为子类。

多态

类中有父类和子类之称,子类创建的对象,也是父类型的一种形态。这就是对象的多态性

扩展:方法的重载和重写也是多态的一种,不过是方法的多态。可以理解为相同方法名称的多种形态。

  • 重载:一个类中方法多态性的体现
  • 重写:子父类中方法多态性的体现

多态在编程实操中体现在对对象的转型。由于对象存在多态性,所以可以进行转型操作。

而转型分为两种:
1、向上转型:将子类对象变为父类对象
java语法:Animal animal = new Dog();
注意:子类(范围小)可以传递给父类(范围大)。但是父类传递给子类则不行,否则会溢出,除非在传的时候进行范围限定。

2、向下转型:将父类对象传给子类对象
java语法:Dog dog= (Dog) animal;

Java:多态的概念和案例实现

重载和重写的区别

重写子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写是发生在类的继承关系,或者类的实现关系中的,重写后的方法和原方法需要保持完全相同的返回值类型、方法名、参数个数以及参数类型,简单来说,就是子类重写的父类的方法必须和父类的方法保持完全一致。返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。若父类方法的访问修饰符为private,则子类不能重新该方法。

重载(overloading) 是在一个类里面,方法名字相同、参数类型不同。返回类型和权限修饰符可以相同也可以不同,发生在编译时

// 如下代码,编译会错误
public int add(int a,int b){
     return 0;
 }
public String add(int a,int b){
     return "0";
 }

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载,比如在ThreadPoolExecutor线程池的实现类中,可看到如下的重载方法。

重载和重写的区别

面向对象的详解,只看这一篇就够了

抽象类和接口的区别

  • 抽象类可以存在普通成员方法;而接口一般只存在 public abstract 方法(抽象方法,没有方法体),若是一定要有方法体,则需要添加default关键字
  • 抽象类中的成员变量可以不被public static final修饰;接口中的成员变量只能被static final修饰
  • 抽象类只能继承一个(java的单继承);接口可以实现多个

接口的设计目的是对类的行为进行约束(更准确的说是一种"有"约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制

抽象类的设计目的是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是is a的关系,比如:BMw is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是like a的关系。比如: Bird like a Aircraft(像飞行器一样可以飞),但其本质上is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。

抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度

集合

java高频面试题_第1张图片

前言

时间复杂度
时间复杂度是用来来评估代码的执行耗时的,大O表示法:不具体表示代码的真正执行时间,而是表示代码执行时间随数据规模增长的变化趋势。
当n很大时,低阶、常量、系数并不能影响其增长趋势,因此可以忽略
java高频面试题_第2张图片

List

ArrayList

list底层是数组实现,数组是一种连续内存空间存储相同数据类型数据的线性结构。

在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是**:数组的首地址+索引乘以存储数据的类型大小**
a[i] = baseAddress + i * dataTypeSize

1、随机查询(根据索引查询)
数组元素的访问是通过索引来访问的,计算机通过数组的首地址和寻址公式能够很快速的找到想要访问的元素

随机(通过下标)查询的时间复杂度是O(1)

2、未知索引查询
查找元素(未知下标)的时间复杂度是O(n)
查找元素(未知下标但排序)通过二分查找的时间复杂度是O(logn)

插入和删除的时间复杂度
插入和删除时,为了保证数组的内存连续性,需要移动数组元素,最好的情况为O(1),最坏的情况为O(n),平均时间复杂度为O(n)

ArrayList的扩容机制
ArrayList底层是基于动态数组实现的
1、ArrayList的初始容量为0,当第一次添加数据时才会初始化容量为10
2、ArrayList在进行扩容时会扩容到原来容量的1.5倍,每次扩容时需要拷贝数组
3、ArrayList在添加数据时

  • 确保数组已使用长度(size)加1之后足够存下下一个数据
  • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
  • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
  • 返回添加成功布尔值。

ArrayList的扩容机制

ArrayList源码分析

面试题
List list = new ArrayList<>(10);扩容了几次
ArrayList构造方法

public ArrayList(int initialCapacity) {
		//  若指定大于0的初始化容量,直接new出对象数组
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        //  若指定为0的初始化容量,那么elementData属性为空
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+                                         initialCapacity);
        }
    }

答案:没有扩容

如何实现数组和List之间的转换
数组转List,调用Array.asList(arr)方法
List转数组,调用list.toArray(new DataType(list.size())) 返回该对象数组
调用list.toArray() 返回Object数组

用Arrays.asList转List后,如果修改了数组内容,list受影响吗
asList方法

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    /**
     * @serial include
     */
    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
        ....
     }

答案:受影响
分析:Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址

List用toArray转数组后,如果修改了List内容,数组受影响吗
toArray方法

 public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

答案:不受影响
分析:list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响

LinkedList

单向链表
单向链表∶每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。记录下个结点地址的指针叫作后继指针next

  • 链表的每个元素称之为结点
  • 物理存储单元是不连续的

双向链表

  • 每个结点不止有一个后继指针next 指向后面的结点
  • 有一个前驱指针prev指向前面的结点

高频面试题:ArrayList和LinkedList的区别
总结如下:

  • ArrayList底层使用了动态数组实现,实质上是一个动态数组
  • LinkedList底层使用了双向链表实现,可当作堆栈、队列、双端队列使用
  • ArrayList在随机查询方面效率高于LinkedList
  • LinkedList在节点的增删方面效率高于ArrayList
  • ArrayList必须预留一定的空间,当空间不足的时候,会进行扩容操作
  • LinkedList的开销是必须存储节点的信息以及节点的指针信息

Map

前言

二叉树

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
二叉树每个节点的左子树和右子树也分别满足二叉树的定义。

常见的二叉树:

  • 满二叉树
  • 完全二叉树
  • 二叉搜索树
  • 红黑树

二叉搜索树

二叉搜索树(Binary Search Tree,BST)又名二叉查找树,有序二叉树或者排序二叉树,是二叉树中比较常用的一种类型二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值

红黑树
红黑树(Red Black Tree):也是一种自平衡的二叉搜索树(BST),之前叫做平衡二叉B树(Symmetric Binary B-Tree)

红黑树性质:

  • 性质1:节点要么是红色,要么是黑色
  • 性质2:根节点是黑色
  • 性质3:叶子节点都是黑色的空节点
  • 性质4:红黑树中红色节点的子节点都是黑色
  • 性质5:从任一节点到叶子节点的所有路径都包含相同数目的黑色节点

红黑树原理

散列表

散列(Hash)表

HashMap实现原理

HashMap底层使用hash表数据结构,即数组、链表(jdk1.7及之前)或红黑树(jdk1.8)

1、当往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
2、存储时,若出现hash值相同的key,此时有两种情况:

  • 若key相同,覆盖旧值

  • 若key不同,则将当前的k-v值放入链表或红黑树中
    3、当调用get方法时,直接找到hash值对应的下标,再判断key是否相同,从而找到对应的value。
    java高频面试题_第3张图片
    面试题:HashMap在jdk1.7和jdk1.8中有什么区别

    在JDK1.6,JDK1.7中,HashMap采用位桶(数组)+链表实现,即使用链表处理冲突,同一hash值的键值对会被放在同一个位桶里,当桶中元素较多时,通过key值查找的效率较低。
    而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(默认为8)且数组长度超过64时,将链表转换为红黑树,这样大大减少了查找时间。当扩容resize()方法时,红黑树拆分成的树节点数小于或等于临界值6时,红黑树将退化成链表。

HashMap的put方法执行流程

java高频面试题_第4张图片HashMap的put方法执行步骤
1、判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
2、根据键值key计算hash值得到数组索引
3、判断table[i]==null,条件成立,直接新建节点添加
4、如果table[i]==null ,不成立

  • 4.1、判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
  • 4.2、判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对
  • 4.3、遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value

5、插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。

HashMap的扩容机制

HashMap扩容流程
java高频面试题_第5张图片
面试回答:请说一下HashMap的扩容机制

1、在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度*0.75)
2、每次扩容的时候,都是扩容之前容量的2倍;
3、扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中

  • 没有hash冲突的节点,则直接使用e.hash & (newCap - 1)计算新数组的索引位置
  • 如果是红黑树,走红黑树的添加
  • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为O,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

HashMap的寻址算法

代码

public V put(K key, V value) {
   return putVal(hash(key), key, value, false, true);
}
// 扰动算法:key的hashCode和key的hashCode右移16位后进行异或操作
// 使hash值更加均匀的分布在数组的各个下标,减少hash冲突
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
     ....
     // (n - 1) & hash :得到数组索引,使用&运算符取代%,效率更高
     // 前提是数组长度必须是2的n次方
     if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
     ....
 }

面试回答:

  • 先计算key的hasCode
  • 再调用hash方法对key进行二次哈希(key的hashCode和key的hashCode右移16位后进行异或操作)
  • 最后(capacity - 1)& hash 得到数组下标

HashMap的数组长度一定是2的n次幂?

面试回答:
1、计算索引效率更高,如果数组长度是2的n次方 可以使用位与运算来取代取模(%)运算
2、当HashMap扩容时重新计算索引的效率更高,hash & oldCap = 0 的元素保留在原来的位置,否则要移到新的位置,新位置计算方式 = 旧位置 + oldCap

更多解析
HashMap底层实现原理概述
HashMap实现原理
HashMap实现原理分析

HashMap在jdk1.7下多线程死循环问题

参考回答
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
线程二∶也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。
JDK 8将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),使用尾插法,就避免了jdk7中死循环的问题。

HashMap在jdk1.7下多线程死循环问题

ConCurrentHashMap

ConCurrentHashMap是一种线程安全的HashMap

底层数据结构
JDK1.7底层采用分段的数组+链表实现
java高频面试题_第6张图片

JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表,采用CAS + synchronized来保证并发安全

  • 数组节点添加使用CAS来控制
  • synchronized只锁定当前链表或红黑树的首节点(根节点),只要hash不冲突,就不会有并发问题,性能更好

如图
java高频面试题_第7张图片

多线程

java高频面试题_第8张图片线程和进程的区别?

  • 本质区别进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

  • 包含关系:一个进程至少有一个线程,线程是进程的一部分。

  • 资源开销:每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。

  • 影响关系:一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。

更多详解:线程与进程,你真得理解了吗

并行和并发的区别
现在都是多核CPU,在多核CPU下

  • 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
  • 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程

并行和并发的区别

创建线程的方式有哪些

  • 继承Thread类
  • 实现Runnable接口(无返回值)
  • 实现Callable接口(返回值)
  • 使用线程池
    ExecutorService executor = Executors.newFixedThreadPool(10);

start方法和run方法的区别
start方法用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次
run方法封装了要被线程执行的代码,可以被调用多次。

线程包括哪些状态,状态之间是如何变化的?

  • NEW (线程新建尚未启动状态)
  • RUNNABLE (线程可运行状态)
  • BLOCKED (线程堵塞等待监视器锁状态)
  • WAITING (线程等待状态)
  • TIMED_WAITING (具有等待时间线程等待状态)
  • TERMINATED(线程已终止状态)

线程状态变化图解
java高频面试题_第9张图片线程状态之间是如何变化的

  1. 创建线程对象是新建状态
  2. 调用了start()方法转变为可执行状态
  3. 线程获取到了cpu的执行权,执行结束是终止状态
  4. 在可执行状态的过程中,如果没有获取cpu的执行权,可能会切换其他状态
  • 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
  • 如果线程调用了wait()方法进入等待状态,其他线程调用**notify()**唤醒后可切换为可执行状态
  • 如果线程调用了**sleep(50)**方法,进入计时等待状态,到时间后可切换为
    可执行状态

新建t1、t2、t3三个线程,如何保证线程顺序执行?
方法1:使用join方法,此方法作用是等待线程运行结束。调用此方法的线程会进入timed_waiting直到线程执行完成后,此线程再继续执行
java高频面试题_第10张图片
如上述代码,在t2线程里调用t1的join方法,在t3线程里调用t2的join方法。

更多方法如下
线程顺序执行的8种方法,最后一种你用过吗?

notify方法和notifyAll方法的区别?
notify方法只会随机唤醒一个线程;notifyAll方法会唤醒所有wait中的线程

wait方法和sleep方法的区别?

共同点:wait() 、wait(long) 和sleep(long) 都可以让当前线程暂时放弃CPU的使用权,进入堵塞状态

不同点

1、方法所属不同

  • sleep(long) 是Thread的静态方法
  • wait() 、wait(long) 是Object的成员方法

2、醒来的时机不同

  • 执行wait(long) 和 sleep(long) 的线程都会等待相应的毫秒数后醒来
  • wait() 、wait(long) 可以被notify方法唤醒,wait() 如果不被唤醒,那么会一直等待
  • 都可以被打断唤醒

如何停止一个正在运行中的线程
有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止,这种方法需要考虑线程安全问题
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程
    • 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
    • 打断正常的线程,可以根据打断状态来标记是否退出线程

3、锁特性不同 ☆

  • wait方法调用必须先获取到wait对象的锁,sleep方法则不需要
  • wait方法执行后会释放对象锁,允许其他线程获取该对象锁(当前线程放弃CPU的使用权,但其他线程还能使用),wait方法必须和synchronized代码块一起使用
  • 若sleep方法若在synchronized代码块中执行,不会释放对象锁(当前线程放弃CPU的使用权,其他线程不能使用)

synchronized关键字
Synchronized作用:Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
Monitor
意为监视器,由jvm提供,c++实现
java高频面试题_第11张图片Owner:存储当前线程获取锁的线程,owner里只能有一个线程
EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
WaitList:关联了调用wait方法的线程,处于Waiting状态的线程

Monitor实现的锁属于重量级锁,你了解过锁升级吗?
Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的MarkWord中就被设置指向Monitor 对象的指针。

轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
如下场景,同一个线程重入,所以不存在锁竞争
java高频面试题_第12张图片轻量级加锁流程
java高频面试题_第13张图片
1、在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2、通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3、如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4、如果CAS修改失败,说明发生了竞争,需要升级为重量级锁。

解锁流程
1、遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2、如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3、如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则升级为重量级锁。

偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

java高频面试题_第14张图片一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。

java高频面试题_第15张图片MarkWord
java高频面试题_第16张图片
总结
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

重量级锁
底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁
线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是cAs操作,保证原子性
偏向锁
一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

一旦发生锁竞争,无论轻量级锁还是偏向锁都会升级成重量级锁

Java内存模型

JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

java高频面试题_第17张图片

CAS
CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
在JUC ( java.util.concurrent )包下实现的很多类都用到了CAS操作

  • AbstractQueuedSynchronizer (AQS框架)
  • AtomicXXX类

CAS图解
第一步
java高频面试题_第18张图片第二步
java高频面试题_第19张图片
第三步
java高频面试题_第20张图片
自旋:重新读取共享变量V,再比较旧的预期值A和内存值V

java高频面试题_第21张图片

解析:
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功

优缺点:
优点:没有加锁,因此线程不会堵塞,效率较高
缺点:如果竞争激烈,会不断自旋,效率会下降

CAS底层实现
CAS底层是调用Unsafe类中的方法,由操作系统提供的,c/c++实现

更多详解:
CAS思想
乐观锁和悲观锁的区别

请谈谈你对volatile的理解
一个共享变量(成员变量、静态变量)被volatile修饰后会有以下特性

  • 保证线程间的可见性
  • 禁止进行指令重排序
    用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。

解析:被volatile修饰的变量,那么可以防止即时编译器(JIT)对该变量进行优化
;让一个线程对共享变量的修改对另一个线程可见
java高频面试题_第22张图片解决方案

java高频面试题_第23张图片volatile使用技巧

  • 写操作时让volatile修饰的变量在代码的最后面
  • 读操作时让volatile修饰的变量在代码的最前面

什么是AQS?
全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架

AQS常用实现类

  • ReenTrantLock (堵塞式锁)
  • Semaphore (信号量)
  • CountDownLatch (倒计时锁)

AQS与Synchronized的区别

Synchronized AQS
关键字 c++实现 java实现
悲观锁,自动释放锁 悲观锁,手动开启和释放
锁竞争激烈情况下都是重量级锁,性能差 锁竞争激烈情况下,提供了很多解决方案

AQS执行原理
AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是O(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源。在对state修改的时候使用cas操作时保证了多线程修改的情况下原子性
java高频面试题_第24张图片

ReenTrantLock

ReenTrantLock为可重入锁,相对于synchronized有以下特点

  • 可中断
  • 可以设置超时时间
  • 可设置公平锁
  • 与synchronized一样可重入
  • 支持多个条件变量

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

ReentrantLock构造方法
java高频面试题_第25张图片

ReenTrantLock的实现原理
java高频面试题_第26张图片

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

什么是死锁

死锁,是指多个线程同时被阻塞,其中一个或者全部线程都在等待某个资源,由于资源争夺而造成的一中僵局。若无外力推进,他们都将无法推进。由于无限期的阻塞,程序没有办法进行正常终止。

死锁产生的条件

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
  • 不可剥夺,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了环路等待。

检查死锁的方法
1、jps 和 jstack命令

  • 命令行输入jps查看JVM运行的进程状态信息
  • 命令行输入jstack -l 进程id 查看java进程内线程的堆栈信息
    java高频面试题_第27张图片

2、jdk自带死锁诊断工具(java安装目录下的bin目录下)
1、jconsole.exe :用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能监控工具
2、VisualVM.exe:故障处理工具,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈

更多详解
死锁基本介绍

死锁的成因和对应的解决方案

面试题:导致并发程序出现问题的根本原因是什么(Java程序中怎么保证多线程的执行安全)
答案:悲观锁,

java并发编程的特性

  • 原子性
    一个线程在cpu中的操作不可暂停,不可中断;要么全部执行完成,要么都不执行,加锁,synchronized锁或者ReenTrantLock类的lock方法
  • 可见性
    内存可见性:让一个线程对共享变量的修改对另一个线程可见;对共享变量加上volatile关键字
  • 有序性
    指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,对共享变量加上volatile关键字

线程池

ThreadPoolExecutor线程池的核心参数

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {}
  • corePoolSize:核心线程数量,即便是线程池里没有任何任务,也会有corePoolSize个线程在等待任务
  • maximumPoolSize:最大线程数量(核心线程数量 + 救急线程的最大数量);超过此数量,会触发拒绝策略。
  • keepAliveTime:线程的存活时间;生存时间 - 救急线程的生存时间;该生存时间内若没有新任务,该线程资源会被释放
  • unit:时间单位, 救急线程的生存时间单位,如TimeUnit.SECONDS等
  • workQueue 当没有空闲核心线程时,新来任务会加入到此(堵塞)队列排队,队列满会创建救急线程执行任务
  • threadFactory线程工厂-可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  • handler:拒绝策略,当所有线程没有空闲且工作队列也满了,会触发拒绝策略

拒绝策略如下:
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;

java高频面试题_第28张图片

更多详解:Java线程池系列–核心参数/大小设置/使用示例

线程池常用的堵塞队列

workQueue -当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
1、ArrayBlockingQueue基于数组结构的有界阻塞队列,FIFO
2、LinkedBlockingQueue基于链表结构的有界阻塞队列,FIFO
3、DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4、SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

LinkedBlockingQueue ArrayBlockingQueue
默认无界(数组长度),支持有界 强制有界
底层是链表 底层是数组
是懒惰的,创建节点时添加数据 提前创建Node数组
入队时添加新Node 提前创建Node
两把锁(头尾) 一把锁

java高频面试题_第29张图片如何确定核心线程数
IO密集型任务:如文件读写、DB读写、网络请求等;核心线程数大小设置为2n+1
CPU密集型任务:如计算型代码、bitmap转换、gson转换;核心线程数大小设置为n+1

线程池的种类
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种1、创建使用固定线程数的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

核心线程数和最大线程数一样,没有救急线程
LinkedBlockingQueue队列的最大容量为Integer.MAX_VALUE

2、单线程线程池,它只有一个线程来执行任务,保证所有任务按照顺序执行(FIFO 先进先出)

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

核心线程数和最大线程数都是1
LinkedBlockingQueue队列的最大容量为Integer.MAX_VALUE

3、可缓存线程池

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

核心线程数为0
最大线程数是Integer.MAX_VALUE
SynchronousQueue:不存储元素的堵塞队列,每个插入操作必须等待一个移出操作

适用于任务数比较密集但每个任务执行时间较短的场景

4、延迟和周期执行的线程池

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), handler);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }

为什么不建议Executors创建线程池

参考阿里开发手册《Java开发手册-嵩山版》
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下∶
1 ) FixedThreadPool和SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2 ) CachedThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM.

如何控制某个方法允许并发访问线程的数量

Semaphore 信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量使用场景。
通常用于那些资源有明确访问数量限制的场景,常用于限流。
Semaphore使用步骤

创建Semaphore对象,可以给一个值
semaphore.acquire()方法请求一个信号量,这时候的信号量个数-1 (一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
semaphore.release()方法释放一个信号量,此时信号量个数+1

谈谈你对ThreadLocal的理解

ThreadLocal是多线程中对于解决线程安全问题的一个操作类,它会为每个线程分配一个独立的线程副本从而解决了变量并发访问冲突问题(线程数据隔离),同时实现了线程内的资源共享。

案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。

ThreadLocal原理
ThreadLocal本质是一个线程内部类,从而让多个线程只操作自己内部的值,从而实现线程间数据隔离

java高频面试题_第30张图片
set方法源码解析

public void set(T value) {
   // 获取当前线程对象
   Thread t = Thread.currentThread();
   // 根据当前线程对象获取ThreadLocalMap
   ThreadLocalMap map = getMap(t);
   // 若map不为空,那么map已经初始化过,然后保存数据
   if (map != null)
       map.set(this, value);
   // 否则创建map并保存数据
   else
       createMap(t, value);
}

void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 内部成员数组INITIAL_CAPACITY = 16
    table = new Entry[INITIAL_CAPACITY];
    // 位与运算,结果与取模相同,计算出存放数据的下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

get方法源码解析

public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 根据当前线程对象获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    	 // 获取ThreadLocalMap对应的entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取entry中的value
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

private Entry getEntry(ThreadLocal<?> key) {
    // 位与运算,结果与取模相同,计算出存放数据的下标
    int i = key.threadLocalHashCode & (table.length - 1);
    // 获取该位置上的entry
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

ThreadLocal内存泄露问题
Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用
强引用:最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收。new的方式

弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收

// 强引用
User user = new User();
// 弱引用
WeakReference weakReference = new WeakReference(user);

每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量的副本
java高频面试题_第31张图片

JVM

JVM组成

JVM有哪些部分组成,运行流程是什么?

java高频面试题_第32张图片

  • 类加载器:用于装载字节码文件(.class文件)
  • 运行时数据区:用于分配存储空间
  • 执行引擎:执行字节码文件或本地方法
  • 垃圾回收器:用于对JVM中的垃圾内容进行回收
  • 程序计数器:线程私有的,保存的是字节码的行号。用于记录正在执行的字节码指令的地址。

什么是堆?
线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

jdk7—JVM内存结构
java高频面试题_第33张图片

jdk8—JVM内存结构

java高频面试题_第34张图片
年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
老年代主要保存生命周期长的对象,一般是一些老的对象

老年代主要保存生命周期长的对象

元空间保存类的信息、静态变量、常量、编译后的代码

jdk7和jdk8—JVM内存结构的区别

  • jdk7有一个永久代,保存类的信息、静态变量、常量、编译后的代码
  • jdk8移除了永久代,把类的信息、静态变量、常量、编译后的代码等数据保存到本地内存的元空间中,防止内存溢出

java虚拟机栈

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(frame)组成,对应每次调用方法所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

java高频面试题_第35张图片
问题1:垃圾回收是否涉及到栈内存

垃圾回收主要指的是堆内存;当栈帧弹出栈后,内存就会释放

问题2:栈内存分配的越大越好吗

不一定,默认的栈内存是1M
栈帧过大会导致线程数变少,例如,机器的总内存为512M,那么能活动的线程数有512个,若把栈内存修改为2M,那么能活动的栈帧将减半

问题3:方法内的局部变量是线程安全的吗

  • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

问题4:哪些情况会导致栈溢出

  • 栈帧过多导致栈内存溢出,典型的如递归调用
  • 栈帧过大导致栈内存溢出

问题5:堆和栈的区别

栈内存一般会用来存储局部变量和方法调用(栈帧);而堆内存是用来存储对象和数组的。堆会被gc,栈不会
栈内存是线程私有的;堆内存是线程共享的
栈空间不足异常: java.lang.StackOverFlowError。
堆空间不足异常: java.lang.OutOfMemoryError。

方法区
方法区是各个线程共享的内存区域,主要存储类的信息、运行时常量池,虚拟机启动时创建,虚拟机关闭时释放;若方法区中的内存无法满足分配请求时,会抛出OutOfMemoryError: Metaspace。
java高频面试题_第36张图片

直接内存
常规IO的数据拷贝流程
java高频面试题_第37张图片
NIO的数据拷贝流程
java高频面试题_第38张图片

类加载器

什么是类加载器,有哪些?

JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

java高频面试题_第39张图片
类加载器及作用

  • 启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库
  • 扩展类加载器(ExtClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类
  • 应用类加载器(AppClassLoader):用于加载classPath下的类
  • 自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。

什么是双亲委派模型?

双亲委派模型:加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类。
(向上查找,向下加载),通俗来说是啃老机制

JVM为什么采用双亲委派机制

  • 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
  • 为了安全,保证类库API不会被修改

类装载的执行流程
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了︰加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)
在这里插入图片描述
加载阶段

  • 通过全类名获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  • 创建java.lang.Class的实例,表示该类型。作为方法区这个类的各种数据的访问入口。

java高频面试题_第40张图片
验证阶段:验证类是否符合JVM规范,安全性检查

java高频面试题_第41张图片连接阶段:为类变量分配内存并设置初始值

  • static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
  • static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成
  • static变量是final的引用类型,那么赋值也会在初始化阶段完成

解析阶段: 把类中的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。

初始化阶段:对类的静态变量、静态代码块执行初始化操作

  • 若初始化一个类时,其父类还未初始化,则优先初始化父类
  • 若同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

使用阶段:JVM开始从入口方法开始执行用户的程序代码

  • 调用静态成员信息(变量、方法…)
  • 使用new关键字为其创建对象实例

面试回答(总结):说一下类的执行过程

  • 加载:查找和导入class文件
  • 验证:保证加载类的准确性
  • 准备:为类变量分配内存并设置类变量初始值
  • 解析:把类中的符号引用转换为直接引用
  • 初始化:对类的静态变量,静态代码块执行初始化操作
  • 使用:JVM开始从入口方法开始执行用户的程序代码
  • 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。

java高频面试题_第42张图片

GC垃圾回收

对象什么时候被垃圾器回收?

简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。

如果要定义什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法

  • 引用计数法
    一个对象被引用了一次,就在当前对象头上增加一次引用次数,若这个对象的引用次数为0,代表这个对象可以被回收。若对象间出现了循环引用,那引用计数就会失效。

  • 可达性分析法(主流)

java高频面试题_第43张图片

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。如图x、y对象是可回收的

哪些对象可以作为GC root

  • 虚拟机栈(栈帧中的本地变量表)中的引用对象
  • 方法区中类静态变量引用的对象
  • 方法区常量引用的变量
  • 本地方法栈中JNI(一般是native修饰的方法)引用的对象

JVM垃圾回收算法有哪些?

  • 标记清除算法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
1、根据可达性分析算法得出的垃圾进行标记
2、对这些标记为可回收的内容进行垃圾回收

java高频面试题_第44张图片

  • 标记整理算法

java高频面试题_第45张图片优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。适用于老年代回收器

  • 复制算法

java高频面试题_第46张图片适用于年轻代垃圾回收器

优点:在垃圾对象多的情况下,效率较高;清理后,内存无碎片

缺点:分配两块内存空间,在同一时刻,只能使用一半,内存使用效率低

总结
标记清除算法:垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续
标记整理算法:标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
复制算法:将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低

JVM的分代回收

分代收集算法

在jdk8中堆被分成新生代和老生代
java高频面试题_第47张图片
工作机制

  • 新创建的对象,都会先分配到eden区
  • 当伊甸园内存不足,标记伊甸园与from(现阶段没有)的存活对象
  • 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和from内存都得到释放
  • 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区
  • 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

java高频面试题_第48张图片

JVM有哪些垃圾回收器

串行回收器

本文编写来源b站java面试八股文

你可能感兴趣的:(java,数据结构,面试)