【Java问题面试总结】

一、 Java基础总结

大部分内容部分转自 https://blog.csdn.net/ThinkWon/article/details/104390612,我只是针对大佬的博文进行自己相对常见问题的总结。大家想了解更多可以关注订阅上述链接

目录

1.JVM、JRE和JDK的关系

JVM:java虚拟机,java跨平台
JRE:java所需核心库,主要包含java.lang包
JDK:java开发工具,内涵JRE,提供了java编译工具,打包工具


2.JAVA数据类型

基本数据类型:整、浮、字符、布
引用类型:类、接口,数组
在这里插入图片描述


3.final 有什么用?

只是针对变量的引用
用于修饰类、属性和方法;

  • 被final修饰的类不可以被继承
  • 被final修饰的方法不可以被重写
  • 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的

4.this关键字的用法

指向对象本身的一个指针

  • 引用本类的构造函数
  • this相当于是指向当前对象本身
  • 形参与成员名字重名

5.super关键字的用法

指向的是离自己最近的一个父类

  • 指向当前对象的父类的引用
  • 区分子类和父类方法成员变量重名
  • 引用父类构造函数

6.this和super要注意的地方

  • super()和this()均需放在构造方法内第一行。
  • his()和super()都指的是对象,所以,均不可以在static环境中使用

7.static主要意义

  • 无对象调用方法 即使没有创建对象,也能使用属性和调用方法!
  • 对象共享用static修饰的变量和方法不属于任何一个实例对象,但是可以被所有实例对象共享

8.面向对象、面向过程区别

  • 面向过程:强调性能(Linux、单片机啥的)
  • 面向对象:设计出低耦合的系统,使系统更加灵活、更加易于维护

9.面向对象三大特性

  • 封装 :隐藏细节,提供对外访问方式,安全性和复用性高
  • 继承:以已有类为基础进行非私有拓展
  • 多态:引用变量所指向的具体类型通过该引用变量发出的方法在编程时不确定,只有在程序运行时才确定

10.抽象类和接口的对比

抽象类:模板设计
接口:行为抽象

相似处:

  • 接口和抽象类都不能实例化
  • 都位于继承的顶端,用于被其他实现或继承
  • 都包含抽象方法,其子类都必须覆写这些抽象方法

不同处:

  • 声明方式
  • 构造器
  • 实现
  • 继承与实现
  • 字段声明
    在这里插入图片描述

11.内部类的分类有哪些

  • 成员内部类 :class作为成员变量
  • 局部内部类 :在方法内部声明
  • 静态内部类 :static class
  • 匿名内部类:

12.匿名内部类特点

  • 必须继承一个抽象类或者实现一个接口。
  • 不能定义任何静态成员和静态方法why
  • 所在的方法的形参需要被匿名内部类使用时,必须声明为 final why?生命周期不一致,加了final,可以确保局部内部类使用的变量与外层的局部变量区分开
public class Outer {

    private void test(final int i) {
        new Service() {
            public void method() {
                for (int j = 0; j < i; j++) {
                    System.out.println("匿名内部类" );
                }
            }
        }.method();
    }
 }
 //匿名内部类必须继承或实现一个已有的接口 
 interface Service{
    void method();
}

局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。

public class Outer {
    private int age = 12;

    class Inner {
        private int age = 13;
        public void print() {
            int age = 14;
            System.out.println("局部变量:" + age);
            System.out.println("内部类变量:" + this.age);
            System.out.println("外部类变量:" + Outer.this.age);
        }
    }

    public static void main(String[] args) {
        Outer.Inner in = new Outer().new Inner();
        in.print();
    }

}


13.重载(Overload)和重写(Override)的区别

  • 重载: 同一个类中,方法名相同,参数列表(类型,个数,顺序可不同)
  • 重写:父子类中。方法名、参数列表必须相同。其他任何东西都小于父类(返回值,异常,访问修饰符)

14.== 和 equals 的区别是什么

  • == 和equal一般来说都是比较对象的内存地址
  • == 针对基本数据类型的比较是比较
  • ==针对引用数据类型的比较是比较内存地址
  • 针对String例子中重写了equal方法

15.String中重写的equal方法比较步骤

  • 先比较内存地址
  • 再比较类型是否为String
  • 比较String内容

16.hashCode 与 equals (重要)

  • equal方法被重写时,hashCode方法也必须被重写
  • 如果两个对象相等equal为true,则hashcode一定也是相同的
  • 两个对象有相同的hashcode值,它们也不一定是相等的

17.值传递和引用传递

  • Java程序设计语言总是采用按值调用,方法得到的是所有参数值的一个拷贝

在这里插入图片描述

  • 按引用传递时,虽然传递的是引用的拷贝,但是该拷贝的引用仍指向原来的对象
    在这里插入图片描述

18.BIO,NIO,AIO 有什么区别?

  • BIO (Blocking I/O):Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
  • NIO (New I/O):Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
  • AIO (Asynchronous I/O):Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

19.反射

在程序运行的时候动态获取类对象的信息以及动态调用对象的方法
举例

  • ①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
  • ②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:
    • 1 将程序内所有 XML 或 Properties 配置文件加载入内存中;
    • 2 Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
    • 3 使用反射机制,根据这个字符串获得某个类的Class实例;
    • 4动态配置实例的属性

20.Java获取反射的三种方法

  • 1.通过new对象实现反射机制 getClass
  • 2.通过路径实现反射机制 Class.forName
  • 3.通过类名实现反射机制 类.class
public class Student {
    private int id;
    String name;
    protected boolean sex;
    public float score;
}

public class Get {
    //获取反射机制三种方式
    public static void main(String[] args) throws ClassNotFoundException {
        //方式一(通过建立对象)
        Student stu = new Student();
        Class classobj1 = stu.getClass();
        System.out.println(classobj1.getName());
        //方式二(所在通过路径-相对路径)
        Class classobj2 = Class.forName("fanshe.Student");
        System.out.println(classobj2.getName());
        //方式三(通过类名)
        Class classobj3 = Student.class;
        System.out.println(classobj3.getName());
    }
}

21.String和StringBuffer、StringBuilder的区别是什么?

  • 可变性
    String不可变:原因使用final修饰的字符数组保存字符串(private final char value[])
    StringBufferStringBulider继承AbstractStringBuilder类,都普通使用char[] value保存字符串
  • 线程安全性
    String不可变->参量->线程安全
    StringBuffer有同步锁->线程安全
    StringBuilder无锁->线程不安全
  • 性能
    String每次改变的时候都会生成一个新的String对象,并将原来的指针指向新的String对象
    StringBuffer/StringBulider 只是对StringBuffer/StringBuilder对象本身进行操作,并不会生成新的对象。相同情况下,StringBulider比StringBuffer性能高。

对于三者使用的总结

  • 如果要操作少量的数据用 = String
  • 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据 = StringBuffer

22.Java常见异常有哪些

java.lang.IllegalAccessError违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。

java.lang.InstantiationError实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.

java.lang.OutOfMemoryError内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。

java.lang.StackOverflowError堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。

java.lang.ClassCastException类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。

java.lang.ClassNotFoundException找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。

java.lang.ArithmeticException算术条件异常。譬如:整数除零等。

java.lang.ArrayIndexOutOfBoundsException数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。

(运行时)java.lang.IndexOutOfBoundsException索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。

java.lang.InstantiationException实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。

java.lang.NoSuchFieldException属性不存在异常。当访问某个类的不存在的属性时抛出该异常。

java.lang.NoSuchMethodException方法不存在异常。当访问某个类的不存在的方法时抛出该异常。

(运行时)java.lang.NullPointerException空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。

java.lang.NumberFormatException数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。

java.lang.StringIndexOutOfBoundsException字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。


23.常见的 RuntimeException 有哪些? ClassCastException(类转换异常)

  • IndexOutOfBoundsException(数组越界)
  • NullPointerException(空指针)
  • ArrayStoreException(数据存储异常,操作数组时类型不一致)
  • 还有IO操作的BufferOverflowException异常

24.Java异常处理最佳实践

  • 在finally中清理资源:close
  • 优先明确的异常
  • 对异常进行文档说明
  • 优先捕获最具体的异常
  • 包装异常时不要抛弃原始的异常

二、Java集合总结

1.List,Set,Map三者的区别?

Java 容器分为 Collection 和 Map 两大类在这里插入图片描述

  • List:有序,可重复,可null。ArrayList、LinkedList 和 Vector
  • Set:一个无序,不可重复,只允许存入一个null元素。HashSet、LinkedHashSet 以及 TreeSet)
  • Map:键值对集合,可重复,不为null。HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

2.Java集合的快速失败机制 “fail-fast”?

  • 集合的一种错误检测机制
  • 多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原理如下:
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。


3.遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

  • for循环,用i作计数器,通过get()方法获取集合元素的值。
  • foreach遍历:内部也是采用了 Iterator 的方式实现,不需要显式声明 Iterator 或计数器
  • Iterator迭代器遍历
  • 最佳方式是:支持Random Access用for,不支持则用lterator或foreach

4.迭代器Iterator

Iterator 接口提供遍历任何 Collection 的接口
使用如下:

List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
  String obj = it. next();
  System. out. println(obj);
}

特点如下:

  • 可以边遍历边修改
  • 只能单向遍历,安全性高

5.ArrayList、Vector、LinkedList的区别

ArrayList是动态数组结构的实现,LinkedList是双向链表的数据结构,Vector是线程安全容器

  • 随机访问效率:ArrayList > LinkedList
  • 增加和删除效率:ArrayList > LinkedList
  • 内存空间占用: ArrayList>LinkedList
  • 线程安全:Vector > ArrayList\LinkedList
  • 扩容方面:Vector每次扩容增加1倍,ArrayList每次扩容则增加50%

6.HashSet实现原理

  • 底层使用HashMap实现,将其value作为HashMap的key存储,因此HashSet的value是不可重复的。而HashMap的value则存入PRESENT的虚值
  • 添加元素操作需要进过两层验证:hashcode和equal
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
    map = new HashMap<>();
}

public boolean add(E e) {
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
	return map.put(e, PRESENT)==null;
}

7.HashMap原理

基本实现:

基于Hash算法:在往HashMap中put()存储数据的时候,通过key值计算hashcode,从而获得当前对象的数组下标;

  • 若key的hash值相同,若key相同则覆盖原先的值,若key不同则将该元素放入key-value链表中
  • 若key的hash值不同,则直接存入table数组之中
在JDK1.7时候:
  • HashMap的数据结构为:拉链法:数组+链表
    在这里插入图片描述
  • 数组存放规则:无冲突时,存放数组;冲突时,存放链表

JDK1.8之后

HashMap的数据结构为:拉链法:数组+链表+红黑树
在这里插入图片描述
无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树

HashMap的put方法的具体流程

在这里插入图片描述①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

putVal源代码:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

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

//实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 步骤①:tab为空则创建 
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤②:计算index,并对null做处理  
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 步骤③:节点key存在,直接覆盖value 
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // 步骤④:判断该链为红黑树 
        // hash值不相等,即key不相等;为红黑树结点
        // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 步骤⑤:该链为链表 
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                
                //判断该链表尾部指针是不是空的
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    //判断链表的长度是否达到转化红黑树的临界值,临界值为8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表结构转树形结构
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 步骤⑥:超过最大容量就扩容 
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

HashMap的扩容操作是怎么实现的?(难点!!)

HashMap是怎么解决哈希冲突的

哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象
如何解决:

  • 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均
  • 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快
  • 在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);

8. HashMap 与HashTable 、CurrentHashMap区别、TreeMap

HashMap与HashTable(基本被淘汰)

  • 线程安全:HashMap 是非线程安全的,HashTable 是线程安全的,由于HashTable内部方法基本都进过synchronized修饰

  • 效率:HashMap 要比 HashTable 效率高一点

  • 对Null key 和Null value的支持:HashMap中允许其key唯一为null,允许多个value为null的键;HashTable不允许其键值为null

HashMap与CurrentHashMap

  • 对Null key 和Null value的支持:HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

  • 底层数据结构:ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)

  • 线程安全:HashMap 是非线程安全的,CurrentHashMap是线程安全的,由于CurrentHashMap在JDK1.8之前使用分段锁,而JDK1.8之后使用Node + CAS + Synchronized来保证并发安全进行实现

HashMap 和TreeMap

  • 插入、删除选HashMap,由于TreeMap的底层原理是一颗红黑树的实现

  • 搜索的话使用TreeMap


9.CurrentHashMap原理(难点!!)


10.TreeMap 和 TreeSet 在排序时如何比较元素?

TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。


11.Collections 工具类中的 sort()方法如何比较元素?

第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;


三、Java JVM总结

1.JVM主要组成以其作用

JVM包含两个子系统和两个组件

  • 两个子系统为Class loader(类装载)、Execution engine(执行引擎)
  • 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
    Execution engine(执行引擎):执行classes中的指令。
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

2.Java程序运行机制详细说明

在这里插入图片描述从上图可以看出,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。


3.JVM 运行时数据区

在这里插入图片描述从上图中可以看到,Java 虚拟机规范规定的区域分为以下 5 个部分:

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

4.深拷贝和浅拷贝

  • 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,

  • 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。


5.说一下堆栈的区别?

  • 物理地址
    • 堆的物理地址分配对对象是不连续的,性能慢!!
    • 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的,性能快!!!
  • 内存分别
    • 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定
    • 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定
  • 存放内容
    • 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
    • 栈存放的时局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
  • 程序可见度
    • 堆对于整个应用程序都是共享、可见的
    • 栈只对于线程是可见的。所以也是线程私有

6.对象的访问定位

Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄直接指针 两种方式。

  • 指针: 指向对象,代表一个对象在内存中的起始地址
  • 句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。

句柄访问

Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
在这里插入图片描述

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针

如果使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。

在这里插入图片描述优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。


7.Java内存泄漏

内存泄漏是指不再被使用的对象或者变量一直被占据在内存中,一般来说Java有GC垃圾回收机制,但是java仍热存在内存泄漏的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露

解释说明:尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景


8.简述Java垃圾回收机制

在JVM中,有一个垃圾回收线程(守护者线程),它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。


9.垃圾回收的优点

  • 使java程序员在编写程序时不再考虑内存管理的问题
  • java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。
  • 有效的防止了内存泄露,可以有效的使用可使用的内存

10.垃圾回收器的基本原理是什么

  • 当对象被创建时,GC就开始监控该对象的地址、大小和使用情况
  • GC采用有向图的方式记录和管理堆(heap)中的所有对象
  • 程序员可以手动执行System.gc(),通知GC运行

11.怎么判断对象是否可以被回收

  • 引用计数器法
    为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
  • 可达性分析算法
    从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
    • GC Roots对象可为以下几种:
      • 栈内存中引用的对象
      • 方法区中静态引用和常量引用指向的对象
      • 被启动类(bootstrap加载器)加载的类和创建的对象
      • Native方法中JNI引用的对象。

12.强引用,软引用,弱引用和虚引用

强引用:普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。
弱引用:通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
虚引用:也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。

13.JVM 有哪些垃圾回收算法

标记-清除算法(将标记好的清理)

标记无用对象,然后进行清除回收
它将垃圾收集分为两个阶段:

  • 标记阶段: 从引用根节点开始标记所有被引用的对象
  • 清除阶段: 遍历整个堆,把未标记的对象清除

优点:实现简单,不需要对象进行移动。

缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

在这里插入图片描述


复制算法(转移再清理)

  • 把内存空间划为两个相等的区域,每次只使用其中一个区域
  • 垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中
  • 将当前使用的区域的可回收的对象进行回收

优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
在这里插入图片描述


标记-整理算法

  • 标记阶段:从根节点开始标记所有被引用对象
  • 整理阶段:遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放
    在这里插入图片描述

分代收集算法

根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代

14.JVM 有哪些垃圾回收器

VM中的垃圾收集器主要包括7种,即Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old以及CMS,G1收集器
在这里插入图片描述

  • Serial收集器:

Serial收集器是一个单线程的垃圾收集器,并且在执行垃圾回收的时候需要 Stop The World。虚拟机运行在Client模式下的默认新生代收集器。Serial收集器的优点是简单高效,对于限定在单个CPU环境来说,Serial收集器没有多线程交互的开销。

  • Serial Old收集器:

Serial Old是Serial收集器的老年代版本,也是一个单线程收集器。主要也是给在Client模式下的虚拟机使用。在Server模式下存在主要是做为CMS垃圾收集器的后备预案,当CMS并发收集发生Concurrent Mode Failure时使用。

  • ParNew收集器:

ParNew是Serial收集器的多线程版本,新生代是并行的(多线程的),老年代是串行的(单线程的),新生代采用复制算法,老年代采用标记整理算法。可以使用参数:-XX:UseParNewGC使用该收集器,使用 -XX:ParallelGCThreads可以限制线程数量。

  • Parallel Scavenge垃圾收集器:

Parallel Scavenge是一种新生代收集器,使用复制算法的收集器,而且是并行的多线程收集器。Paralle收集器特点是更加关注吞吐量(吞吐量就是cpu用于运行用户代码的时间与cpu总消耗时间的比值)。可以通过-XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间;通过-XX:GCTimeRatio参数直接设置吞吐量大小;通过-XX:+UseAdaptiveSizePolicy参数可以打开GC自适应调节策略,该参数打开之后虚拟机会根据系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。自适应调节策略是Parallel Scavenge收集器和ParNew的主要区别之一。

  • Parallel Old收集器:

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

15.CMS(Concurrent Mark Sweep)收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,是一种老年代收集器,通常与ParNew一起使用

CMS的垃圾收集过程分为4步:

  • 初始标记:需要“Stop the World”,初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快。
  • 并发标记:是主要标记过程,这个标记过程是和用户线程并发执行的。
  • 重新标记:需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)。
  • 并发清除:和用户线程并发执行的,基于标记结果来清理对象。

16.描述一下JVM加载Class文件的原理机制

类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中

  • 显式装载
    反射通过Class.forName()等方法,显式加载需要的类
  • 隐式装载
    当碰到通过new 等方式生成对象时,调用类装载器加载对应的类到jvm中

17.类加载器有哪些

  • 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库(加载\lib\ext目录或Java. ext.)。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

18.说一下类装载的执行过程(5过程)

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作

19.双亲委派模型

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

20.JVM常用内存调优命令

jps,jinfo,jstack,jmap以及jstat命令

  • jps:主要用来输出JVM中运行进程状态信息,一般使用jps命令来查看进程的状态信息,包括JVM启动参数等。
  • jinfo:主要用来观察进程运行环境参数等信
  • jstack:主要用来查看某个Java进程内的线程堆栈信息。jstack pid 可以看到当前进程中各个线程的状态信息,包括其持有的锁和等待的锁。
  • jmap:用来查看堆内存使用状况。jmap -heap pid可以看到当前进程的堆信息和使用的GC收集器,包括年轻代和老年代的大小分配等
  • jstat:进行实时命令行的监控,包括堆信息以及实时GC信息等。可以使用jstat -gcutil pid1000来每隔一秒来查看当前的GC信息。

常见指令总结

  • jps
jps 
-q:仅输出VM标识符,不包括class name,jar name,arguments in main method 
-m:输出main method的参数 
-l:输出完全的包名,应用主类名,jar的完全路径名 
-v:输出jvm参数 
  • jinfo
jinfo pid可以查看指定进程的运行环境参数
  • jstack
    显示jvm中当前所有线程的运行情况和线程当前状态,以及其当前所占用的锁和等待的锁
jstack -I
  • jmap
jmap -heap 打印堆内存的概要信息,GC使用的算法以及堆的一些配置

在这里插入图片描述

  • jstat 进行实时的分析与监控
jstat -gc 121559 1000 每隔1000ms周期性打印该进程的GC情况

21.如何排查一个线上的服务异常

  • jstack pid查看当前的线程状态,是否存在死锁等关键信息
  • jstat -gcutil pid查看当前进程的GC情况
  • jmap -heap pid查看当前进程的堆信息

四、Java 多线程总结

1.并行和并发有什么区别

  • 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
  • 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
  • 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。

2.形成死锁的四个必要条件是什么 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放

  • 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
  • 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

3.创建线程的四种方式

  • 继承 Thread 类;
  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 使用 Executors 工具类创建线程池

继承Thread类

步骤

  • 定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法就是线程要执行的业务逻辑方法
  • 创建自定义的线程子类对象
  • 调用子类实例的star()方法来启动线程
public class MyThread extends Thread {
    @Override
    public void run() {
  		System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
    }
}

public class TheadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); 	
        myThread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }
}


实现 Callable 接口

步骤

  • 创建实现Callable接口的类myCallable
  • 以myCallable为参数创建FutureTask对象
  • 将FutureTask作为参数创建Thread对象
  • 调用线程对象的start()方法
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
        return 1;
    }

}

public class CallableTest {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("返回结果 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

实现 Runnable 接口

步骤

  • 定义Runnable接口实现类MyRunnable,并重写run()方法
  • 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
  • 调用线程对象的start()方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}
public class RunnableTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

使用 Executors 工具类创建线程池

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}
public class SingleThreadExecutorTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnableTest);
        }
        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }
}


4.说一下 runnable 和 callable 有什么区别?

RunnableCallable
返回值run()无返回值call()方法有返回值,且是个泛型,和Future和FutureTask配合可以获取异步执行结果
异常捕获run()异常不能捕获处理call()方法允许抛出且可以捕获

5.线程的 run()和 start()有什么区别

  • start()启动一个线程 <=> run()线程逻辑体
  • start()只能调用一次 <=> run()可调用多次
  • 系统调用start()方法之后,无需等待run()方法执行返回
  • 系统调用start()方法,线程处于就绪状态 <=>系统调用run()之后,线程才处于运行状态
  • 若直接调用run()方法会就破坏了线程执行,使run()变成一个普通方法

6.什么是 Callable 和 Future?

  • Callable 表示线程定义的接口,和Runnable对比
  • Future接口相当于异步任务
  • 这两个接口分工合作,Callable接口产生结果,Future接口获取结果

7.什么是 FutureTask

FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作.

8.线程状态

在这里插入图片描述

  • 新建(new):新创建了一个线程对象。
  • 可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
  • 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  • 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
    阻塞的情况分三种:
    • 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    • 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  • 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

9.Java 中用到的线程调度算法是什么?

分时调度模型抢占式调度模型

  • 分时调度模型:让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片
  • 抢占式调度模型:JVM采用的就是抢占式调度模型,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

10.sleep() 和 wait() 有什么区别?

sleepwait
类的从属关系ThreadObject
是否释放锁不释放锁释放锁
用途用于暂停执行线程用于线程之间的交互和通信
唤醒形式自然苏醒不会自然苏醒,别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法

11.线程的 sleep()方法和 yield()方法有什么区别?

  • sleep()方法不考虑线程优先级 <=>yield()方法考虑线程优先级,给优先级相同或者更高的线程分配处理器资源
  • sleep()方法会使线程进入阻塞状态 <=> yield()方法回事线程转入就绪状态

12.interrupted 和 isInterrupted 方法的区别?

  • interrupt方法可以用于中断线程,并将线程状态设置为“中断”状态
    这里的中断状态并不是线程固有的状态,因此并不会正真地将线程中断,而是抛出中断异常(interruptedException),需要用户自己去处理
  • interrupted方法是静态方法,用于查看当前线程中断信号的true/false

13. notify() 和 notifyAll() 有什么区别以及notifyAll的使用原理

  • notify()方法只会唤醒一个线程 <=> notifyAll()会唤醒所有线程
  • notifyAll() 调用后,会将全部线程由等待池移到锁,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池,等待锁被释放后再次参与竞争

14. 为什么wait(), notify()和 notifyAll()被定义在 Object 类里

由于wait、notify、notifyAll这三个方法作用于对象,负责对象加锁等待和唤醒,所以定义在Object类中


15.为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用

这三个方法时针对对象有锁的情况下,所以如果不放在synchronized块或者方法中,在多线程并发的情况下,会导致线程的死锁。


16.同步方法和同步块,哪个是更好的选择

原则:同步的范围越小越好
由于同步块只是锁住目标对象,因此涉及的其他代码逻辑较少,提交并发速度


17.实现线程同步的方法

  • 同步代码方法:sychronized 关键字修饰的方法
  • 同步代码块:sychronized 关键字修饰的代码块
  • 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
  • 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义

18.Java 线程数过多会造成什么异常

  • 线程的生命周期开销非常高
  • 消耗过多的 CPU
  • 降低稳定性JVM

19.为什么代码会重排序?

为了提供性能:处理器和编译器常常会对指令进行重排序,且有以下两个条件:

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序

重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义


20.as-if-serial规则和happens-before规则的区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值