【最新面试问题记录持续更新,java,kotlin,android,flutter】

最近找工作,复习了下java相关的知识。发现已经对很多概念模糊了。记录一下。部分是往年面试题重新整理,部分是自己面试遇到的问题。持续更新中~

Android开发高频面试题之——Java基础篇
Android开发高频面试题之——Kotlin基础篇
Android开发高频面试题之——Android基础篇
Android开发高频面试题之——Flutter基础篇

目录

java相关

1. 面向对象设计原则

  • 单一职责原则——SRP
    一个类的职责尽量单一,清晰。即一个类最好专注做一件事情,而不是分散的做好几件事。
    每个类都只负责一项任务,可以降低类的复杂性;提高可读性;提高系统可维护性;避免类的臃肿和功能太多太复杂。
  • 依赖倒置原则——DIP
    实现时尽量依赖抽象,而不依赖于具体实现。
    可以减少类间的耦合性,提高系统稳定性
    提高代码的可读性,可维护性以及扩展性。
  • 接口隔离原则——ISP
    即应当为客户端提供尽可能小的单独的接口,而不是大而杂的接口。
    也就是要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。依赖几个专用的接口要比依赖一个综合的接口更灵活。
  • 里氏替换原则——LSP
    即超类存在的地方,子类是可以替换的。
    里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
  • 迪米特原则——LOD
    即一个软件实体应当尽可能少的与其他实体发生相互作用。
    一个对象对另一个对象知道的越少越好,即一个软件实体应当尽可能少的与其他实体发生相互作用,在一个类里能少用多少其他类就少用多少,尤其是局部变量的依赖类,能省略尽量省略。
  • 开闭原则——OCP
    面向修改关闭,面向扩展开放。
    即一个软件、一套系统在开发完成后,当有增加或修改需求时,应该对拓展代码打开,对修改原有代码关闭

2. 面向对象的特征是什么

  • 封装
    把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  • 继承
    继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
    • 通过继承创建的新类称为“子类”或“派生类”。
    • 被继承的类称为“基类”、“父类”或“超类”。
    • 继承的过程,就是从一般到特殊的过程。
    • 要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。
  • 多态
    就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。
    实现多态一般通过重写重载

封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用!多态的作用,就是为了类在继承和派生的时候,保证使用“家谱”中任一类的实例的某一属性时的正确调用。

3. 重载和重写

  • 重写
    是指子类重新定义父类的虚函数的做法。需要保持参数个数,类型,返回类型完全一致。
    属于运行时多态的表现
  • 重载
    是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)
    其实,重载的概念并不属于“面向对象编程”,重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)

4. 基本数据类型

数据类型 位数 默认值 取值范围 示例
byte 1字节 0 -2(128~127) byte a = 10;
short 2字节 0 -2(-32768~32767) short a = 10;
int 4字节 0 (-2147483648~2147483647) int a = 10;
float 4字节 0.0 -2(-2147483648~2147483647) float a = 10;
long 8字节 0 -2(-9223372036854774808~9223372036854774807) long a = 10;
double 8字节 0.0 -2(-9223372036854774808~9223372036854774807) char a = 10;
char 2字节 -2(128~127) char a = ‘a’;
boolean 1字节 false true、false booleana = true;
特殊类型void

对应的包装类
byte——Byte
short——Short
int ——Integer
float——Float
long——Long
double——Double
char——Character
boolean——Boolean
void——Void

包装类出现的原因是Java语言是面对对象的编程语言,而基本数据类型声明的变量并不是对象,为其提供包装类,增强了Java面向对象的性质。
void是一个特殊的类型,有人把它归到Java基本数据类型中,是因为可以通过Class.getPrimitiveClass(“void”)获取对应的原生类型。
void有个对应的类型Void,可以把它看做是void的包装类,Void的作用主要作用有以下2个:

  • 泛型占位
    当我们定义了泛型时,如果没有写泛型参数,idea等开发工具会给出提醒,建议写上泛型类型,但实际上却是不需要固定的泛型类型,这时候据可以写上Void来消除警告,例如ResponseData
  • 反射获取void方法
Method[] methods = String.class.getMethods();
for (Method method : methods) {
    if(method.getGenericReturnType().equals(Void.TYPE)){
        System.out.println(method.getName());
    }
}
//输出:
//getBytes
//getChars
//wait
//wait
//wait
//notify
//notifyAll

5. 装箱和拆箱

将基本数据类型转化为包装类就叫做装箱;
调用 包装类.valueOf()方法

     int a = 22;
    //装箱   在实例化时候进行装箱
    Integer inter1 = new Integer(a);
    //装箱  调用valueOf方法进行装箱
    Integer inter2 = Integer.valueOf(a);
    valueOf 方法是一个静态方法,直接通过类进行调用 

拆箱
将包装类转化为基本数据类型;
调用 包装类.parseXXX()方法

int a = Integer.parseInt("3");

6. final 有什么作用

  • final 修饰的类叫最终类,该类不能被继承。
  • final 修饰的方法不能被重写。
  • final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。

7. String是基本类型吗,可以被继承吗

String不是基本类型,不可以被继承。因为String被final关键字修饰,不可以被继承

public final class String implements Serializable, Comparable<String>, CharSequence {
   

8. String、StringBuffer和StringBuilder的区别?

String 大小固定数不可变的。
因为String是字符串常量,是不可变的。实际的拼接操作最后都是产生了一个新的对象并存储这个结果

String str1 = "123";
String str2 = "123"//实际上 "123"这个字符串在常量池中只有一份,str1,str2 两个对象都是指向"123"
str2 = str2 + "45";
实际上是产生了个新的String对象存放"12345"这个结果
查看字节码实际代码是
 0 ldc #2 <123>
 2 astore_1
 3 new #3 <java/lang/StringBuilder>
 6 dup
 7 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
10 aload_1
11 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
14 ldc #6 <45>
16 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
22 astore_1
23 return
//实际也是通过 StringBuilder.append方法实现拼接,然后toString的性能比较低
//所以字符串拼接直接使用StringBuilder实现效率更高

StringBuffer 大小可变,线程安全(有锁),同步,效率低,适用于多线程,低并发。
StringBuilder 大小可变,线程不安全(无锁),不同步,效率高,适用于单线程,高并发。

8. 抽象类和接口的区别

  • 抽象类是对事物属性的抽象,接口是对行为的抽象,是一种规范或者说行为约束。
  • 抽象类关键词abstract 接口关键字interface
  • 抽象类可以有成员变量,普通方法和抽象方法,接口只能有抽象方法(只有方法定义,无具体函数实现)
  • 抽象类可以有构造方法,接口没有构造方法
  • 继承了抽象类的子类,要么对父类的抽象方法进行重写,要么自己也是抽象类
  • 抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
  • 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
  • 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
  • 9. String 类的常用方法都有那些?

  • inexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。
  • equals():字符串比较。
  • 10. Java 中 IO 流分为几种?

    按功能来分:输入流(input)、输出流(output)。
    按类型来分:字节流和字符流。
    字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。

    11. Java容器有哪些

    Java 容器分为 Collection 和 Map 两大类,其下又有很多子类,如下所示:
  • Collection
    • List
      • ArrayList
      • LinkedList
      • Vector
      • Stack
    • Set
      • HashSet
      • LinkedHashSet
      • TreeSet
  • Map
    • HashMap
      • LinkedHashMap
    • TreeMap
    • ConcurrentHashMap
    • Hashtable

12. Collection 和 Collections 有什么区别?

  • Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
  • Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法:Collections. sort(list)。

13. HashMap 和 Hashtable 有什么区别?

  • 存储:HashMap 允许 key 和 value 为 null,而 Hashtable 不允许。
  • 线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。
  • 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

14. 如何决定使用 HashMap 还是 TreeMap?

对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择

15. 说一下 HashMap 的实现原理?

HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。
jdk1.7以前使用数组+链表实现HashMap,jdk1.8以后用数组+红黑树实现。当链表长度较短时使用链表,长度达到阈值时自动转换为红黑树,提高查询效率。

  • 其它问题
  • 默认大小 16 ,负载因子0.75 大小到达12时 自动两倍扩容。

16. equals和 == 的区别

  • 对于 == 来说:
    如果比较的是基本数据类型变量,比较两个变量的值是否相等。(不一定数据类型相同)
    如果比较的是引用数据类型变量,比较两个对象的地址值是否相同,即两个引用是否指向同一个地址值
  • 对于 equals 来说:
    如果类中重写了equals方法,比较内容是否相等。
    String、Date、File、包装类都重写了Object类的equals方法。

如果类中没有重写equals方法,比较地址值是否相等(是否指向同一个地址值)。

Student stu1 = new Student(11, "张三");
Student stu2 = new Student(11,"张三");
System.out.println(stu1.equals(stu2));//false

既然equals比较的是内容是否相同,为什么结果还是false呢?
回顾知识:
在Java中我们知道任何类的超类都是Object类,Student类也继承Object类。
查看Object类中的equals方法也是 == 比较(也就是比较地址值),因此结果当然是false。

 public boolean equals(Object obj) {
   
        return (this == obj);
    }

既然这样我们如何保证两个对象内容相同呢?
这里就需要我们去重写equals方法?

  @Override
    public boolean equals(Object obj){
   
        if (this == obj){
   
            return true;
        }
        if (obj instanceof Student) {
   
            Student stu = (Student)obj;
            return this.age == stu.age && this.name.equals(stu.name);
        }
        return false;
    }

17. ConcurrentHashMap如何实现线程安全的

  • jdk 1.7以前结构是segment数组 + HashEntry数组 + 链表,使用分段式锁,实现线程安全。容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的”分段锁”思想,见下图:
    在这里插入图片描述
    get()操作:
    HashEntry中的value属性和next指针是用volatile修饰的,保证了可见性,所以每次获取的都是最新值,get过程不需要加锁。
    1.将key传入get方法中,先根据key的hashcode的值找到对应的segment段。
    2.再根据segment中的get方法再次hash,找到HashEntry数组中的位置。
    3.最后在链表中根据hash值和equals方法进行查找。
    ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
    put()操作:
    1.将key传入put方法中,先根据key的hashcode的值找到对应的segment段
    2.再根据segment中的put方法,加锁lock()。
    3.再次hash确定存放的hashEntry数组中的位置
    4.在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。

  • jdk1.8以后结构是 数组+Node+红黑树实现,采用**Synchronized + CAS(自旋锁)**保证线程安全。Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS

为什么在有Synchronized 的情况下还要使用CAS
因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized
来保证线程安全,大大提高了低并发下的性能.

锁 :
锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁,锁全表),并发扩容.

读操作无锁 :

  • Node的val和next使用volatile修饰,读写线程对该变量互相可见

  • 数组用volatile修饰,保证扩容时被读线程感知

  • get()操作:
    get操作全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的。
    在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见的。
    1.计算hash值,定位到Node数组中的位置
    2.如果该位置为null,则直接返回null
    3.如果该位置不为null,再判断该节点是红黑树节点还是链表节点
    如果是红黑树节点,使用红黑树的查找方式来进行查找
    如果是链表节点,遍历链表进行查找

  • put()操作:
    1.先判断Node数组有没有初始化,如果没有初始化先初始化initTable();
    2.根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入
    3.如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁
    4.如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。
    5.如果是红黑树,就按照红黑树的结构进行插入。

18. 说一下 HashSet 的实现原理?

  • HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

19. ArrayList 和 LinkedList 的区别是什么?

  • 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数
  • 存储方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

20. ArrayList 和 Vector 的区别是什么?

  • 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
  • 性能:ArrayList 在性能方面要优于 Vector。
  • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

21. Array 和 ArrayList 有何区别?

  • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
  • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

22. 在 Queue 中 poll()和 remove()有什么区别?

  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
  • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

多线程

23. 并行和并发有什么区别?

  • 并行:多个处理器或多核处理器同时处理多个任务。
  • 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

24. 线程和进程的区别?了解pthread吗,new一个线程占用多少内存?HandlerThread是什么?

  • 进程是cpu资源分配的基本单位
  • 线程是cpu调度和执行的最小单位
  • 一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。

了解pthread吗,new一个线程占用多少内存?

  • pthread一般是指 POSIX的线程标准,是一套定义了创建和操纵线程的API。 一般用于Unix-like系统,如Linux、Mac OS。
    Java的跨平台基于虚拟机,由JVM屏蔽不同的操作系统的底层实现。在Java中创建线程,运行在Linux中(包括Android),实际上就是封装了pthread
    线程内存
  • 在Java中每个线程需要分配线程内存,用来存储自身的线程变量。在JDK 1.4中每个线程是256K的内存,在JDK 1.5之后每个线程是1M的内存。

HandlerThread是什么?
HandlerThread就是一个自带Handler的Thread,更确切的讲是一个内含有Looper的Thread。
使用场景:
对于普通的Thread,执行一遍就结束了,一个实例不能start多次,如果多次启动同一个thread,那么将会抛出一个IllegalThreadStateException异常。HandlerThread由于自带Handler,也就相当自带了一个任务队列,需要进行耗时操作的时候只要通过Handler执行send或post系列操作就行,不再需要频繁创建Thread,节省资源。

25. 守护线程是什么?如何退出一个线程

  • 守护线程是运行在后台的一种特殊线程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

    • 守护线程的创建和普通线程的创建方式相同,使用Thread类或者继承Thread类。
    • 使用setDaemon()方法将线程设置为守护线程,默认情况下线程是非守护线程。
    • 守护线程的主要作用是为其他线程提供服务,如监控、日志记录等。
    • 守护线程在执行过程中会随着所有非守护线程的结束而结束,即使守护线程未执行完其任务。
    • 守护线程不能持有或者修改非守护线程的资源,否则会引发线程安全问题。
    • 守护线程通常需要设置合适的睡眠时间,以免占用过多的系统资源。
    • 守护线程不能捕获和处理异常,一旦出现异常,守护线程会自动结束。
    • 守护线程无法使用join()方法等待其他线程的结束,因为守护线程会在所有非守护线程结束后自动结束。
    • 守护线程的优先级通常较低,以免抢占过多CPU资源。
  • 退出线程

    • 使用标志位退出线程
      线程执行run方法过程中,我们可以通过一个自定义变量来决定是否还需要退出线程,若满足条件,则退出线程,反之继续执行。
    • 使用interrupt方法中断线程(推荐使用)
      使用interrupt方法来中断线程,说到底这其实也是一个标志位的存在,跟上面的原理一样,只不过是Thread类内部提供的罢了。其他线程通过调用某个线程的interrupt()方法对其进行中断操作,被中断的线程则是通过线程的isInterrupted()来进行判断是否被中断

26. 创建线程有哪几种方式?,如何保证线程顺序执行

  • 继承 Thread 重写 run 方法;
  • 实现 Runnable 接口;
  • 实现 Callable 接口。

如何保证线程顺序执行

  • 使用线程的join方法
  • 使用主线程的join方法
  • 使用线程的wait方法
  • 使用线程的线程池方法
  • 使用线程的Condition(条件变量)方法
  • 使用CountDownLatch(倒计数)的方法
  • 使用线程的CyclicBarrier(回环栅栏)方法
  • 使用线程的Semaphore(信号量)方法
  • 使用LockSupport的park与unpark方法
  • 使用阻塞队列的put与take方法
  • 使用CAS思想来完成多线程的顺序执行AtomicReference

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

  • runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。

28. 线程有哪些状态?线程可以多次调用start吗?

线程的状态:

  • NEW 尚未启动
  • RUNNABLE 就绪态
  • RUNNING 运行中
  • BLOCKED 阻塞的(被同步锁或者IO锁阻塞)
  • WAITING 永久等待状态
  • TIMED_WAITING 等待指定的时间重新被唤醒的状态
  • TERMINATED 执行完成
    在这里插入图片描述

线程可以多次调用start吗?
结论:多次调用时,代码会抛出IllegalThreadStateException异常

// 使用synchronized关键字保证这个方法是线程安全的
public synchronized void start() {
   
    // threadStatus != 0 表示这个线程已经被启动过或已经结束了
    // 如果试图再次启动这个线程,就会抛出IllegalThreadStateException异常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    // 将这个线程添加到当前线程的线程组中
    group.add(this);

    // 声明一个变量,用于记录线程是否启动成功
    boolean started = false;
    try {
   
        // 使用native方法启动这个线程
        start0();
        // 如果没有抛出异常,那么started被设为true,表示线程启动成功
        started = true;
    } finally {
   
        // 在finally语句块中,无论try语句块中的代码是否抛出异常,都会执行
        try {
   
            // 如果线程没有启动成功,就从线程组中移除这个线程
            if (!started) {
   
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
   
            // 如果在移除线程的过程中发生了异常,我们选择忽略这个异常
        }
    }
}

调用native方法start0()时,threadStatus=5,线程状态为RUNNABLE。

29. sleep()、wait()、join()、yield()有什么区别?

  • 类的不同:sleep() 来自 Thread,wait() 来自 Object。
  • 释放锁:sleep() 不释放锁;wait() 释放锁。都可中断。yield()不会释放锁,sleep后进入阻塞状态,yield让线程重回就绪状态。
  • 优先级:sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。yield 方法只能使同优先级或更高优先级的线程有执行的机会
  • 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。注意:wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用
  • sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  • join: Thread的方法,这是一种特殊的wait,若有两个线程T1,T2,当在T1中调用T2.join后,T1线程就会进入阻塞状态,直到T2线程运行完了,T1才会继续运行。使用时要先调用线程的start,再调用join.

30. notify()和 notifyAll()有什么区别?

  • notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

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

  • start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
  • 直接调用run方法,不会在线程中执行,只是相当于执行了一个普通的方法。

32. 为什么要使用线程池,非阻塞式生产者消费者如何实现?

为什么使用线程池

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。

非阻塞式生产者消费者如何实现?
因为要求是非堵塞式,所以缓冲区不能使用便捷的堵塞队列,只能使用一般的集合代替,类似ArrayList等。很明显会引起两个问题。1. 并发问题 2. 线程通信问题

/**
 * 实现非堵塞式生产者消费者模式
 */     
public class ProducerConsumerDemo {
   

    /**
     * 定义队列最大容量,指缓冲区最多存放的数量
     */
    private static int MAX_SIZE = 3;

    /**
     * 缓冲区队列,ArrayList为非堵塞队列,线程不安全
     * static修饰,全局唯一
     */
    private static final List<String> list = new ArrayList<>();

    public static void main(String[] args) {
   
        //创建生产者线程
        Producer producer = new Producer();
        //创建消费者线程
        Consumer consumer = new Consumer();
        //生产者线程开启
        producer.start();
        //消费者线程开启
        consumer.start();
    }
}
/**
 * 生产者线程
 */
 class Producer extends Thread {
   

        @Override
        public void run() {
   
            //使用while循环执行run方法
            while (true) {
   
                try {
   
                    //生产者 sleep 300ms, 消费者 sleep 500ms,模拟两者的处理能力不均衡
                    Thread.sleep(300);
                } catch (InterruptedException e1) {
   
                    e1.printStackTrace();
                }

                //第1步:获取队列对象的锁,与消费者持有的锁是同一把,保证线程安全
                synchronized (list) {
   
                    //第2步:判断缓冲区当前容量
                    //第2.1步:队列满了就不生产,等待
                    while (list.size() == MAX_SIZE) {
   
                        System.out.println("生产者 -> 缓冲区满了,等待消费...");
                        try {
   
                            //使用wait等待方法,内部会释放当前持有的锁
                            list.wait();
                        } catch (InterruptedException e) {
   
                            e.printStackTrace();
                        }
                    }
                    //第2.2步:队列未满就生产一个产品
                    list.add("产品");
                    System.out.println("生产者 -> 生产一个产品,当前队列大小:" + list.size());

                    //唤醒其他线程,这里其他线程就是指消费者线程
                    list.notify();
                }
            }
        }
    }
}
/**
 * 消费者线程
 */
class Consumer extends Thread {
   

        @Override
        public void run() {
   
            //使用while循环执行run方法
            while (true) {
   
                try {
   
                    Thread.sleep(500);
                } catch (InterruptedException e1) {
   
                    e1.printStackTrace();
                }

                //第1步:获取队列对象的锁,与生产者持有的锁是同一把,保证线程安全
                synchronized (list) {
   
                    //第2步:判断缓冲区当前容量
                      //第2.1步:队列空了,等带
                    while (list.size() == 0) {
   
                        System.out.println("消费者 -> 缓冲区空了,等待生产...");
                        try {
   
                            //使用wait等待方法,内部会释放当前持有的锁
                            list.wait();
                        } catch (InterruptedException e) {
   
                            e.printStackTrace();
                        }
                    }
                    //第2.2步:队列不为空,消费一个产品
                    list.remove(0);
                    System.out.println("消费者 -> 消费一个产品,当前队列大小:" + list.size());

                    //唤醒其他线程,这里其他线程就是指生产者线程
                    list.notify();
                }
            }
        }
    }

33. 创建线程池有哪几种方式?

线程池创建有七种方式,最核心的是最后一种:

  • 单线程线程池(newSingleThreadExecutor()):它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
  • 可回收缓存线程池(newCachedThreadPool()):它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
  • 可控最大并发数线程池(newFixedThreadPool(int nThreads)):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
  • 支持定时与周期性任务的线程池(newScheduledThreadPool(int corePoolSize)):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
  • newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
  • newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
  • ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

34. ThreadPoolExecutor了解吗?参数是什么意思

  • corePoolSize: 线程池中的核心线程数,默认情况下核心线程一直存活在线程池中,如果将ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设为 true,如果线程池一直闲置并超过了 keepAliveTime 所指定的时间,核心线程就会被终止。
  • maximumPoolSize: 最大线程数,当线程不够时能够创建的最大线程数(包含核心线程数)

临时线程数 = 最大线程数 - 核心线程数

  • keepAliveTime: 线程池的闲置超时时间,默认情况下对非核心线程生效,如果闲置时间超过这个时间,非核心线程就会被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设为 true 的时候,核心线程如果超过闲置时长也会被回收。
  • unit: 配合 keepAliveTime 使用,用来标识 keepAliveTime 的时间单位。
  • workQueue: 线程池中的任务队列,使用 execute() 或 submit() 方法提交的任务都会存储在此队列中。
  • threadFactory: 为线程池提供创建新线程的线程工厂。
  • rejectedExecutionHandler: 线程池任务队列超过最大值之后的拒绝策略, RejectedExecutionHandler 是一个接口,里面只有一个rejectedExecution方法,可在此方法内添加任务超出最大值的事件处理;
    在这里插入图片描述
    在这里插入图片描述

ThreadPoolExecutor 也提供了 4 种默认的拒绝策略:

  • DiscardPolicy():丢弃掉该任务但是不抛出异常,不推荐这种(导致使用者没觉察情况发生)
  • DiscardOldestPolicy():丢弃队列中等待最久的任务,然后把当前任务加入队列中。
  • AbortPolicy():丢弃任务并抛出 RejectedExecutionException 异常(默认)。
  • CallerRunsPolicy():由主线程负责调用任务的run()方法从而绕过线程池直接执行,既不抛弃任务也不抛出异常(当最大线程数满了,任务队列中也满了,再来一个任务,由主线程执行)
    在这里插入图片描述

35. 线程池中 submit() 和 execute() 方法有什么区别?

  • execute():只能执行 Runnable 类型的任务。
  • submit():可以执行 Runnable 和 Callable 类型的任务。
  • Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。

36. 线程池都有哪些状态?

  • RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
  • TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
  • TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
    在这里插入图片描述

37. 知道线程池中线程复用原理吗?

  • 线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
  • 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

38. 什么是死锁?怎么避免死锁?

  • 当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

死锁的四个必要条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
  • 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
  • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • 循环等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

避免死锁的策略:

  1. 避免嵌套锁:尽量避免在一个线程中同时获取多个锁,因为这可能导致锁的顺序问题,进而引发死锁。如果确实需要获取多个锁,那么应该始终按照相同的顺序来获取,这样可以减少死锁的可能性。
  2. 使用超时锁:在尝试获取锁的时候,可以设置一个超时时间。如果在这个时间内无法获取到锁,那么就放弃获取,并等待一段时间后再重试。这样可以避免线程无限期的等待下去,从而导致死锁。
  3. 使用锁顺序:当多个线程需要同时访问多个资源时,始终按照一致的顺序请求锁。这样可以确保不会出现循环等待的情况,从而避免死锁。
  4. 检测死锁并恢复:通过检测系统中的资源分配图和进程等待图,来检测系统是否发生了死锁。如果检测到死锁发生,就采取一些措施来解除死锁,例如终止一些进程的执行,或者剥夺一些进程占有的资源等。
  5. 使用读写锁:ReadWriteLock接口是Java并发包java.util.concurrent.locks提供的一个锁接口,它允许多个线程同时读一个资源,但在写资源时只允许一个线程,从而提高了并发性能。这种锁在读取操作远多于写入操作的场景中特别有用,因为它减少了线程间的竞争,进而减少了死锁的可能性。
  6. 使用条件变量:Condition接口可以用来替代Object的wait()、notify()和notifyAll()方法,提供更灵活、更强大的线程间通信机制。通过条件变量,我们可以更精确地控制线程的等待和唤醒,减少不必要的线程阻塞,从而降低死锁的风险。
  7. 谨慎使用中断:线程中断是Java并发编程中的一个重要概念,但如果不正确地使用,也可能导致死锁。例如,一个线程在持有锁的情况下被中断,如果中断处理不当,可能会导致该线程无法释放锁,从而引发死锁。因此,在使用中断时,我们应该确保线程能够正确地处理中断,并在必要时释放持有的锁。

java中避免死锁的具体实践:

  1. 使用java.util.concurrent包中的工具:Java的java.util.concurrent包提供了很多并发编程的工具,例如Lock接口和它的实现类ReentrantLock,它们提供了更灵活的锁机制,可以帮助我们更好地避免死锁。
  2. 使用tryLock方法:ReentrantLock类的tryLock方法可以在无法获取锁时立即返回,而不是一直等待下去。这可以避免线程在无法获取锁时进入阻塞状态,从而减少死锁的可能性。
  3. 避免在持有锁的时候进行I/O操作:I/O操作通常耗时较长,如果线程在持有锁的时候进行I/O操作,那么其他需要这个锁的线程就会被阻塞,从而增加了死锁的风险。因此,我们应该尽量避免在持有锁的时候进行I/O操作。

39. ThreadLocal 是什么?有哪些使用场景?TreadLocal为什么会导致内存泄漏呢?

  • ThreadLocal 是什么
    ThreadLocal 即线程本地变量,为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。起到线程隔离的作用。
    • Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
    • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
    • 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。
  • 为什么会导致内存泄漏
    因为ThreadLocalMap内部Entry 中的 key 是弱引用,当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null)时,那么系统 GC 的时候,根据可达性分析,这个 ThreadLocal 实例就没有任何一条链路能够引用到它,此时 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。
    因此使用完,就要手动调用remove()
    (回忆一下四种引用:
    • 强引用:我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
    • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
    • 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
    • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。)

40. 说一下 synchronized 底层实现原理?

  • synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

41. synchronized 和 volatile 的区别是什么?

  • volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

42. synchronized 和 Lock 有什么区别?

  • synchronized 可以给方法、代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,可能会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

43. synchronized 和 ReentrantLock 区别是什么?

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
主要区别如下:

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。

44. 说一下 atomic 的原理?

  • Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

45. 什么是反射?

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

46. 创建对象有几种方式

  • 4种
  • new 关键字
Example example = new Example();
  • 反射 这是我们运用反射创建对象时最常用的方法。Class类的newInstance使用的是类的public的无参构造器。因此也就是说使用此方法创建对象的前提是必须有public的无参构造器才行,否则报错
// 1.
Example example = Example.class.newInstance();
// 2.Constructor.newInstance
// 本方法和Class类的newInstance方法很像,但是比它强大很多。 java.lang.relect.Constructor类里也有一个newInstance方法可以
//创建对象。我们可以通过这个newInstance方法调用有参数(不再必须是无参)的和私有的构造函数(不再必须是public)。
Constructor<?>[] declaredConstructors = test.class.getDeclaredConstructors();
Constructor<?> noArgsConstructor = declaredConstructors[0];
noArgsConstructor.setAccessible(true); // 非public的构造必须设置true才能用于创建实例
Object test = noArgsConstructor.newInstance();
  • 克隆 无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去,用clone方法创建对象并不会调用任何构造函数。 要使用clone方法,我们必须先实现Cloneable接口并复写Object的clone方法。
public class Test {
   
    String b = "123";

    @Override
    public Test clone() throws CloneNotSupportedException {
   
        return (Test) super.clone();
    }

    public Test() {
   
        Log.d("TAGGG", "print: init ");
    }
}

public class Main {
   
    public static void main(String[] args) throws Exception {
   
        Test test= new Test();
        Object clone = Test.clone();

        System.out.println(test);
        System.out.println(clone);
        System.out.println(test == clone); //false
    }
}
  • 反序列化 当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。
public class Main {
   

    public static void main(String[] args) throws Exception {
   
        Test test= new Test();
        byte[] bytes = SerializationUtils.serialize(test);

        // 字节数组:可以来自网络、可以来自文件(本处直接本地模拟)
        Object deserTest = SerializationUtils.deserialize(bytes);
        System.out.println(test);
        System.out.println(deserTest);
        System.out.println(test == deserTest);
    }
}

47. 使用过哪些设计模式?

  • 设计模式可分为三大类
  • 创建型模式——对象实例化的模式,创建型模式用于解耦对象的实例化过程。
    1. 单例模式:某个类只能有一个实例,提供一个全局的访问点。
    2. 工厂方法模式:一个工厂类根据传入的参量决定创建出哪一种产品类的实例。
    3. 抽象工厂模式:创建相关或依赖对象的家族,而无需明确指定具体类。
    4. 建造者模式:封装一个复杂对象的创建过程,并可以按步骤构造。
    5. 原型模式:通过复制现有的实例来创建新的实例。
  • 结构型模式——把类或对象结合在一起形成一个更大的结构。
    1. 装饰器模式:动态的给对象添加新的功能。
    2. 代理模式:为其它对象提供一个代理以便控制这个对象的访问。
    3. 桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
    4. 适配器模式:将一个类的方法接口转换成客户希望的另一个接口。
    5. 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。
    6. 外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
    7. 享元模式:通过共享技术来有效的支持大量细粒度的对象。
  • 行为型模式
    1. 策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
    2. 模板方法模式:定义一个算法结构,而将一些步骤延迟到子类实现。
    3. 命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
    4. 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
    5. 观察者模式:对象间的一对多的依赖关系。
    6. 仲裁者模式:用一个中介对象来封装一系列的对象交互。
    7. 备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
    8. 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
    9. 状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
    10. 责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
    11. 访问者模式:不改变数据结构的前提下,增加作用于一组对象元素的新功能。

48. 线程间如何通信?

  • 使用共享变量:多个线程可以通过共享变量来进行通信。通过对共享变量的读写操作,一个线程可以向另一个线程传递信息。
  • 使用wait()和notify()方法:线程可以通过调用wait()方法来等待某个条件的满足,而其他线程可以通过调用notify()方法来通知等待的线程条件已经满足。
  • 使用Lock和Condition:Java并发包中的Lock和Condition接口提供了一种更灵活的线程通信机制。通过Lock接口的newCondition()方法可以获得一个Condition对象,线程可以通过调用Condition对象的await()方法等待某个条件的满足,而其他线程可以通过调用Condition对象的signal()或signalAll()方法来通知等待的线程条件已经满足。
  • 使用管道(PipedInputStream和PipedOutputStream):管道是一种特殊的流,可以用于在两个线程之间传递数据。一个线程可以将数据写入管道的输出流,而另一个线程可以从管道的输入流中读取数据。
  • 使用阻塞队列:Java并发包中的阻塞队列(BlockingQueue)提供了一种线程安全的队列实现,可以用于在多个线程之间传递数据。一个线程可以将数据放入队列中,而另一个线程可以从队列中取出数据。
  • 使用信号量(Semaphore):信号量是一种计数器,用于控制同时访问某个资源的线程数。线程可以通过调用信号量的acquire()方法获取一个许可,从而允许同时访问资源的线程数减少;线程可以通过调用信号量的release()方法释放一个许可,从而允许同时访问资源的线程数增加。
  • 使用CountDownLatch:CountDownLatch是一种同步工具类,可以用于控制一个或多个线程等待其他线程执行完毕后再继续执行。一个线程可以通过调用CountDownLatch的await()方法等待其他线程执行完毕,而其他线程可以通过调用CountDownLatch的countDown()方法告知自己已经执行完毕。
  • 使用CyclicBarrier:CyclicBarrier是一种同步工具类,可以用于控制多个线程在某个屏障处等待,直到所有线程都到达屏障后才继续执行。每个线程可以通过调用CyclicBarrier的await()方法等待其他线程到达屏障,而当所有线程都到达屏障后,屏障会自动打开,所有线程可以继续执行。

49. ArrayList 如何保证线程安全

  • 方法一、使用 Collections.synchronizedList() 方法:Java 提供了 Collections 类中的 synchronizedList() 方法,可以将一个普通的 ArrayList 转换成线程安全的 List。示例如下:
List<E> myArrayList = Collections.synchronizedList(new ArrayList<E>());
  • 方法二、使用 java.util.concurrent.CopyOnWriteArrayList 类:
    CopyOnWriteArrayList 是 Java 并发包(java.util.concurrent)中提供的一个线程安全的列表实现。它在读取操作上没有锁,并且支持在迭代期间进行修改操作,而不会抛出 ConcurrentModificationException 异常。示例如下:
CopyOnWriteArrayList<E> myArrayList = new CopyOnWriteArrayList<E>();
  • 方法三、写一个myArrayList继承自Arraylist,然后重写或按需求编写自己的方法,这些方法要写成synchronized,在这些synchronized的方法中调用ArrayList的方法。
import java.util.ArrayList;
public class MyArrayList<E> extends ArrayList<E> {
   
 
    @Override
    public synchronized boolean add(E e) {
   
        // 在 synchronized 方法中调用 ArrayList 的 add 方法
        return super.add(e);
    }
 
    @Override
    public synchronized void add(int index, E element) {
   
        // 在 synchronized 方法中调用 ArrayList 的 add 方法
        super.add(index, element);
    }
 
    @Override
    public synchronized E remove(int index) {
   
        // 在 synchronized 方法中调用 ArrayList 的 remove 方法
        return super.remove(index);
    }
 
    @Override
    public synchronized boolean remove(Object o) {
   
        // 在 synchronized 方法中调用 ArrayList 的 remove 方法
        return super.remove(o);
    }
 
    // 可以按需求继续重写其他需要同步的方法
 
    // 注意:需要根据具体的需求选择要同步的方法,不一定需要将所有方法都同步
    }
  • 方法四、使用显式的锁:可以使用 java.util.concurrent.locks 包中提供的显式锁(如 ReentrantLock)来手动实现对 ArrayList 的同步。这需要在访问 ArrayList 的地方显式地获取和释放锁,从而确保在同一时刻只有一个线程可以访问 ArrayList。

以下是使用显式锁 (ReentrantLock) 实现对 ArrayList 进行同步的示例代码:

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
 
public class MyArrayList<E> {
   
 
    private ArrayList<E> arrayList = new ArrayList<>();
    private ReentrantLock lock = new ReentrantLock();
 
    public void add(E e) {
   
        lock.lock();
        try {
   
            // 在锁内部调用 ArrayList 的 add 方法
            arrayList.add(e);
        } finally {
   
            lock.unlock();
        }
    }
 
    public void add(int index, E element) {
   
        lock.lock();
        try {
   
            // 在锁内部调用 ArrayList 的 add 方法
            arrayList.add(index, element);
        } finally {
   
            lock.unlock();
        }
    }
 
    public E remove(int index) {
   
        lock.lock();
        try {
   
            // 在锁内部调用 ArrayList 的 remove 方法
            return arrayList.remove(index);
        } finally {
   
            lock.unlock();
        }
    }
 
    public boolean remove(Object o) {
   
        lock.lock();
        try {
   
            // 在锁内部调用 ArrayList 的 remove 方法
            return arrayList.remove(o);
        } finally {
   
            lock.unlock();
        }
    }
 
    // 可以按需求继续实现其他需要同步的方法
    }

50. 静态内部类与非静态内部类的区别

  • 实例化方式:静态内部类可以直接通过外部类名来实例化,而非静态内部类必须要通过外部类的实例来实例化。
  • 对外部类的引用:静态内部类不持有对外部类实例的引用,而非静态内部类则会持有对外部类实例的引用。这意味着在静态内部类中不能直接访问外部类的非静态成员(方法或字段),而非静态内部类可以。
  • 生命周期:静态内部类的生命周期与外部类相互独立,即使外部类实例被销毁,静态内部类仍然存在。非静态内部类的生命周期与外部类实例绑定,只有在外部类实例存在时才能创建非静态内部类的实例。
  • 访问权限:静态内部类对外部类的访问权限与其他类一样,根据访问修饰符而定。非静态内部类可以访问外部类的所有成员,包括私有成员。

51. java在传参时是值传递还是引用传递

在Java中,参数传递始终是按值进行的。这意味着当你将一个参数传递给一个方法时,你实际上传递的是这个参数的值的副本。如果参数是基本数据类型(如int, double, char等),则传递的是该值的一个副本。如果参数是对象引用,则传递的是这个引用的副本,但是原始引用和副本引用都指向同一个对象。

public class Example {
   
    public static void main(String[] args) {
   
        int number = 10;
        System.out.println("Before: " + number); // 输出10
        changeNumber(number);
        System.out.println("After: " + number); // 输出10,因为传递的是值的副本
 
        MyClass obj = new MyClass();
        System.out.println("Before: " + obj.value); // 输出0
        changeObject(obj);
        System.out.println("After: " + obj.value); // 输出100,因为引用指向的对象内容被改变
    }
 
    public static void changeNumber(int num) {
   
        num = 20;
    }
 
    public static void changeObject(MyClass obj) {
   
        obj.value = 100;
    }
}
 
class MyClass {
   
    int value = 0;
}

changeNumber方法接收一个整数值,并在方法内部改变它的值。这不会影响main方法中的number变量,因为changeNumber方法接收的是number值的副本。

然而,changeObject方法接收一个MyClass类型的对象引用,并改变这个对象的value字段。由于传递的是引用的副本,副本引用和原始引用都指向同一个对象,所以原始引用指向的对象内容被改变,导致main方法中的对象也被改变。

52. String s = new String(“123”)创建了两个对象。‌

第一个对象是静态存储区的"123"字符串。
第二个对象是通过new关键字在堆内存中创建的新字符串对象。‌

对象的创建过程 在Java中,每当使用​​new​​关键字创建一个对象时,会经历以下几个步骤:

  • 在堆内存中分配空间,创建一个新的对象。
  • 调用对象的构造方法,对对象进行初始化。
  • 返回对象的引用,赋值给指定的变量。

String类的特殊性
String类在Java中是一个特殊的类,它有一个特殊的地位和使用方式。当我们使用​​String s = new String()​​语句创建一个String对象时,实际上会创建两个对象。

  • 堆内存中的对象:通过​​new String()​​创建的对象存储在堆内存中。这个对象是一个新的String对象,可以在堆内存中进行操作。这个对象是可变的,可以通过一些方法来修改其内容。
  • 字符串常量池中的对象:Java中有一个特殊的内存区域叫做字符串常量池(String Pool),用于存储字符串常量。当我们使用​​new
    String()​​创建一个String对象时,首先在堆内存中创建一个新的对象,然后会检查字符串常量池中是否已经存在相同内容的字符串。如果字符串常量池中已经存在该字符串,则不会创建新的对象,而是将已存在的字符串对象的引用赋值给变量​​s​​。如果字符串常量池中不存在空字符串,那么会在字符串常量池中创建一个新的空字符串对象,并将其引用赋值给变量​​s​​。

因此,通过​​String s = new String()​​语句创建了两个对象:一个在堆内存中,一个在字符串常量池中。不过,实际上我们很少使用这种方式来创建String对象,通常会直接使用字符串字面量来创建String对象,例如​​String s = ""​​或者​​String s = "Hello"​​。这样可以确保只创建一个对象,并且优化了内存使用。

53. java异常机制中,Exception和Error的区别

在 Java 中,异常处理的核心类是 Throwable。Throwable 是所有错误和异常的超类,分为两个主要子类:Exception 和 Error。

  • Exception
    用于表示程序中可以预见的、可以捕获和处理的异常。
    • RuntimeException: 不检查异常(Unchecked Exception),在编译时不强制要求处理,例如 NullPointerException。
    • Checked Exception: 可检查异常,在编译时强制要求处理,例如 IOException。
  • Error: 表示严重的错误,通常是程序无法恢复的,例如 OutOfMemoryError、StackOverflowError

54. finally中的代码一定会执行吗,try里有return,finally还执行吗

  1. 不管有木有出现异常,finally块中代码都会执行;
  2. 当try和catch中有return时,finally仍然会执行;
  3. finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码如何运行,返回的值都不会改变,仍然是之前保存的值),函数返回值是在finally执行前确定的;
  4. finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

JVM没有被强制终止运行,并且没有遭遇不可恢复的系统错误时,finally块中的代码会被执行。但存在一些特殊情况:

  1. 如果程序正常退出例如在try或catch块中有System.exit(int)调用成功终止了JVM,那么finally块也不会被执行。
  2. 遇到严重的系统级错误
  3. JVM自身出现致命错误导致无法继续执行时,finally块将无法执行。

55. parcelable和serializable的区别?

‌Parcelable和Serializable是Android开发中两种不同的对象序列化方式,它们各有特点,适用于不同的场景。‌

  • 性能差异‌:Parcelable比Serializable更高效。Parcelable的设计目标是在Android中传递对象数据,尤其是在IPC(进程间通信)的场景中,而Serializable是通用的Java接口,其实现可能导致较高的性能开销‌。(
    1、Serializable的本质是使用了反射,序列化的过程比较慢,这种机制在序列化的时候会创建很多临时的对象,比引起频繁的GC。
    2、Parcelable方式的本质是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的类型,这样就实现了传递对象的功能了。)
  • 序列化机制‌:Parcelable使用Android提供的序列化机制,基于Binder,适用于Android系统内部的数据传递。这种机制在Android中传递对象数据更为高效。而Serializable使用Java标准的序列化机制,虽然它是Java平台通用的一种对象序列化方式,但在Android中可能不如Parcelable高效‌。
  • 实现难度‌:Parcelable的实现相对复杂,需要手动编写writeToParcel和createFromParcel方法,这些方法负责将对象的字段写入Parcel对象和从Parcel对象中读取字段。而Serializable不需要手动实现序列化和反序列化方法,只需要让类实现Serializable接口,但这可能会导致一些性能损失‌。
  • 支持性‌:Serializable是Java提供的一个空接口,用于标识当前类可以被序列化。它提供了一个serialVersionUID字段来进行版本控制,确保序列化和反序列化的兼容性。而Parcelable虽然提供了高效的序列化方式,但它的使用相对局限在Android开发中‌。

综上所述,选择使用Parcelable还是Serializable取决于具体的应用场景和性能要求。如果需要在Android应用内部进行高效的数据传递,Parcelable是更好的选择;而如果需要在不同平台或系统中进行通用序列化,则可以考虑使用Serializable。

56. 为什么intent传递对象需要序列化?

  1. Linux系统中不同进程之间对象是无法传输,所以需要要对对象进行序列化。通过序列化,我们可以将对象转换为字节流,然后将字节流作为数据存储在Intent中。这样在传递对象时,可以保持对象的完整性和结构,并且可以在不同的组件之间进行传递。
  2. Intent在启动其他组件时,会离开当前应用程序进程,进入ActivityManagerService进程,这也就意味着,Intent所携带的数据要能够在不同进程间传输。所以需要对对象进行序列化,从而实现对象在应用程序进程和ActivityManagerService进程之间传输。

57. 泛型是什么?泛型擦除了解吗?

1、泛型是一种重用程序的设计手段
泛型,即参数化类型,目的是将具体类型参数化,在使用时需要传入具体类型进行替换。
参数分为实参和形参,泛型属于类型形参(好比抽象函数,是一种泛指,类似于数学函数中用 x,y,z 代表具体的值)。
2、为什么需要使用泛型?
(1)使用泛型可以编写模板代码来适应任意类型,减少重复代码
(2)保证类型安全,进行编译期错误检查,使代码具有更好的安全性和可读性。
(3)不需要进行类型强制转换。
1,什么是泛型檫除?
泛型擦除是指编译器在处理泛型代码时,会在编译阶段移除(擦除)所有与泛型相关的类型参数信息,将其替换为原始类型,这一过程确保了生成的字节码不包含任何泛型类型信息,使得泛型在运行时实际上是类型无关的

public class GenericDemo {
   
    public static void main(String[] args) {
   
        List<String> list1 = new ArrayList();
        list1.add("String");
        List<Integer> list2 = new ArrayList<>();
        list2.add(10);
        System.out.println("两者相同吗?:"+(list1.getClass()==list2.getClass()));
        System.out.println("list1集合:"+list1.getClass());
        System.out.println("list2集合:"+list2.getClass());
    }
}

存放 String 和 Integer 类型 list 的集合,原始类型均为 java.util.ArrayList,并没有携带具体的存储数据类型
在这里插入图片描述

2,泛型主要经历以下三个阶段
(1)编译时检查
在编译阶段,编译器利用泛型类型信息进行类型检查,确保类型安全
例如,编译器会阻止向List< String >中添加整数或其他非字符串对象,或者尝试将List< Integer >赋值给List< Double >等不兼容的操作
(2)擦除与替换
编译后的字节码中,泛型类型参数被替换为其上界(如果没有指定上界,则默认为Object)
例如,List< String >和List< Integer >在编译后的字节码中都表现为List< Object >,方法参数、局部变量和字段的类型也会相应调整。泛型类和方法的签名在字节码层面不再包含泛型信息
下边定义的是一个泛型类:

public class Pair<T> {
   
    private T value;
    public T getValue() {
   
        return value;
    }
    public void setValue(T value) {
   
        this.value = value;
    }
}
        编译后,它是下面这样的,类型被擦除,并用原始类型替换:

public class Pair{
   
    private Object value;
    public Object getValue() {
   
        return value;
    }
    public void setValue(Object value) {
   
        this.value = value;
    }
}

(3)类型转换
由于类型信息在运行时不可用,编译器会在需要的地方插入必要的类型转换(通常是从Object到具体类型或其子类型的转换)。例如,从List< String >中获取元素时,编译器会在幕后添加一个从Object到String的显式类型转换
注意
泛型类中的静态方法和静态变量不可以使用泛型类所申明的泛型类型参数。
原因:因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用,对象都没有创建,所以不能确定这个泛型参数是何种类型,因此编译的时候会报错。
参考:https://blog.youkuaiyun.com/swadian2008/article/details/100546069

58. 泛型擦除后retrofit是怎么获取类型的?

Retrofit是如何传递泛型信息的?

public interface GitHubService {
   
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

//----------------------------使用jad查看反编译后的class文件:------------------------------------------------

import retrofit2.Call;
public interface GitHubService
{
   
    public abstract Call listRepos(String s);
}
//可以看到class文件中已经将泛型信息给擦除了,那么Retrofit是如何拿到Call<List>的类型信息的?
//我们看一下retrofit的源码

  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
   
    ...
    Type returnType = method.getGenericReturnType();
    ...
  }
 
    public Type getGenericReturnType() {
   
       // 根据 Signature 信息 获取 泛型类型 
      if (getGenericSignature() != null) {
   
        return getGenericInfo().getReturnType();
      } else {
    
      	return getReturnType();
      }
    }

可以看出,retrofit是通过getGenericReturnType来获取类型信息的
jdk的Class 、Method 、Field 类提供了一系列获取 泛型类型的相关方法。
以Method为例,getGenericReturnType获取带泛型信息的返回类型 、 getGenericParameterTypes获取带泛型信息的参数类型。

问:泛型的信息不是被擦除了吗?
答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以Signature的形式 保留在Class文件的Constant pool中。

通过javap命令 可以看到在Constant pool中#5 Signature记录了泛型的类型。

Constant pool:
   #1 = Class              #16            //  com/example/diva/leet/GitHubService
   #2 = Class              #17            //  java/lang/Object
   #3 = Utf8               listRepos
   #4 = Utf8               (Ljava/lang/String;)Lretrofit2/Call;
   #5 = Utf8               Signature
   #6 = Utf8               (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
   #7 = Utf8               RuntimeVisibleAnnotations
   #8 = Utf8               Lretrofit2/http/GET;
   #9 = Utf8               value
  #10 = Utf8               users/{
   user}/repos
  #11 = Utf8               RuntimeVisibleParameterAnnotations
  #12 = Utf8               Lretrofit2/http/Path;
  #13 = Utf8               user
  #14 = Utf8               SourceFile
  #15 = Utf8               GitHubService.java
  #16 = Utf8               com/example/diva/leet/GitHubService
  #17 = Utf8               java/lang/Object
{
   
  public abstract retrofit2.Call<java.util.List<com.example.diva.leet.Repo>> listRepos(java.lang.String);
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #6                           // (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
    RuntimeVisibleAnnotations:
      0: #8(#9=s#10)
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #12(#9=s#13)
}

这就是我们retrofit中能够获取泛型类型的原因

59. CAS 无锁编程的原理

‌‌CAS(Compare And Swap)是一种无锁机制,是乐观锁的一种实现方式。‌ CAS操作通过比较内存中的值和预期值是否相等,如果相等则将内存中的值替换为新值,否则不做任何操作。这种操作由‌CPU保证其原子性,避免了传统锁机制带来的性能开销和死锁问题。‌
CAS操作有三个操作数:内存中的值A、预期原始值B和新值C。当A的值等于B时,将B的值设为C;否则,说明有其他线程已经修改了A的值,当前线程会自旋重试。‌
在这里插入图片描述
CAS操作的优点包括高性能、简单性和可伸缩性。由于CAS操作是原子的,可以避免加锁带来的性能开销。此外,CAS操作相对于传统的锁机制来说更加简单易懂,在高并发场景下,其性能表现通常优于传统的锁机制。‌
CAS操作在实现无锁数据结构方面非常有用。例如,通过CAS操作可以实现无锁队列、无锁栈等数据结构,这些数据结构在多线程环境下可以避免传统锁机制带来的性能问题
java.util.concurrent.atomic包下的AtomicInteger,AtomicBoolean, AtomicLong,AtomicLongArray, AtomicReference等原子类采用了CAS思想实现线程安全,底层是通过volatile变量和CAS两者相结合来保证更新数据的原子性。
缺点:
ABA问题,即一个变量被其他线程修改后又改回原值,CAS操作会误认为这个变量从来没有被修改过。这个问题可以通过引入版本号或时间戳来解决。此外,如果CAS操作失败,它会一直重试,可能导致CPU资源的浪费。为了避免这种情况,可以引入‌退避策略来减少重试的频率。
JUC也是提供了原子对象AtomicStampedReference来解决这个问题。
内部维护了一个 Pair 对象, 存储了 value 值和一个版本号, 每次更新除了 value 值还会更新版本号

private static class Pair<T> {
   
  	// 存储值, 相当于上文的值100
    final T reference;
  	// 类似于版本号的概念
    final int stamp;

    private Pair(T reference, int stamp) {
   
        this.reference = reference;
        this.stamp = stamp;
    }
		// 创建一个新的Pair对象, 每次值变化时都会创建一个新的对象
    static <T> AtomicStampedReference.Pair<T> of(T reference, int stamp) {
   
        return new AtomicStampedReference.Pair<T>(reference, stamp);
    }
}

60. AQS的原理

AQS: AbstractQueuedSynchronizer,顾名思义,翻译过来叫抽象队列同步器,它是JUC并发编程的基石,定义了一套多线程访问共享资源的同步器框架,众多同步类底层都是基于AQS实现的,如常用的ReentrantLock、Semaphore、CountDownLatch等。
原理: AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的先进先出的CLH队列来完成获取资源线程的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
参考:https://www.zhihu.com/question/483996441/answer/2883010448

61. synchronized 和 ReentrantLock 的区别

  • 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
  • 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
  • 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
  • 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  • 底层实现不同:synchronized 是 JVM 层面通过monitor(监视器)实现的,而 ReentrantLock 是基于 AQS 实现的。

62. volatile关键字干了什么?

volatile关键字主要用于两个方面:保证变量的可见性禁止指令重排序

  • 保证可见性: 当一个变量被volatile修饰时,在一个线程中对该变量的修改会立即被其他线程所见。这是因为volatile修饰的变量会被立即写回主内存,并且从主内存中读取最新的值,保证了各个线程之间对变量的修改是可见的。
  • 禁止指令重排序: volatile关键字还会禁止指令重排序优化,保证了程序执行的顺序与代码的顺序一致。这样可以防止某些情况下的线程安全问题,例如双重检查锁定问题。

volatile关键字能够保证变量的可见性,但它并不能保证原子性
(线程安全的三要素包括原子性、可见性和有序性。‌

  • 原子性‌:指的是一个或多个操作要么全部执行成功,要么全部执行失败,满足原子性的操作中途不可被中断。这确保了在多线程环境中,对共享资源的访问是互斥的,同一时刻只能有一个线程对其进行操作,从而避免了数据的不一致状态。
  • 可见性‌:多个线程共同访问共享变量时,某个线程对变量的修改能够立即被其他线程看到。这保证了共享变量的更新对所有线程都是可见的,避免了因缓存或延迟更新导致的数据不一致。
  • 有序性‌:程序的执行顺序按照代码的先后顺序执行。尽管在JMM模型中允许编译器和处理器为了效率进行指令重排序的优化,但在多线程并发编程中,需要考虑如何在保证部分指令允许重排的同时,仍能保证程序的有序性,以确保正确的执行顺序和维护数据的一致性。)

63. AsyncTask的原理

  • AsyncTask是一个抽象类:
    是由Android封装的一个轻量级异步类,内部封装了两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler)。其中SerialExecutor线程池用于任务的排队,让需要执行的多个耗时任务,按顺序排列,THREAD_POOL_EXECUTOR线程池才真正地执行任务,InternalHandler用于从工作线程切换到主线程。

  • 三个参数:
    Params:执行AsyncTask时,后台任务需要传入的参数,在doInBackground中取出使用。
    Progress:后台任务执行时,如果需要显示当前进度,则制定进度的数据类型。
    Result:后台任务执行完成后的返回值类型。

  • 五个方法

    • onPreExecute:在主线程中执行,在后台任务执行前调用,通常用于做一些准备操作。
    • doInBackground(Params… params):在线程池中执行,用于执行后台任务;params 参数是 execute(Params… params)方法中传递的参数。在此方法中可以调用 publishProgress 方法来更新任务的进度,publishProgress方法会调用onProgressUpdate方法。
    • publishProgress(Progress… values):用于更新任务的进度,需要手动调用,publishProgress方法会调用onProgressUpdate方法;values参数为设置的进度值。
    • onProgressUpdate(Progress… values):在主线程中执行,当后台任务的执行进度发生改变时,会被调用,values参数为进度值。
    • onPostExecute(Result result): 在主线程中执行,当后台任务执行完成时,会被调用。result 的值是doInBackground的返回值。
  • 注意事项:

      1. 内存泄漏
      • 原因:非静态内部类持有外部类的匿名引用,导致Activity无法释放。
      • 解决:
        AsyncTask内部持有外部Activity的弱引用。
        AsyncTask改为静态内部类。
        AsyncTask.cancel()。
      1. 生命周期
        不受Activity生命周期的影响,在Activity销毁之前,取消AsyncTask的运行,以此来保证程序的稳定。
      1. 结果丢失
        由于屏幕旋转、Activity在内存紧张时被回收等情况下,Activity会被重新创建,此时,还在运行的AsyncTask会持有一个Activity的非法引用即之前的Activity实例。导致onPostExecute()没有任何作用。
      1. 并行or串行
        Android 1.6之前,默认采用串行执行任务;
        Android 1.6~ 2.3 ,默认采用并行执行任务;
        Android 3.0,默认采用串行执行任务,如果需要改为并行,可以调用AsyncTask的executeOnExecutor()来执行任务即可。
      1. AsyncTask的使用限制
        AsyncTask的对象必须在主线程中创建;
        execute 方法必须在UI线程调用;
        不要在程序中手动调用 onPreExecute、onPostExecute、 doInBackground、onProgressUpdate方法;
        一个AsyncTask对象只能调用一次excute()方法,执行一次,否则会报异常。

参考:https://blog.youkuaiyun.com/weixin_42504805/article/details/126933587

64. java中try catch能捕获异常的原理?

异常捕获机制的实现,借助于编译时生成的异常表。

异常表在编译期生成,存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。

  1. 起始/结束PC:此条异常捕获生效的字节码起始/结束位置。
  2. 跳转PC:异常捕获之后,跳转到的字节码位置。
  3. 异常类型 : 需要捕获的异常具体类型
    public void test4() {
   
        int i = 0;
        try {
   
           i = 1;
        } catch (Exception e) {
   
            i = 2;
        }
    }
//字节码如下 :

 0 iconst_0
 1 istore_1
 2 iconst_1
 3 istore_1
 4 goto 10 (+6)
 7 astore_2
 8 iconst_2
 9 istore_1
10 return

在这里插入图片描述
在位置2到4字节码指令执行范围内,如果出现了Exception对象的异常或者子类对象异常,直接跳转到位置7的指令。也就是i = 2代码位置。
程序运行中触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。
1、如果匹配,跳转到“跳转PC”对应的字节码位置。
2、如果遍历完都不能匹配,说明异常无法在当前方法执行时被捕获,此方法栈帧直接弹出,在上一层的栈帧中进行异常捕获的查询。
多个catch分支情况下,异常表会从上往下遍历,先捕获RuntimeException,如果捕获不了,再捕获Exception。
finally的处理方式就相对比较复杂一点了,分为以下几个步骤:
1、finally中的字节码指令会插入到try 和 catch代码块中,保证在try和catch执行之后一定会执行finally中的代码。
如下,在i=1和i=2两段字节码指令之后,都加入了finally下的字节码指令。
在这里插入图片描述
2、如果抛出的异常范围超过了Exception,比如Error或者Throwable,此时也要执行finally,所以异常表中增加了两个条目。覆盖了try和catch两段字节码指令的范围,any代表可以捕获所有种类的异常。
在这里插入图片描述

JVM相关

1. 简单介绍下jvm虚拟机模型

分为三个部分

  • 类加载子系统(Class Loader Sub System)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎、本地方法接口(本地方法库)(Execution Engine)
    在这里插入图片描述

2. 类加载器子系统

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

  • 类加载流程
    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载这7个阶段,其中其中验证、准备、解析3个部分统称为连接。JVM没有规定类加载的时机,但却严格规定了五种情况下必须立即对类进行初始化,加载自然要在此之前。

    • 运行JVM必须指定一个含有main方法的主类,虚拟机会先初始化这个类。
    • 遇到new、getstatic、putstatic、invokestatic这四条指令时,如果类没有被初始化,则首先对类进行初始化。
    • 使用java.lang.reflect包的方法对类进行反射调用时,若类没有进行初始化,则触发其初始化。
    • 当初始化一个类时假如该类的父类没有进行初始化,首先触发其父类的初始化。
    • 当使用Jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、 - REF_putstatic、REF_inokestatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,触发该类初始化。
  • 1、加载
    在加载的过程中,虚拟机会完成以下三件事情:

    • 通过一个类的全限定名加载该类对应的二进制字节流。
    • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区各个类访问该类的入口。
  • 2、验证
    这一步的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。具体验证的东西如下:

    • 文件格式验证:这里验证的时字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
    • 元数据的验证:就是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,例如:这个类是否有父类,除了 java.lang.Object之外。
    • 字节码校验:字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:① 在字节码的执行过程中,是否会跳转到一条不存在的指令。② 函数的调用是否传递了正确类型的参数。③ 变量的赋值是不是给了正确的数据类型等。
    • 符号引用验证:虚拟机在将符号引用转化为直接引用,验证符号引用全限定名代表的类是否能够找到,对应的域和方法是否能找到,访问权限是否合法,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个 方法无法被找到,则会抛出NoSuchMethodError;这个转化动作将在连接的第三个阶段-解析阶段中发生。
  • 3、准备
    为类变量(static修饰的变量)分配内存并且设置该类变量的默认初始值,即零值,初始化阶段才会设置代码中的初始值
    这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化
    这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配给Java堆中。

  • 4、解析
    解析阶段是虚拟机将常量池内的符号引用(类、变量、方法等的描述符 [名称])替换为直接引用(直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 [地址])的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

  • 5、初始化
    初始化阶段编译器会将类文件声明的静态赋值变量和静态代码块合并生成方法并进行调用。

    • 初始化阶段就是执行类构造器方法的过程,这个方法不需要定义,只需要类中有静态的属性或者代码块即可,javac编 译器自动收集所有类变量的赋值动作和静态代码块中的语句合并而来
    • 构造器方法中指令按照源文件出现的顺序执行
    • 如果该类有父类,jvm会保证子类的在执行前,执行父类的
    • 虚拟机必须保证一个类的方法在多线程情况下被加锁,类只需要被加载一次
  • 类加载器分类
    JVM层面支持两种类加载器:启动类加载器自定义类加载器,启动类加载器由C++编写,属于虚拟机自身的一部分;继承自java.lang.ClassLoader的类加载器都属于自定义类加载器,由Java编写。逻辑上我们可以根据各加载器的不同功能继续划分为:扩展类加载器应用程序类加载器自定义类加载器

    • 1、启动类加载器
      • 由C/C++语言实现,嵌套在JVM内部
      • 负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
      • 没有父加载器,加载扩展类和应用程序类加载器,并作为他们的父类加载器
      • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
    • 2、扩展类加载器
      • 由Java语言实现,派生于ClassLoader类
      • 负责加载java.ext.dirs系统属性所指定目录中的类库,或JAVA_HOME/jre/lib/ext目录(扩展目录)下的类库,如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载
      • 作为类(在rt.jar中)被启动类加载器加载,父类加载器为启动类加载器
    • 3、应用程序类加载器
      • 由Java语言实现,派生于ClassLoader类
      • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
      • 作为类被扩展类加载器加载,父类加载器为扩展类加载器
      • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
      • 通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器,所以有些场合中也称它为“系统类加载器”
    • 4、自定义类加载器
      在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。自定义类加载器作用:
      • 隔离加载类(相同包名和类名的两个类会冲突,引入自己定义类加载器可以规避冲突问题)
      • 修改类加载的方式
      • 扩展加载源(默认从jar包、war包等源加载,可以自定义自己的源)
      • 防止源码泄漏(对编译后的class字节码进行加密,加载时用自定义的类加载器进行解密后使用)
  • 类加载器写协作方式
    Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成Class对象,当触发类加载时,JVM并不知道当前类具体由哪个加载器加载,都是先给到默认类加载器(应用程序类加载器),默认类加载器怎么分配到具体的加载器呢,这边使用了一种叫双亲委派模型的加载机制。

  • 1、双亲委派模型
    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。举例如下:

    • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
    • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
    • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
      若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
  • 2、全盘负责
    当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

  • 3、缓存机制
    缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

3.运行时数据区

按照是否线程私有可分为

  • 线程共有:方法区
  • 线程私有 java虚拟机栈本地方法栈程序计数器

1.方法区
在这里插入图片描述

方法区,也称非堆(Non-Heap),是一个被线程共享的内存区域。其中主要存储类的类型信息方法信息域信息JIT代码缓存运行时常量池等。

    1. 方法区是各个线程共享的内存区域,在虚拟机启动时创建
    1. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
    1. 用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。
    1. 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
      在JDK7之前,习惯把方法区称为永久代,而在JDK8之后,又取消了永久代,改用元空间代替。元空间的本质与方法区类似,都是对JVM规范中方法区这一内存区域的一种实现。不过元空间与永久代的最大区别就是:元空间不在虚拟机设置的内存中,而是直接使用的本地内存。所以元空间的大小并不受虚拟机本身的内存限制,而是受制于计算机的直接内存。

2. 堆 Java堆是Java虚拟机所管理的内存最大的一块区域,Java堆是线程共享的,在虚拟机启动时创建。

  • 几乎所有的对象实例都在这里分配内存。

  • 字符串常量池(String Table),静态变量也在这里分配内存。

  • Java堆是垃圾收集器管理的内存区域,有些资料称为GC堆,当对象不再使用了,被当做垃圾回收掉后,这些为对象分配的内存又重新回到堆内存中。

  • Java堆在逻辑上应该认为是连续的,但是在具体的物理实现上,可以是不连续的。

  • Java堆可以是固定大小的,也可以是可扩展的。现在主流Java虚拟机都是可扩展的。
    -Xmx 最大堆内存
    -Xms 最小堆内存

  • 如果Java堆没有足够的内存给分配实例,并且也无法继续扩展,则抛出 OutOfMemoryError 异常。

    • 1.堆内存结构
      • 堆内存从结构上来说分为年轻代(YoungGen)和老年代(OldGen)两部分;
      • 年轻代(YoungGen)又可以分为生成区(Eden)和幸存者区(Survivor)两部分;
      • 幸存者区(Survivor)又可细分为 S0区(from space)和 S1区 (to space)两部分;
      • Eden 区占大容量,Survivor 两个区占小容量,默认比例是 8:1:1;
      • 静态变量和字符串常量池在年轻代与老年代之外单独分配空间。
        在这里插入图片描述

3.Java虚拟机栈 Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的,因此也是线程安全的。

  • Java虚拟机栈是线程私有的,其生命周期和线程相同。
  • 虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、参与方法的调用与返回等。
  • 每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中出入栈到出栈的过程
  • JVM 允许指定 Java 栈的初始大小以及最大、最小容量。
  • 1.栈帧
    • 定义:栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的 Java 虚拟机栈的栈元素。栈帧存储了方法的局部变量表操作数栈动态链接方法返回地址等信息。
    • 栈帧初始化大小:在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。 因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
    • 栈帧结构:在一个线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
      -在这里插入图片描述在这里插入图片描述

4.程序计数器 程序计数器的英文全称是Program Counter Register,又叫程序计数寄存器。Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。JVM中的PC寄存器是对 物理PC寄存器的一种抽象模拟。
在这里插入图片描述

  • 程序计数器其实就是一个指针,它指向了我们程序中下一句需要执行的指令,可以看做当前线程执行的字节码的行数指示器。
  • 不管是分支、循环、跳转等代码逻辑,字节码解释器在工作时就是改变程序计数器的值来决定下一条要执行的字节码。
  • 每个线程都有一个独立的程序计数器,在任意一个确定的时刻,一个CPU内核都只会执行一条线程中的指令,CPU切换线程后是通过程序计数器来确定该执行哪条指令。
  • 程序计数器占用内存空间小到基本可以忽略不计,是唯一一个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。
  • 如果正在执行的是Native方法,则这个计数器为空。

5.本地方法栈 本地方法栈与虚拟机栈所发挥的作用是非常相似的。只不过虚拟机栈为虚拟机执行的Java方法(即字节码)服务,本地方法栈为虚拟机执行的本地方法(Native方法、C/C++ 实现)服务。

  • 与虚拟机栈一样,当栈深度溢出时,抛出 StackOverFlowError 异常。
  • 当栈扩展内存不足时,抛出 OutOfMemoryError 异常。

4. 执行引擎

负责执行class文件中包含的字节码指令;
在这里插入图片描述
JVM的主要任务之一是负责装载字节码到其内部(运行时数据区),但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只 是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息。

那么,如果想要让一个 Java 程序运行起来,执行引擎(Execution Engine) 的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。

1. 解释器
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令然后执行。

JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行,执行效率低。

在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普遍使用的模板解释器。字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率非常低效。而模板解释器将每一条字节码和一个模板函数性关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。

在Hotspot VM中,解释器主要由Interpreter模块和Code模块构成。

  • Interpreter模块:实现了解释器的核心功能。
  • Code模块:用于管理Hotspot VM在与运行时生成的本地机器指令。

由于解释器在设计和实现上非常简单,因此除了 Java 语言之外,还有许多高级语言同样也是基于解释器执行的,比如:Python、Perl、Ruby等。但就是因为多了中间这一“翻译”过程,导致代码执行效率低下。

为了解决这个问题,JVM平台支持一种叫做即时编译的的技术。即时编译的目的是为了避免函数被解释执行,而是将整个函数编译成机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。

2. 即时(JIT)编译器
就是虚拟机将Java字节码一次性整体编译成和本地机器平台相关的机器语言,但并不是马上执行。JIT 编译器将字节码翻译成本地机器指令后,就可以做一个缓存操作,存储在方法区 的 JIT 代码缓存中。JVM真正执行程序时将直接从缓存中获取本地指令去执行,省去了解释器的工作,提高了执行效率高。

HotSpot VM 是目前市面上高性能虚拟机的代表作之一。它采用解释器与及时编辑器并行的结构。在 Java 虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。

JIT 编译器执行效率高为什么还需要解释器?

  • 当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。
  • 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取 一个平衡点。
    是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用的执行频率而定。关于那些需要被编译成本地代码的字节码,也被称为热点代码,JIT编译器在运行时会对那些频繁被调用的热点代码做出深度优化,将其直接编译成对应平台的本地机器指令,以此提升Java程序的执行性能。

一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体,都可以被称为热点代码。因此都可以通过JIT编译器编译成本地机器指令。由于这种编译方式发生在方法执行的过程中,因此也被称为栈上替换,或者简称为OSR编译。

一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准,必然需要一个明确的阈值。JIT编译器才会将这些热点代码编译成本地机器码执行。

5. 了解GC吗

GC是JVM中的垃圾回收机制。主要作用于Java堆区,用于将不再使用的对象回收,释放内存。简单的说垃圾就是内存中不再使用的对象,所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而不再使用的对象(未引用对象),则没有被任何指针指向。如果这些不再使用的对象不被清除掉,我们内存里面的对象会越来越多,而可使用的内存空间会越来越少,最后导致无空间可用。
垃圾回收的基本步骤分两步:

  • 查找内存中不再使用的对象(GC判断策略)
  • 释放这些对象占用的内存(GC收集算法)

1.对象存活判断 即内存中不再使用的对象,判断对象存活一般有两种方式:引用计数算法和可达性分析法

  • 1. 引用计数算法 给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器+1,当引用失效时,计数器-1,任何时候当计数器为0的时候,该对象不再被引用。

    • 优点:引用计数器这个方法实现简单,判定效率也高,回收没有延迟性。
    • 缺点无法检测出循环引用。 如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0,Java的垃圾收集器没有使用这类算法。
  • 2. 可达性分析算法 可达性分析算法是目前主流的虚拟机都采用的算法,程序把所有的引用关系看作一张图,从所有的GC Roots节点开始,寻找该节点所引用的节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
    在这里插入图片描述

在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈中引用的对象(局部变量);
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象
- 所有被同步锁持有的对象;
- 虚拟机的内部引用如类加载器、异常管理对象;
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

2.垃圾回收算法

  1. 标记-清除算法 标记-清除算法的基本思想就跟它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

    1. 标记阶段 标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的 GC Roots 对象,对从 GCRoots 对象可达的对象都打上一个标识,一般是在对象的 header 中,将其记录为可达对象;

    2. 清除阶段清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header 信息),则将其回收。
      在这里插入图片描述

    标记-清除算法缺点

    1. 效率问题
      标记和清除两个阶段的效率都不高,因为这两个阶段都需要遍历内存中的对象,很多时候内存中的对象实例数量是非常庞大的,这无疑很耗费时间,而且 GC 时需要停止应用程序,这会导致非常差的用户体验。
    2. 空间问题
      标记清除之后会产生大量不连续的内存碎片(从上图可以看出),内存空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

2. 复制算法

复制算法是将可用内存按容量划分为大小相等的两块,每次使用其中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

进击的code

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值