文章目录
- 前言
- 一、Java基础题
- 1、Java语言的三大特性
- 2、JDK 和 JRE 有什么区别
- 3、Java基本数据类型及其封装类
- 4、说明一下public static void main(String args[])这段声明里关键字的作用
- 5、java的数据结构有哪些?
- 6、抽象类和接口的区别?
- 7、== 与 equals 的区别
- 8、String stringBuffer 和 stringBuilder 的区别是什么
- 9、Java创建对象有几种方式?
- 10、HashCode的作用的是什么?
- 11、深拷贝和浅拷贝的区别是什么?
- 12、String str="abc"与 String str=new String("abc")一样吗?
- 13、Java的四种引用分别是什么
- 14、什么是隐式转换,什么是显式转换?
- 15、String str = new String("abc") 与 String str ="abc"分别创建了几个对象?
- 16、什么情况需要Java序列化?
- 17、Java 序列化中如何对一些字段不进行序列化?
- 18、实例化数组后,能不能改变数组长度?
- 19、什么是Java中的集合框架?它包含哪些主要接口和实现类?
- 20、List、Set、Map 之间的区别是什么?
- 21、ArrayList和LinkedList的区别
- 22、HashMap和HashTable的区别
- 23、 Queue 中 poll()和 remove()有什么区别?
- 24、HashMap的底层实现原理?
- 25、谈谈对ConcurrentHashMap的认识?
- 26、红黑树有什么特征?
- 27、列举几个常见的RuntimeException?
- 28、Java中异常处理有哪两种方式?
- 29、Error与Exception区别?
- 30、java反射机制原理
- 31、利用反射如何创建对象?
- 32、利用反射动态创建对象实例?
- 33、IO模型有几种?
- 34、字节流与字符流的区别?
- 35、谈谈对双亲委派机制的认识?
- 二、多线程
- 1、什么是进程,什么是线程?
- 2、java中实现多线程有几种方式?
- 3、Java中线程有几种状态?
- 4、Thread 类中的start() 和 run()方法有什么区别?
- 5、java中线程的优先级?
- 6、notify和notifyAll的区别是什么?
- 7、sleep()和wait()有什么区别 ?
- 8、volatile关键字有什么作用?
- 9、synchronized 和 ReentrantLock 有什么不同?
- 10、SynchronizedMap和ConcurrentHashMap有什么区别?
- 11、当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法?
- 12、Synchronized与Lock的区别?
- 13、线程池中submit()和execute()方法有什么区别?
- 14、java中常见的线程池有哪些?
- 15、进程死锁和线程死锁的区别是什么?
- 16、线程池的任务是顺序执行的吗?
- 17、在java中什么是CAS 什么是AQS?
- 18、什么是ABA问题?
- 19、ThreadLocal的作用?
- 20、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
- 21、简述一下CyclicBarrier和CountDownLatch的区别 ?
- 22、Callable和Future的区别是什么?
- 23、多线程情况下,如果对一个数进行叠加,该怎么做?
- 24、实际开发中,线程池的线程数量如何确定?
- 25、使用ThreadPoolExecutor进行自定义线程池的参数有哪些,分别表示什么意思?
- 26、产生死锁的几个必要条件?
- 27、Java中的锁机制包括多种类型和实现方式是什么?
- 28、实际项目中什么场景可以使用信号量Semaphore?
- 29、Java中的守护线程和用户线程的区别?
- 30、Java中的可见性、原子性、有序性?它们如何影响并发编程?
- 三、JVM
- 1、JVM内存模型结构?
- 2、JVM运行时内存区域划分是什么?
- 3、Java堆内存组成?
- 4、Edem:from:to默认比例是多少?
- 5、JVM(Java虚拟机)中常见的垃圾收集算法有哪些?
- 6、JVM中有哪些常见的垃圾收集器?
- 7、JVM中常见的垃圾收集器组合?
- 8、描述一下JVM加载class文件的原理机制?
- 9、引用计数法?可达性分析?
- 10、jvm中一个对象如何才会被垃圾收集器回收?
- 11、有哪些常见的类加载器?
- 12、调优命令有哪些?
- 13、常用的JVM调优参数设置指令?
- 14、Stop The World是什么?
- 15、什么是空闲列表?
- 16、一个对象头具体都包含哪些内容?
- 17、描述Java对象在堆内存中的分配过程?
- 18、指针碰撞是什么?
- 19、如何触发垃圾回收?
- 20、什么是GC引入的对象模型?
- 21、谈谈你对Java内存模型(JMM)的理解?
- 22、自定义类加载器在哪些场景下会被使用?
前言
分享互联网大厂Java高频面试知识点(持续更新中)
一、Java基础题
1、Java语言的三大特性
1.封装
首先,属性可用来描述同一类事物的特征,方法可描述一类事物可做的操作。封装就是把属于同一类事物的共性(包括属性与方法)归到一个类中,以方便使用。
2.继承
使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承可以提高代码复用性。
3.多态
多态是以封装和继承为基础的,在抽象的层面上实施一个统一的行为,到个体(具体)的层面上时,这个统一的行为会因为 个体(具体)的形态特征而实施自己的特征行为。(针对一个抽象的事,对于内部个体又能找到其自 身的行为去执行。)
2、JDK 和 JRE 有什么区别
1.JDK:
Java Development Kit 的简称, Java 开发工具包,提供了 Java 的开发环境和运行环境。
2.JRE:
Java Runtime Environment 的简称, Java 运行环境,为 Java 的运行提供了所需环境。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序 调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。
3、Java基本数据类型及其封装类
为什么需要封装类?
因为泛型类包括预定义的集合,使用的参数都是对象类型,无法直接使用基本数据类型,所以Java又提供了这些基本类型的封装类。
基本类型和对应的封装类有本质的一些区别:
1.基本类型只能按值传递,而封装类按引用传递。
2.基本类型会在栈中创建,而对于对象类型,对象在堆中创建,对象的引用在栈中创建,基本类型由 在栈中,效率会比较高,但是可能存在内存泄漏的问题。
4、说明一下public static void main(String args[])这段声明里关键字的作用
public: main方法是Java程序运行时调用的第一个方法,因此它必须对Java环境可见。所以可见性设置为 public。
static: Java平台调用这个方法时不会创建这个类的一个实例,因此这个方法必须声明为static。
void: 表示方法没有返回值。
String:传进来参数的类型。
args:是指传进来的字符串数组。
5、java的数据结构有哪些?
Java的数据结构大致可以分为两种类型:线性结构和非线性结构。
1.线性结构:
数组:有序元素的序列,内存中的分配是连续的,通过下标访问元素,下标从0开始。
链表:由一系列节点组成,数据元素的逻辑顺序通过链表的指针地址实现。链表可分为单向链表、双向链表、循环链表等。
栈:特殊的线性表,只能在表的一端(栈顶)操作,遵循后进先出(LIFO)的原则。
队列:也是线性表,限制在表的一端进行插入,在另一端进行删除,遵循先进先出(FIFO)的原则。
散列表(哈希表):根据键(key)和值(value)直接访问的数据结构,通过key和value映射到集合中的一个位置,查找速度快。
2.非线性结构:
树:常见的包括二叉树、B树、B+树等。
堆:可以看作是用数组实现的二叉树,根据“堆属性”来排序。
6、抽象类和接口的区别?
1.定义:
抽象类:抽象类是一个不能被实例化的类,它可以包含抽象方法和非抽象方法。抽象类的主要目的是为其他类提供一个公共的基类,这些子类可以共享某些属性和行为。
接口:接口是一种完全抽象的类型,它只能包含抽象方法的声明,不能包含实例字段或实例方法的实现。接口主要用于定义行为,让类去实现这些行为。
2.实现:
抽象类:可以通过继承来实现,子类继承抽象类并可以选择性地实现其中的抽象方法。
接口:则通过类实现(implements)来实现,类必须实现接口中声明的所有方法。
3.方法实现:
抽象类:可以包含抽象方法和非抽象方法的实现。
接口:只能包含抽象方法的声明,不能包含方法的实现。
4.继承与实现:
一个类只能继承自一个抽象类(Java中的单继承)。
一个类可以实现多个接口(Java中的接口多实现)。
5.字段:
抽象类:可以有字段(成员变量),这些字段可以是私有的、受保护的、默认的或公共的。
接口:不能有任何字段(在Java 8及之前),从Java 8开始,接口可以包含静态和默认方法,但仍然不能包含实例字段。
6.设计目的:
抽象类:主要用于定义有相似属性和行为的类的模板,这些类之间可能存在一些细微的差异。
接口:主要用于定义一组行为,这些行为可以由多个不相关的类实现。
7.使用场景:
当你想在类之间共享代码并保留一些灵活性时,使用抽象类。
当你想定义一组行为,并且希望多个不相关的类实现这些行为时,使用接口。
7、== 与 equals 的区别
equals:是判断两个变量或实例所指向的内存空间的值是不是相同。
== : 它的作用是判断两个对象的地址是不是相等。
public static void main(String[] args) {
String t1 = "abc" ;
String t2 = t1;
String t3 = new String("abc");
String t4 = new String("abc");
String t5 = "abc";
System.out .println(t1 == t5 );
System.out .println(t1 == t2);
System.out .println(t3 == t4 );
System.out .println(t1 .equals(t2));
System.out .println(t3.equals(t4));
}
String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
8、String stringBuffer 和 stringBuilder 的区别是什么
String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。
StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
9、Java创建对象有几种方式?
- new创建新对象
- 通过反射机,制
- 采用clone机制
- 通过序列化机制
10、HashCode的作用的是什么?
HashCode它返回一个整数值,通常用于快速比较对象的内容。这个整数(哈希码)通常用于散列数据结构,如 HashMap,HashSet 等,以优化数据的存储和检索速度。
hashCode() 的主要作用有以下几点:
1.性能优化:在散列数据结构中,hashCode() 的主要目的是为对象生成一个独特的标识码,这个标识码用于确定对象在散列表中的存储位置。如果两个对象有相同的 hashCode() 值,那么它们的存储位置可能会相同(这种情况称为哈希冲突),但是它们的 equals() 方法必须返回 false,以防止数据错误。
2.快速比较:hashCode() 可以快速比较两个对象是否可能相等,而无需比较它们的具体内容。如果两个对象的 hashCode() 值不同,那么它们肯定不相等。这可以大大提高比较操作的效率。
3.一致性:如果两个对象根据 equals(Object) 方法是相等的,那么调用这两个对象的 hashCode 方法必须产生相同的整数结果。这是 hashCode() 方法的通用契约。
然而,值得注意的是,hashCode() 不一定要求为不同的对象生成不同的值。也就是说,两个不同的对象可能有相同的 hashCode() 值(这种情况称为哈希碰撞)。因此,在使用 hashCode() 进行对象比较时,还需要结合 equals() 方法来确保比较的准确性。
11、深拷贝和浅拷贝的区别是什么?
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝:被复制对象的所有变量都含有与原来的对象相同的值。而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
12、String str="abc"与 String str=new String(“abc”)一样吗?
不一样,因为内存的分配方式不一样。
String str=“abc”;常量池
String str=new String(“abc”) 堆内存
13、Java的四种引用分别是什么
强引用
强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,
代码示例:
String str = new string("str");
System.out.println(str);
软引用
软引用在程序内存不足时,会被回收。
代码示例:
SoftReference<String> wrf = new SoftReference<String>(new string("abc"));
弱引用
弱引用就是只要JVM垃圾回收器发现了它,就会将之回收。
WeakReference<String> wrf = new WeakReference<String>(str);
虚引用
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意哦,其它引用是被IVM回收后才被传入 Referencequeue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。
PhantomReference<String> prf = new PhantomReference<String>(new string("str"),new ReferenceQueue<>());
14、什么是隐式转换,什么是显式转换?
类型转换(Type Conversion)是指将一个数据类型的值转换为另一个数据类型的值。这种转换可以是隐式(Implicit)的,也可以是显式(Explicit)的。
隐式转换:
隐式转换是编译器自动进行的类型转换,通常是在不引起数据丢失或改变的情况下发生的。例如,将一个较小的数据类型(如int)转换为较大的数据类型(如long)时,编译器通常会隐式地进行这种转换,因为这样做不会丢失任何数据。
int a = 10;
long b = a; // 隐式转换,int 到 long
隐式转换通常发生在以下几种情况:
1.数值类型之间的转换,如将byte、short、char转换为int,或将int、float转换为double。
2.赋值语句中,当右侧表达式的类型与左侧变量类型不兼容时,且这种转换是安全的。
3.方法调用时,当传递的参数类型与方法签名中的参数类型不匹配,但可以进行安全转换时。
显式转换:
显式转换是程序员明确指定的类型转换,通常需要使用特定的语法或函数。这种转换通常在两种不兼容的数据类型之间进行,可能会导致数据丢失或改变。
double d = 3.14;
int i = (int) d; // 显式转换,double 到 int
在上述例子中,double类型的值3.14被转换为int类型,结果i将是3,因为小数部分被丢弃了。
显式转换通常发生在以下几种情况:
1.数值类型之间的转换,可能导致数据丢失或截断,如将double转换为int或float。
2.引用类型之间的转换,如将一个类的子类对象转换为父类对象(向上转型)或父类对象转换为子类对象(向下转型,需要确保对象实际是子类实例)。
3.使用特定的转换函数或方法,如String.valueOf()将其他类型转换为String。
15、String str = new String(“abc”) 与 String str ="abc"分别创建了几个对象?
String str = “abc” : 创建了1个或者0个对象。
说明:
1个:字符串产量池中不存在“abc”的情况下,这时会新建一个“abc"的字符串常量对象。
0个:字符串常量池存在“abc”,直接把str指向字符串常量池中的“abc"常量对象,不会新建对象。
String str = new String(“abc”) : 创建了1个或者2个对象。
说明:
首先在堆中创建一个实例对象new String, 并让a引用指向该对象。(创建第1个对象)
JVM拿字面量"abc"去字符串常量池试图获取其对应String对象的引用。
若存在,则让堆中创建好的实例对象new String引用字符串常量池中"abc"。
若不存在,则在堆中创建了一个"abc"的String对象,并将其引用保存到字符串常量池中,然后让实例对象new String引用字符串常量池中"abc"(创建2个对象)
16、什么情况需要Java序列化?
当 Java 对象需要在网络上传输 或者 持久化存储到文件中时。
17、Java 序列化中如何对一些字段不进行序列化?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用:阻止实例中那些用此关键字修饰的的变量序列化,当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
18、实例化数组后,能不能改变数组长度?
在Java中,一旦数组被实例化(即分配了内存空间并赋予了特定长度),数组的长度就不能被改变。这是因为数组在内存中是以连续的方式存储的,其长度在创建时就已经固定下来,并且与数组的内存分配直接相关。
19、什么是Java中的集合框架?它包含哪些主要接口和实现类?
1.集合框架是Java中提供的一组接口和类,用于存储和操作对象集合。
2.主要接口:Collection, List, Set, Map等。
3.主要实现类:ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap等。
20、List、Set、Map 之间的区别是什么?
List、Set、Map 的区别主要体现在两个方面:元素是否有序、是否允许元素重复。三者之间的区别,如下:
21、ArrayList和LinkedList的区别
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问效率:ArrayList比LinkedList在随机访问的时候效率要高,因为 LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作,LinkedList要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时更推荐使用 LinkedList。
22、HashMap和HashTable的区别
1.两者父类不同
HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
2.对外提供的接口不同
Hashtable比HashMap多提供了elments()和contains() 两个方法。 elments() 方法继承自Hashtable的父类Dictionnary。elements()方法用于返回此Hashtable中的value的枚举。contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上,contansValue()就只是调用了-下contains() 方法。
3.对null的支持不同
Hashtable:key和value都不能为null.
HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个key值对应的value为null。
4.安全性不同
HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自己处理多线程的安全问题。
Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。
虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全ConcurrentHashMap。
ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5.初始容量大小和每次扩充容量大小不同
23、 Queue 中 poll()和 remove()有什么区别?
相同点:都是返回第一个元素,并在队列中删除返回的对象。
不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
Queue<String> queue = new LinkedList<String>();
queue.offer("123");
System. out.println(queue.poll());
System. out. println(queue.remove());
System. out. println(queue.size());
24、HashMap的底层实现原理?
HashMap是Java集合框架中常用的数据结构,用于存储键值对(Key-Value)映射关系。其底层原理主要涉及数组、链表和红黑树这三种数据结构。
1.数据结构:HashMap的底层主要由数组和链表(或红黑树)组成。数组是HashMap的主干,每个元素都是一个链表节点,每个节点包含一个键值对以及指向下一个节点的指针。当HashMap的容量不足以容纳新的键值对时,会触发扩容机制,创建一个更大的数组来存储元素。
2.哈希值计算:当需要查找或插入一个元素时,HashMap首先会调用键对象的hashCode()方法获取哈希码,然后通过哈希函数将哈希码映射到数组的某个位置。在JDK 1.8之前,通常使用哈希码与数组长度取模后的余数作为数组下标;而在JDK 1.8及之后,使用了更加高效的哈希算法来减少哈希冲突的可能性。
3.解决哈希冲突:当两个不同的键具有相同的哈希码时,它们会被映射到数组的同一个位置,形成链表。为了解决这个问题,HashMap使用了链表法(Chaining)来处理哈希冲突。所有哈希值相同的元素作为同一个链表的节点,并按照插入顺序排列。当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,以提高查询效率。
4.扩容机制:当HashMap中的元素个数超过数组长度乘以负载因子时,会触发扩容机制。在JDK 1.7中,新的容量是原来的2倍,并重新计算每个元素的哈希值并插入到新的数组中。而在JDK 1.8中,扩容时不需要重新计算节点的哈希值,只需根据哈希值的高位判断节点在新数组中的位置。
5.性能特点:HashMap具有高效的存取性能,其查询速度在三种数据结构中最快,时间复杂度为O(1)。链表在数据量小的时候性能较好,但随着数据量增大,其性能逐渐下降,时间复杂度为O(N)。红黑树在数据量较大时性能优于链表,其时间复杂度为O(logN)。
需要注意的是,HashMap是非线程安全的,即多个线程同时修改HashMap可能会导致数据不一致。如果需要线程安全的映射结构,可以考虑使用Hashtable或ConcurrentHashMap等线程安全的实现类。
25、谈谈对ConcurrentHashMap的认识?
ConcurrentHashMap是Java集合框架中的一个线程安全的哈希表实现。它是concurrent包下的一个集合类,继承自AbstractMap类并实现了ConcurrentMap和Serializable接口。
ConcurrentHashMap的主要特点和用法:
1.线程安全:ConcurrentHashMap使用锁分段技术来保证线程安全。它将整个数据结构分成多个段(Segment),每个段都有一个锁来控制对该段的访问。这样,不同的线程可以同时访问不同的段,从而提高并发性能。
2.高效性能:ConcurrentHashMap在并发环境下具有良好的性能。它可以支持多个读操作同时进行,而不会阻塞其他线程的写操作。这使得它非常适合于高并发的应用场景。
3.可扩展性:ConcurrentHashMap可以根据需要动态地调整其容量大小。当容量达到预设的阈值时,ConcurrentHashMap会自动进行扩容操作,以保证高效的性能。
4.可调整的一致性:ConcurrentHashMap提供了不同的一致性级别来满足不同应用场景的需求。它可以使用弱一致性(weak consistency)来提高并发性能,也可以使用强一致性(strong consistency)来保证数据的完整性。
5.使用方式:ConcurrentHashMap的使用方式与HashMap类似,可以通过put(key, value)方法添加元素,通过get(key)方法获取元素。此外,它还提供了remove(key)方法来删除指定的键值对,并提供了多种遍历方式,如迭代器遍历、forEach遍历等。
需要注意的是,为了优化性能和线程安全,ConcurrentHashMap的构造函数需要三个参数:initialCapacity(初始容量)、loadFactor(加载因子)和concurrencyLevel(并发级别)。并发级别表示预计的同步更新线程的数量,用于在ConcurrentHashMap内部分为相应的分区,并创建相应数量的线程来保证线程安全。
26、红黑树有什么特征?
红黑树(Red-Black Tree)是一种自平衡的二叉查找树,具有以下几个主要特征:
1.每个节点都带有一个颜色属性,可以是红色或黑色。
2.根节点必须是黑色的。
3.所有叶子节点(NIL或NULL节点)都是黑色的。
4.每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)。
5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些特征保证了红黑树从根到叶子的最长可能路径不多于最短可能路径的两倍长,从而确保红黑树大致上是平衡的。这种平衡性使得红黑树在进行插入、删除和查找等操作时的最坏情况时间要求与树的高度成比例,因此红黑树能够在O(log n)时间内完成查找、插入和删除操作,这里的n是树中元素的数目。
27、列举几个常见的RuntimeException?
- Java.lang.NullPointerException 空指针异常。
- Java.lang.NumberFormatException 字符串转换为数字异常。
- Java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
- Java.lang.llegalArgumentException 方法传递参数错误。
- Java.lang.ClassCastException 数据类型转换异常。
28、Java中异常处理有哪两种方式?
1.try catch:
try{} 中放入可能发生异常的代码。catch{}中放入对捕获到异常之后的处理。
2.throw throws:
throw是语句抛出异常,出现于函数内部,用来抛出一个具体异常实例,throw被执行后面的语句不起作用,直接转入异常处理阶段。
throws是函数方法抛出异常,一般写在方法的头部,抛出异常,给方法的调用者进行解决。
29、Error与Exception区别?
1.基本概念:
Error:表示在正常情况下不可能发生的错误,这些错误会导致JVM(Java虚拟机)处于不可恢复的状态。由于这些错误是不可预料的,因此通常不可能被捕获并处理,例如OutOfMemoryError或NoClassDefFoundError等。
Exception:表示程序在正常运行过程中可能会遇到的错误,这些错误是可以预料的,并且应该被捕获并进行相应的处理。Exception是异常现象的一种,分为编译时异常和运行时异常。
2.分类:
Error:通常不需要程序员进行捕获和处理,因为它们是由JVM无法控制的因素引起的,例如硬件故障或系统崩溃等。
Exception:分为编译时异常和运行时异常。编译时异常是编译器在编译时能够检测到的异常,要求程序员必须进行处理,否则编译不通过。运行时异常则是在程序运行时才出现的异常,这些异常通常可以被程序员忽略。
3.处理方式:
Error:由于Error是不可恢复的,因此通常不需要(也无法)进行捕获和处理12。
Exception:编译时异常需要在编写代码时使用try-catch块进行捕获处理,或者通过throws关键字声明抛出。运行时异常可以在编写代码时不处理,系统会自动对其进行处理。
总结来说,Error和Exception的主要区别在于它们的性质、发生原因、分类以及处理方式。Error通常表示严重的、不可恢复的错误,而Exception则表示可预料的、需要被捕获并处理的异常现象1
30、java反射机制原理
Java反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能调用它的任意方法和属性,这种动态获取信息以及动态调用对象方法的功能称为Java的反射机制。
Java反射机制主要提供了以下功能:
1.在运行时判断任意一个对象所属的类。
2.在运行时构造任意一个类的对象。
3.在运行时判断任意一个类所具有的成员变量和方法。
4.在运行时调用任意一个对象的方法。
5.生成动态代理。
31、利用反射如何创建对象?
1.使用Class.forName()静态方法,需要传入类的全限定名。
2.使用.class语法,适用于基本数据类型和引用类型。
3.使用对象的getClass()方法,这需要有一个具体的对象实例。
32、利用反射动态创建对象实例?
public class ReflectionExample {
public static void main(String[] args) {
try {
// 指定要创建实例的类的完全限定名
String className = "com.example.MyClass";
// 加载类
Class<?> myClass = Class.forName(className);
// 获取无参数构造器
Constructor<?> constructor = myClass.getDeclaredConstructor();
// 设置为可访问(如果构造器是私有的)
constructor.setAccessible(true);
// 创建实例
Object instance = constructor.newInstance();
// 现在你可以将instance转换为适当的类型,并调用它的方法
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
33、IO模型有几种?
1.阻塞IO模型:在这种模型下,用户线程发送IO请求后,内核会查看数据是否就绪。在数据就绪之前,用户线程会处于阻塞状态,交出CPU。当数据就绪后,内核会拷贝数据到用户线程,并返回成功提示,用户线程解除阻塞状态。这是最简单和传统的IO模型,但性能较低,因为它会阻塞CPU的执行,导致CPU利用率低。
2.非阻塞IO模型:在这种模型下,用户线程发起read操作后,无需等待即可立即得到一个结果。如果数据没有准备好,会得到一个错误提示。当数据准备好且用户线程发送了IO请求时,内核会拷贝数据到用户线程。非阻塞IO模型可以提高系统的并发性,但需要不断轮询检查数据是否已准备好,导致CPU占用率较高。
3.IO多路复用模型:这是目前使用得较多的模型,例如Java NIO就是基于这种模型。在这种模型中,一个线程会不断轮询多个socket的状态,只有当socket真正有读写事件时,才会调用实际的IO读写操作。这种模型可以大大减少资源占用,提高系统性能。
4.信号驱动IO模型:用户线程发送一个IO请求操作后,会给对应的socket注册一个信号函数。当内核数据准备好时,会发送信号给用户线程,用户线程调用信号函数执行实际的IO操作。这种模型通常用于UDP。
5.异步IO模型:这是最理想的IO模型。用户线程发起请求后,无需关心实际的整个IO操作是如何进行的。当内核返回成功信号时,表示IO操作已经完成,用户线程可以直接使用数据。异步IO模型在内核中执行了实际的IO操作,无需用户线程参与,因此性能较高。但需要注意的是,异步IO模型需要操作系统的底层支持。
34、字节流与字符流的区别?
1.组成不同:字节流是由字节组成的,而字符流则是由字符组成的。在Java中,一个字符通常等于两个字节(采用UTF-16编码)。
2.处理方式不同:字节流主要用于处理二进制数据,它是按字节来处理的1。而字符流则是按虚拟机的字符编码来处理,这通常涉及到字符集的转化。例如,OutputStreamWriter是字符流通向字节流的桥梁,它使用指定的字符集将要写入流中的字符编码成字节。同样,InputStreamReader使用编码表对字节流中的数据进行解码。
3.使用场景不同:字节流是最基本的,用于处理所有类型的数据,包括文本、图像、音频等,因为所有类型的数据在计算机中都可以表示为字节序列。而字符流则主要为了方便读写文本文件,它只能读写文本文件,不能用于其他类型的文件读写。
35、谈谈对双亲委派机制的认识?
Java中的双亲委派机制(Parent Delegation Mechanism)是一种类加载机制。这种机制的工作方式是:当一个类加载器收到类加载请求时,它不会自己先去加载,而是将这个请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。
双亲委派机制的主要目的:是为了保证类的加载是有序的,避免重复加载同一个类,从而提高类加载的效率和安全性。此外,这种机制还允许开发人员自定义类加载器,实现特定的加载策略。
Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,负责加载Java核心类库。其他加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。
双亲委派机制的优势:
1.避免类的重复加载。
2.保护程序安全,防止核心API被随意篡改(沙箱安全机制,保护Java核心源代码)。
3.确保类的加载顺序和依赖关系,保证Java程序的稳定性。
需要注意的是,双亲委派机制并不是Java中唯一的类加载机制,但它是一种常用的、被广泛接受的机制。在某些特殊场景下,开发人员可能会选择打破这种机制,以实现特定的功能或优化。
二、多线程
1、什么是进程,什么是线程?
进程:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。进程拥有独立的内存空间,一般来说,进程不共享内存,因此进程之间的通信需要借助某些手段,如管道、消息队列、信号量、共享内存、Socket等。进程在执行过程中有三种基本状态,即就绪状态、执行状态和阻塞状态。
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存和消息传递的方式来实现。
2、java中实现多线程有几种方式?
1.继承Thread类:通过继承Thread类并重写其run()方法,可以创建新的线程。当调用start()方法时,会启动新线程并执行run()方法中的代码。
public class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
MyThread myThread = new MyThread();
myThread.start();
2.实现Runnable接口:通过实现Runnable接口并重写其run()方法,可以创建线程。与继承Thread类不同,这种方式下,需要将Runnable实现类的对象作为Thread构造函数的参数,然后调用Thread的start()方法启动线程。
public class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
3.使用Callable和FutureTask:通过实现Callable接口并使用FutureTask来创建线程。与Runnable接口不同,Callable接口的call()方法具有返回值,可以返回执行结果。
public class MyCallable implements Callable<String> {
public String call() {
// 线程执行的代码,并返回结果
return "result";
}
}
Callable<String> callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
// 获取执行结果
String result = futureTask.get();
4.使用线程池:通过Executor框架或ExecutorService来创建和管理线程池。这种方式可以更加灵活地创建和管理多个线程,并实现线程的复用。
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(new MyRunnable());
// 关闭线程池
executorService.shutdown();
3、Java中线程有几种状态?
1.新建(New):当一个线程被创建时,它处于新建状态。此时,线程对象已经创建,但还没有启动。在这个状态下,线程还没有开始执行任务
2.运行态(RUNNABLE):在Java中,运行态包括就绪态和运行态
①就绪(Runnable):当线程已经被启动并且没有在等待资源或执行任务时,它处于就绪状态。线程已经准备好运行,但是否真正执行取决于操作系统的调度。这个状态也被称为“可执行状态”。
②运行中(Running):当线程获得CPU资源并执行任务时,它处于运行中状态。此时,线程正在执行其run()方法中的代码。只有一个线程可以处于运行中状态,其他线程则需要等待或竞争资源
3.阻塞(Blocked):线程在等待获取一个锁以进入或重新进入同步代码块时,它会进入阻塞状态。只有当该锁被释放并且线程被调度去获取这个锁,线程才能转换到Runnable状态。
4.等待(Waiting):当线程被另一个线程所阻塞,等待某个条件成立或获得某个对象的监视器锁时,它处于等待状态。此时,线程正在等待某个事件发生才能继续执行。在Java中,通过调用Thread类的wait()方法使线程进入等待状态。
5.定时等待(Timed Waiting):当线程等待另一个线程执行特定操作或等待指定时间后继续执行时,它处于定时等待状态。在Java中,通过调用Thread类的sleep()方法或使用java.util.concurrent包中的工具类来使线程进入定时等待状态。定时等待是等待状态的一种特例,线程在等待时会设定等待超时时间,如超过了设定的等待时间,等待线程将自动唤醒进入Blocked状态或Runnable状态。
6.终止(Terminated):当线程完成执行任务或因异常终止时,它处于终止状态。此时,线程已经不再运行,并且无法再次被启动。在Java中,通过调用Thread类的interrupt()方法或使用异常来终止线程。
4、Thread 类中的start() 和 run()方法有什么区别?
1.start()方法:启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程终止。然后CPU再调度其它线程。
2.run ()方法:当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码。程序中只有主线程,这一个线程,其程序执行路径还是只有一条, 这样就没有达到写线程的目的。
5、java中线程的优先级?
在Java中,线程的优先级是通过整数表示的,范围从1到10。其中,1代表最低优先级,而10代表最高优先级。默认情况下,线程的优先级是5。
线程的优先级决定了线程在抢占CPU资源时的竞争情况。具有更高优先级的线程在一定程度上具有更高的执行几率,但并不保证它们始终能够在较低优先级的线程之前执行。线程的调度顺序还受到操作系统调度策略、CPU负载情况等因素的影响。
可以使用Thread类的setPriority方法来设置线程的优先级,并使用getPriority方法来获取线程的优先级。
6、notify和notifyAll的区别是什么?
1.唤醒线程的数量:
notify方法:只会随机唤醒一个正在等待该对象锁的线程(如果存在多个等待线程)。被唤醒的线程将尝试重新获取对象锁并继续执行。
notifyAll方法:会唤醒所有正在等待该对象锁的线程。所有被唤醒的线程将进入锁池竞争对象锁。一旦某个线程获取了对象锁,它就可以继续执行。
2.使用场景:
notify方法:适用于确信只有一个线程等待条件变量或者不关心哪个线程被唤醒的情况。
notifyAll方法:适用于所有等待线程都必须得到通知的情况,例如,多个线程等待不同的条件变量,而这些条件变量可能同时成立。
3.资源竞争:
notify方法:由于只唤醒一个线程,可能减少资源竞争。
notifyAll方法:由于唤醒所有等待线程,可能导致较高的资源竞争,因为所有被唤醒的线程都将竞争锁。
4.死锁风险:
使用notify方法时,如果多个线程等待不同的条件变量,并且每个条件变量只能由特定的线程来满足,那么notify可能只唤醒了一个错误的线程。这个被错误唤醒的线程可能因条件不满足而无法继续执行,从而导致所有等待的线程都无法继续执行,发生死锁。
为了避免死锁,通常建议在使用wait/notify机制时总是使用notifyAll方法,尽管这可能会带来性能上的开销,但它能确保在任何情况下所有能够继续执行的线程都会被唤醒。
7、sleep()和wait()有什么区别 ?
sleep()方法,是属于Thread类中的。wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
8、volatile关键字有什么作用?
1.可见性:当一个线程修改了一个volatile变量的值,新值对其他线程来说是立即可见的。这是因为volatile关键字会禁止CPU缓存和编译器优化,确保每次读取变量时都会直接从主内存中读取,而不是从本地缓存中读取。这样,当多个线程同时访问同一个volatile变量时,它们看到的值都是最新的。
2.禁止指令重排序:为了提高程序的性能,编译器和处理器可能会对指令进行重排序。然而,这可能会导致程序在多线程环境下的行为出现意外的结果。volatile关键字可以禁止指令重排序,确保程序的执行顺序与预期一致。
9、synchronized 和 ReentrantLock 有什么不同?
1.使用方式:
synchronized 可以用于修饰代码块、非静态方法和静态方法。当修饰代码块时,需要指定一个对象作为锁;当修饰方法时,对于非静态方法,锁的是调用该方法的对象实例;对于静态方法,锁的是该类的 Class 对象。
ReentrantLock 需要手动创建锁对象,并使用 lock() 方法进行加锁,在代码执行完毕后使用 unlock() 方法释放锁。它通常用于代码块,而不是整个方法。
2.锁的获取和释放:
synchronized 是自动的,当进入同步代码块或方法时,锁会自动被获取,当离开同步代码块或方法时,锁会自动被释放。
ReentrantLock 需要手动获取和释放锁,如果不正确地释放锁(例如在异常情况下),可能会导致死锁。
3.锁类型:
synchronized 是非公平锁,意味着等待的线程获取锁的顺序不是按照它们到达的顺序。
ReentrantLock 既可以是公平锁也可以是非公平锁。默认情况下,它是非公平锁,但可以通过构造函数设置为公平锁。
4.扩展性:
synchronized 是 JVM 内置的,因此其功能相对固定,不容易扩展。
ReentrantLock 提供了更多的灵活性和扩展性,例如它支持中断锁定的线程,而 synchronized 不支持
10、SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap:通过在整个方法上加锁来保证线程安全,它使用synchronized关键字来保护每个方法,使得同一时间只有一个线程能够访问该对象。
ConcurrentHashMap:采用了更细粒度的锁机制,即分段锁(Segment)。它将整个Map分成多个Segment,每个Segment都有自己的锁,这样不同Segment上的操作互不影响,支持多个线程并发访问不同的Segment。
在并发访问情况下,ConcurrentHashMap的性能通常比SynchronizedMap好,因为它允许多个线程同时读取不同部分的数据,而SynchronizedMap则只能同一时间让一个线程访问整个对象。
11、当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法?
1.其他方法前是否加了 synchronized 关键字,如果没加,则能。
2.如果这个方法内部调用了wait,则可以进入其他 synchronized 方法。
3.如果其他个方法都加了synchronized 关键字,并且内部没有调用 wait,则不能。
4.如果其他方法是 static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是 this。
12、Synchronized与Lock的区别?
1.实现方式:
Synchronized是Java语言的一个关键字,它在JVM层面由内置语言直接实现。
Lock是一个接口或类,属于JDK层面实现的接口。
2.锁的释放:
如果线程在执行synchronized代码块时发生异常,JVM会自动释放锁,因此不会导致死锁。
对于Lock,如果线程发生异常,锁不会自动释放,必须手动在finally块中释放锁。
3.锁的类型和状态:
Synchronized是非中断锁,意味着线程必须等待直到锁被释放;它不能判断锁是否被占用,并且它的公平性取决于JVM的实现(通常是非公平的)。
Lock接口允许更灵活的操作,如可以中断等待获取锁的线程,可以判断锁是否被占用,并且可以通过构造函数指定公平性(公平或非公平)。
4.性能:
Synchronized通常用于少量代码的同步,因为它在JVM层面上提供了内置的支持。
Lock接口通常用于大量代码的同步,并且它提供了读锁,这可以提高多线程读取的效率。
5.灵活性:
Synchronized的使用相对固定,不能中断等待锁的线程,也不能尝试获取锁。
Lock接口提供了更多的灵活性,允许手动控制锁的获取和释放,以及中断等待锁的线程
13、线程池中submit()和execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中,而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
14、java中常见的线程池有哪些?
1.newFixedThreadPool:创建一个指定工作线程数量的线程池。
2.newCachedThreadPool:创建一个可缓存线程池。
3.newSingleThreadExecutor:创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务。
4.newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行。
5.newSingleThreadScheduledExecutor:创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。
15、进程死锁和线程死锁的区别是什么?
进程死锁和线程死锁在本质上都是由于资源竞争和互斥条件导致的系统状态,其中多个进程或线程无法继续执行,因为它们都在等待对方释放资源。然而,它们之间有一些关键的区别:
1.发生层次:
进程死锁:发生在进程级别。进程是程序的一次运行实例,具有独立的地址空间和其他系统资源。当多个进程因为竞争资源而陷入互相等待的状态时,就会发生进程死锁。
线程死锁:发生在线程级别。线程是进程中的一个执行分支,它们共享进程的资源(如地址空间),但有自己的栈和寄存器。当多个线程因为竞争资源或锁而陷入互相等待的状态时,就会发生线程死锁。
2.资源范围:
进程死锁涉及的是进程间的资源竞争,这些资源通常是系统级别的,如内存、文件、设备等。
线程死锁涉及的是线程间的资源竞争,这些资源通常是进程内部的,如对象锁、变量等。
3.影响范围:
由于进程是独立的,进程死锁的影响范围通常比线程死锁更广。当发生进程死锁时,整个进程都会被阻塞,这可能导致系统性能下降或任务失败。
线程死锁的影响范围局限于进程内部。当一个线程死锁时,其他线程可能仍然可以继续执行,除非它们也涉及到相同的资源竞争。
16、线程池的任务是顺序执行的吗?
在Java的线程池中,任务通常按照先进先出(FIFO)的顺序执行。当有新任务添加到线程池中时,如果当前线程数小于核心线程数,则会创建新的线程并立即执行该任务。如果当前线程数等于核心线程数,新任务会被加入到工作队列中,等待现有线程执行完毕后才能执行。工作队列中的任务是按照先进先出的顺序执行的。
然而,如果线程池使用了优先级队列,任务的执行顺序就会按照优先级从高到低的顺序执行。任务的优先级由用户自定义,可以通过实现Comparable或Comparator接口并覆写compareTo或compare方法来实现。
此外,线程池中的任务执行顺序还与任务的性质有关。例如,一些任务可能需要等待其他任务完成后才能执行,这也会影响任务的执行顺序。
总之,线程池的任务执行顺序不是固定的,而是根据任务提交的方式、线程池的配置和任务本身的性质来决定的。在实际应用中,需要根据具体需求来选择合适的线程池配置和任务提交方式,以确保任务能够按照预期的顺序执行。
17、在java中什么是CAS 什么是AQS?
CAS,即比较并交换,是一种非阻塞式并发控制技术。
CAS包含三个操作数:内存位置、预期原值及更新值。当执行CAS操作时,会将内存位置的值与预期原值进行比较。如果这两个值相匹配,那么处理器会自动将该位置的值更新为新值;如果不匹配,处理器则不做任何操作。这种机制可以在不使用锁的情况下实现数据的同步和并发控制,避免了传统锁机制在高并发场景下可能带来的性能问题。CAS操作通过硬件保证了比较-更新的原子性,是一条CPU的原子指令(如cmpxchg指令),因此它更高效且可靠。
在Java中,CAS主要是通过java.util.concurrent.atomic包下的一些类和方法来实现的,例如AtomicBoolean类提供的compareAndSet等方法。
AQS,即抽象队列同步器,是Java并发编程框架中的一个核心类,位于java.util.concurrent.locks包中。它用于构建各种同步器,如锁、信号量、条件变量等。
AQS的核心思想是使用一个队列来管理对共享资源的访问。当线程请求访问共享资源时,如果资源空闲,则将线程设置为有效的工作线程,并将资源标记为已占用;如果资源被占用,则将线程加入队列并阻塞,直到资源空闲并被唤醒。
AQS内部有一个state变量和一个双向链表Node,每个Node内部又有一个变量thread来记录当前线程。
18、什么是ABA问题?
因为cas需要在操作值的时候,检查值有没有变化,如果没有变化则更新,如果一个值原来是A,变成了B,又变成了A,那么使用cas进行检测时会发现发的值没有发生变化,其实是变过的。
解决:
添加版本号,每次更新的时候追加版本号,A-B-A->1A-2B-3A。
从jdk1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。
19、ThreadLocal的作用?
ThreadLocal 在 Java 中主要用于解决多线程环境下的数据共享问题,同时保持每个线程对共享数据的独立性。通过 ThreadLocal,你可以在多个线程之间维护独立的变量副本,这样每个线程就可以独立地改变自己的副本,而不会影响其他线程。
主要作用:
1.线程隔离的数据存储:在多线程环境中,ThreadLocal 提供了一种为每个线程创建独立变量副本的机制。这允许你在不同线程间保持数据的隔离性,防止数据的不当共享和竞争条件。
2.避免线程安全问题:使用 ThreadLocal 可以避免在多线程环境中对共享变量的同步操作,从而减少了线程同步带来的性能开销。
3.存储线程上下文信息:ThreadLocal 常常用于存储线程上下文信息,如用户会话信息、事务 ID、数据库连接等。这些信息是线程特有的,并且需要在线程执行期间保持可用。
4.减少内存消耗:尽管 ThreadLocal 为每个线程创建了变量副本,但如果正确地使用和管理,这种开销通常是可以接受的。然而,如果不正确地使用(例如,不删除不再需要的变量),可能会导致内存泄漏。
public class ThreadLocalUsage {
// 创建一个 ThreadLocal 实例
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 创建并启动线程
Thread thread1 = new Thread(() -> {
threadLocal.set("Thread 1 data"); // 设置线程特有的数据
processData(); // 处理数据
});
Thread thread2 = new Thread(() -> {
threadLocal.set("Thread 2 data"); // 设置线程特有的数据
processData(); // 处理数据
});
thread1.start();
thread2.start();
}
// 这个方法访问并使用 ThreadLocal 中存储的数据
public static void processData() {
String data = threadLocal.get(); // 获取线程特有的数据
System.out.println(Thread.currentThread().getName() + " is processing " + data);
}
}
20、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
乐观锁(Optimistic Lock)
理解:
乐观锁基于一种乐观的假设,即认为多个线程同时修改共享资源的概率很小。因此,在数据处理过程中,它不会直接锁定数据。只是在更新数据时,会判断在此期间有没有其他线程修改过这个数据,通常通过版本号或时间戳等机制来实现。
实现方式:
1.版本号机制:在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。更新时检查版本号是否一致,如果一致则可以更新,否则说明有其他程序已更新该记录,返回错误。
2.CAS(Compare And Swap)机制:CAS操作包括三个操作数:需要读写的内存位置(V)、进行比较的预期值(A)和拟写入的新值(B)。如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。
应用场景:乐观锁适用于多读的应用类型,可以提高吞吐量,特别是当系统的并发非常大时,悲观锁定会带来非常大的性能问题,此时选择乐观锁进行并发控制更为合适。
悲观锁(Pessimistic Lock)
理解:
悲观锁基于一种悲观的假设,即认为多个线程同时修改共享资源的概率很高。因此,在数据处理过程中,它总是会将数据锁定,以防止其他线程同时修改。
实现方式:
1.数据库提供的锁机制:如行锁、表锁等。在对任意记录进行修改前,先尝试为该记录加上排他锁。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。如果成功加锁,则可以对记录做修改,事务完成后就会解锁。
2.代码层面的锁:如Java中的synchronized关键字,它可以从偏向锁、轻量级锁到重量级锁,都是悲观锁的实现。
应用场景:悲观锁的策略过于保守,并发性能不好而且有产生死锁的风险,因此一般用于多写的场景,并且需要慎重使用。
21、简述一下CyclicBarrier和CountDownLatch的区别 ?
两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
1.CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行。CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
2.CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。
3.CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。
22、Callable和Future的区别是什么?
Callable:
1.Callable是Java中的一个接口,类似于Runnable接口,但它允许定义的方法有返回值,并且可以抛出异常。
2.实现Callable接口的类可以被其他线程执行,并在执行后返回一个结果。
3.Callable通常与线程池一起使用,用于执行可能需要返回结果的任务。
Future:
1.Future是一个代表异步计算结果的接口,它提供了检查计算是否完成的方法,并允许等待计算完成并检索其结果。
2.Future通常与Callable一起使用,当你提交一个Callable任务到线程池时,线程池会返回一个Future对象,你可以使用这个对象来检查任务是否完成,以及获取任务的结果。
3.Future还提供了取消任务执行的方法,并允许你获取任务执行过程中抛出的异常。
简而言之,Callable是一个可以返回结果的任务接口,而Future是代表这个异步计算结果的接口,提供了对异步计算的控制和结果访问。当你在线程池中提交一个Callable任务时,你会得到一个Future对象,这个对象可以用来跟踪和控制任务的执行,并获取最终的计算结果。
23、多线程情况下,如果对一个数进行叠加,该怎么做?
1.使用 synchronized / ReentrantLock 加锁
2.使用 AtomInteger 原子操作
24、实际开发中,线程池的线程数量如何确定?
线程池的大小应该根据任务的性质、系统资源、性能要求和并发需求来确定。以下是一些方法来确定线程池的数目:
1.任务性质:
CPU密集型任务:对于这类任务,线程池的大小通常应等于CPU的核心数。这是因为CPU密集型任务主要消耗CPU资源,同时进行的任务数量应当与CPU的核心数相匹配,以充分利用CPU资源。
IO密集型任务:这类任务大部分时间都在等待IO操作完成,如网络请求或文件读写。对于IO密集型任务,线程池的大小通常可以设置为CPU核心数的两倍,以提高CPU的利用率。
2.系统资源:
考虑系统的可用内存、网络sockets等资源。如果资源有限,可能需要限制线程池的大小,以防止资源耗尽。
3.性能要求:
根据系统的性能要求进行调整。如果系统需要高吞吐量,可能需要增加线程池的大小。如果系统需要低延迟,可能需要减少线程池的大小。
默认值情况:
如果不确定如何设置线程池的大小,可以使用一些默认值。例如,线程数一般取CPU核心数加2(即CPU核心数 + 2)可能是一个合理的起点。
25、使用ThreadPoolExecutor进行自定义线程池的参数有哪些,分别表示什么意思?
1.corePoolSize(核心线程数):
①.表示线程池中最少需要保持的活动线程数。
②.当有新任务提交到线程池时,如果当前活动线程数小于corePoolSize,线程池会立即创建一个新的线程来处理该任务。
③.即使这些线程空闲时,也不会被销毁,除非设置了allowCoreThreadTimeOut。
2.maximumPoolSize(最大线程数):
①.表示线程池允许创建的最大线程数。
②.当工作队列满了,且已创建的线程数小于maximumPoolSize时,线程池会创建新的线程来处理任务。
③.当这个参数设置后,如果队列满了且线程数达到这个值,线程池将不再创建新线程。
3.keepAliveTime(空闲线程存活时间):
①.表示线程池中非核心线程空闲时的存活时间。
②.当线程池中的线程数大于corePoolSize时,多余的空闲线程如果在指定时间内没有被使用,就会被销毁。
③.这个参数有助于减少资源的浪费。
4.unit(时间单位):
①.与keepAliveTime配合使用,指定存活时间的单位,如秒、分钟等。
5.workQueue(工作队列):
①.用于存放待执行的任务。
②.当所有核心线程都在忙时,新提交的任务会放入工作队列中等待执行。
③.队列可以是有界的,也可以是无界的。有界队列可以避免任务过多导致内存溢出,但可能会导致任务被拒绝;无界队列可以保证所有任务都能被执行,但可能会导致内存占用过高。
6.threadFactory(线程工厂):
①.用于创建新线程。
②.通过自定义线程工厂,可以给创建的线程设置一些属性,如线程名称、线程组、优先级等。
7.rejectedExecutionHandler(拒绝策略):
①.当线程池和队列都满了,新提交的任务的处理方式。
②.Java提供了几种内置的拒绝策略,如AbortPolicy(默认策略,直接抛出异常)、CallerRunsPolicy(由调用者自己运行任务)、DiscardPolicy(丢弃新任务)和DiscardOldestPolicy(丢弃队列中最老的任务)等
26、产生死锁的几个必要条件?
1.互斥条件:系统中存在一个资源一次只能被一个进程所使用。也就是说,一个资源在任何时刻只能被一个线程占用。
2.占有且等待条件:也称为请求与保持条件,即一个进程已占有了至少一个资源,但又因为请求其他资源被阻塞,且对已获得的资源保持不放。也就是说,拿到一个锁后都要等待另一个锁。
3.不可被抢占条件:也称为不剥夺条件,即资源只能被占有它的进程所释放,而不能被别的进程强行抢占。也就是说,其他线程不可强行抢占已被某个线程锁占有的资源。
4.循环等待条件:在系统中存在一个由若干进程形成的环形请求链,其中的每一个进程均占有若干种资源中的某一种,同时每一个进程还要求链上下一个进程所占有的资源。也就是说,T1等待T2占有的资源,T2等待T1占有的资源。
27、Java中的锁机制包括多种类型和实现方式是什么?
1.锁的概念和作用:
①.锁是一种同步机制,用于协调多个线程的并发访问,以保证对共享资源的安全访问。
②.锁的作用可以概括为两个方面:保证线程安全和提高性能。
2.锁的类型:
①.互斥锁(Mutex):最基本的锁机制,也称为独占锁。同一时刻只能有一个线程占用锁,其他线程必须等待锁被释放。Java中的synchronized和ReentrantLock都是互斥锁的实现方式。
②.读写锁(ReadWriteLock):一种特殊的锁机制,用于控制读写操作。在同一时刻,可以允许多个线程对共享数据进行读操作,但只允许一个线程进行写操作。Java中的ReentrantReadWriteLock就是读写锁的实现方式。
③.自旋锁(SpinLock):一种特殊的互斥锁,用于解决轻量级的并发问题。当一个线程需要占用锁时,如果锁已被其他线程占用,则该线程不会进入阻塞状态,而是一直等待直到锁被释放。Java中的AtomicInteger就是自旋锁的典型应用。
④.公平锁和非公平锁:根据线程获取锁的顺序是否遵循公平性原则来区分。公平锁保证线程按照请求的顺序获取锁,而非公平锁则没有明确的顺序规定。
3.锁的实现方式:
①.synchronized:Java中最基本、最常用的锁机制。可以修饰代码块、方法和静态方法,用来保证多个线程对共享资源的安全访问。
②.ReentrantLock:Java中比synchronized更灵活的互斥锁实现方式。提供了更多的功能,如可中断锁、公平锁等。
4.锁的状态:
Java中的锁有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级,目的是为了提高锁和释放锁的效率。
28、实际项目中什么场景可以使用信号量Semaphore?
1.线程同步:当多个线程需要访问共享资源或执行一系列相互依赖的操作时,可以使用信号量进行同步。信号量可以确保线程按照一定的顺序执行,避免竞态条件(race condition)和其他并发问题。
2.资源计数:信号量可以用作一个递增或递减的计数器,以记录可用资源的数量。当线程访问共享资源时,信号量会递减;当线程释放资源时,信号量会递增。这有助于确保资源不会被过度使用。
3.中断与线程同步:在中断驱动的系统中,信号量可以用于同步中断处理程序和线程。当中断触发时,中断服务程序可以通过释放信号量来通知线程进行后续处理。
4.生产者-消费者问题:在生产者-消费者问题中,信号量可以用于同步生产者和消费者线程。生产者线程在生成新数据项时会增加信号量的值,而消费者线程在消费数据项时会减少信号量的值。这可以确保生产者和消费者之间的同步,避免生产速度过快导致缓冲区溢出,或消费速度过快导致缓冲区为空。
5.避免竞态条件:竞态条件是多线程编程中常见的问题,可能导致数据不一致或其他未定义行为。使用信号量可以避免这种情况,因为它可以控制对共享资源的访问。
29、Java中的守护线程和用户线程的区别?
1.生命周期:守护线程的生命周期取决于是否存在任何前台线程(即用户线程)。当所有的前台线程都结束时,守护线程会自动退出。相反,用户线程不会阻止JVM的退出,即使所有守护线程都已结束,只要还有一个或多个用户线程在运行,JVM就会继续运行。
2.用途:守护线程通常用于执行一些支持性或系统服务性任务,如垃圾回收、周期性任务等。而用户线程通常用于执行应用程序的自定义逻辑。
3.对JVM的影响:如果只剩下守护线程在运行,JVM会正常退出。相反,只要任何一个用户线程未结束,Java虚拟机就不会结束。
4.I/O操作:由于守护线程的生命周期不受控制,因此不建议在这些线程中执行涉及I/O操作的任务。因为I/O操作可能需要等待外部资源,这可能导致线程无限期地等待,从而阻止JVM的正常退出。
30、Java中的可见性、原子性、有序性?它们如何影响并发编程?
可见性(Visibility)
可见性是指一个线程对共享变量的修改对其他线程是立即可见的。在Java中,由于每个线程都有自己的本地内存或工作内存,它们从主内存中拷贝变量的副本。当一个线程修改了这个变量的值,这个修改不会立即反映到其他线程的本地内存中。为了解决这个问题,Java提供了synchronized和volatile关键字来确保可见性。
synchronized:当一个线程进入同步块或同步方法时,它会从主内存中获取变量的最新值,并在退出同步块或同步方法时将变量的值刷新回主内存。
volatile:声明一个变量为volatile可以确保修改的值会立即被更新到主内存,当有其他线程需要读取时,它会去主内存中读取新值。
原子性(Atomicity)
原子性是指一个或多个操作在并发环境中被视为一个单一不可分割的单位。即这些操作要么全部执行,要么全部不执行。Java中的synchronized和java.util.concurrent.atomic包中的原子类(如AtomicInteger、AtomicLong等)可以确保原子性。
synchronized:当一个线程进入同步块或同步方法时,其他线程无法进入该同步块或同步方法,从而确保代码块的原子性执行。
Atomic类:java.util.concurrent.atomic包中的原子类提供了线程安全的原子操作,如incrementAndGet()、decrementAndGet()等,这些操作是不可中断的,因此具有原子性。
有序性(Ordering)
有序性是指程序执行的顺序按照代码的先后顺序执行。由于JVM和硬件的优化,可能会导致指令的执行顺序与预期的顺序不一致,从而影响并发编程的正确性。Java中的volatile和synchronized可以保证一定的有序性。
volatile:禁止JVM和硬件对指令进行重排序。
synchronized:确保每个时刻只有一个线程执行同步代码块或同步方法,从而保证了代码的有序性。
这三个特性在并发编程中起着至关重要的作用:
可见性:确保线程之间能够正确地共享和读取数据,防止脏读(读取到其他线程尚未写入主内存的数据)和幻读(一个线程读取到另一个线程之前读取过的数据)。
原子性:确保在并发环境中,代码块或操作不会被其他线程中断,从而保持数据的完整性和一致性。
有序性:确保程序按照预期的顺序执行,防止由于指令重排序导致的数据不一致或其他并发问题。
三、JVM
1、JVM内存模型结构?
2、JVM运行时内存区域划分是什么?
1.程序计数器:
这是一个较小的内存空间,用作当前线程所执行的字节码的行号指示器。
每个线程都有自己独立的程序计数器,因此它是线程私有的。
程序计数器用于存储下一条将要执行的指令的地址,确保线程切换后能恢复到正确的执行位置。
2.Java虚拟机栈:
与线程同时创建,用于存储方法执行时的局部变量、操作数栈、动态链接和方法出口等信息。
每个方法在执行时都会创建对应的栈帧,栈帧包括局部变量表、操作数栈、动态链接、方法返回地址等信息。
栈帧的大小是提前定义好的,可以动态扩展,但不会超出最大限制。
3.本地方法栈:
与Java虚拟机栈类似,但它主要为执行native方法(即使用本地语言如C或C++编写的方法)服务。
4.Java堆:
Java堆是Java虚拟机中最大的一块内存区域,被所有线程共享。
它是用来存放对象实例的地方,也是垃圾收集器进行垃圾回收的重点区域。
Java堆可以动态扩展,通过-Xmx和-Xms参数控制其最大和初始大小。
5.方法区:
方法区也是所有线程共享的内存区域,用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
3、Java堆内存组成?
新生代(Young Generation):又分为一个Eden空间和两个Survivor空间(From Survivor和To Survivor) 新创建的对象通常会被分配到这里,随后随着垃圾回收(GC),如果对象还活着,则会被晋升到老年代。
老年代(Old Generation):新生代中经过多次垃圾回收仍然存活的对象会被移动到这里。
永久代(Perm Generation,Java 8 之前)或元空间(Metaspace,Java 8 及之后):存储类信息、常量池等元数据。
4、Edem:from:to默认比例是多少?
Edem : from : to=8: 1 :1
此比例可以通过 -XX:SurvivorRatio 来设定
5、JVM(Java虚拟机)中常见的垃圾收集算法有哪些?
1.标记-清除算法(Mark-Sweep):
①.这是最基本的垃圾回收算法。它分为两个阶段:标记阶段和清除阶段。
②.在标记阶段,垃圾回收器会遍历所有对象,并标记存活的对象。
③.在清除阶段,垃圾回收器会清除未被标记的对象,并释放其内存。
④.该算法适用于存活对象较多的情况,特别是在老年代(即旧生代)。
⑤.缺点是容易产生内存碎片,当需要分配较大对象时,可能会提前触发垃圾回收。
2.复制算法(Copying):
①.该算法将内存划分为两块,每次只使用其中一块。当这块内存满了,就将里面的对象复制到另一块内存,然后清空原来的内存块。
②.现在的商业虚拟机都采用这种收集算法来回收新生代。
③.优点是简单高效,不会产生内存碎片。
④.缺点是可用内存大小缩小为原来的一半,当对象存活率高时,会频繁进行复制。
3.标记-压缩算法(Mark-Compact):
①.该算法在标记-清除算法的基础上进行了优化。在标记存活对象后,它将所有存活对象压缩到内存的一端,然后清理边界外的空间。
②.该算法既避免了碎片的产生,又不需要两块相同的内存空间,因此性价比比较高。
4.分代收集算法(Generational Collection):
①.根据对象的存活周期将内存划分为几块,如年轻代和老年代。年轻代中每次回收对象较多,可以使用复制算法;而老年代因为存活的对象较多,则可使用标记-清除或标记-压缩算法3。
②.这种算法充分利用了不同代对象的特点,提高了垃圾回收的效率。
5.引用计数算法:
①.每个对象都有一个引用计数器,每当有一个地方引用该对象时,计数器就加一;每当引用失效时,计数器就减一。当计数器为零时,该对象就可以被回收。
6、JVM中有哪些常见的垃圾收集器?
1.Serial收集器:
这是一个单线程工作的收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到收集结束。
适用于内存小、单核处理器的环境,特点是简单高效。
2.ParNew收集器:
实质上是Serial收集器的多线程版本,可以同时采用多条线程进行垃圾处理。
通常与CMS垃圾收集器结合使用,主要用于减少垃圾收集时的停顿时间。
3.Parallel Scavenge收集器:
一款新生代收集器,基于标记复制算法,能够并行收集。
它的目标是达到一个可控的吞吐量,而不是尽可能缩短垃圾收集时的用户线程停止时间。
4.Serial Old收集器:
Serial收集器的老年代版本,也是一个单线程的收集器,使用标记-整理算法。
在JDK 1.5及之前的版本中,与Parallel Scavenge收集器搭配使用;也是CMS收集器失败之后的后预案。
5.Parallel Old收集器:
Parallel Scavenge的老年代版本,支持并发收集,基于标记-整理算法实现。
在注重吞吐量或处理器资源较为稀缺的场合,优先选取Parallel Scavenge + Parallel Old的搭配方式。
6.CMS(Concurrent Mark-Sweep)收集器:
是一种以获取最短停顿时间为目标的垃圾收集器,基于标记-清除法的垃圾收集器。
适用于减少应用程序停顿时间的场景,但可能存在内存碎片问题。
7.G1(Garbage-First)收集器:
面向大堆内存和多核处理器的收集器,将堆内存分割成多个区域进行回收,具有可预测的停顿时间。
适用于大堆内存和需要低延迟的应用程序,能够在多核处理器上发挥更好的性能。
8.ZGC(Z Garbage Collector):
面向大堆内存、低延迟的垃圾收集器,通过并发压缩来减少内存碎片。
适用于需要快速响应和低停顿时间的应用场景,在大堆内存和多核环境下表现出色
7、JVM中常见的垃圾收集器组合?
1.Serial收集器 + Serial Old收集器:适用于小型应用程序和移动设备。
2.Parallel收集器 + Parallel Old收集器:适用于多核CPU的应用程序。
3.CMS收集器 + Serial Old收集器:适用于大型应用程序和需要短暂暂停的应用程序。
4.CMS收集器 + Parallel Old收集器:适用于大型应用程序和需要更高的吞吐量的应用程序。(一般没有这个组合)
5.CMS收集器 + Parallel New收集器:适用于大型应用程序和需要更高的吞吐量的应用程序。(Parallel Old专门针对CMS 改进的收集器 )
6.G1收集器:适用于大型应用程序和需要更可预测的暂停时间的应用程序
8、描述一下JVM加载class文件的原理机制?
1.加载:
通过类的全限定名来获取定义的二进制字节流。这个二进制字节流代表了类的静态存储结构1。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在Java堆中生成一个代表这个类的java.lang.Class对象。这个对象作为对方法区内这些数据的访问入口。
加载类的方式可以是从本地系统直接加载、通过网络下载.class文件、从zip、jar等归档文件中加载.class文件、从专有数据库中提取.class文件,甚至还可以将Java源文件动态编译为.class文件(例如,在服务器环境下)。
2.连接:
验证:确保.class文件中的字节流信息符合JVM规范,并且不会危害到JVM自身的安全。这包括文件格式验证、元数据验证、字节码验证和符号引用验证。
准备:为类的静态变量分配内存,并设置其初始值(不包括实例变量)。
解析:将符号引用转换为直接引用。
3.初始化:
为类的静态变量赋予初始值。
在这个过程中,JVM的类加载是通过ClassLoader及其子类来完成的。类的层次关系和加载顺序由ClassLoader的层次结构决定。最顶层的加载器是BootstrapClassLoader,它负责加载JAVA_HOME中jre/lib/目录下的类库。
值得注意的是,JVM的类加载具有双亲委派模型的特点,即当一个类收到了类加载请求时,它不会自己先去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。
只有当父类加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
JVM的类加载机制确保了Java的动态扩展性,因为可以通过预定义的类加载器和自定义的类加载器在运行时从各种来源加载.class文件。
9、引用计数法?可达性分析?
引用计数法(Reference Counting):
定义:引用计数是一种内存管理技术,它通过跟踪每个资源(如对象、内存块等)的引用次数来工作。每当一个资源被引用时,其引用计数增加;每当引用失效或被删除时,引用计数减少。当引用计数减少到0时,该资源被视为不再使用,可以被安全地释放或回收。
优点:引用计数法的优点是垃圾回收的判定效率较高,因为一旦一个对象的引用计数为0,就可以立即将其回收。此外,它不需要像可达性分析那样暂停所有应用线程(Stop-The-World)。
缺点:然而,引用计数法也有其局限性。最显著的问题是它无法处理循环引用的情况,即两个或多个对象相互引用,即使它们实际上不再被程序的其他部分使用。这种情况下,这些对象的引用计数永远不会为0,从而导致内存泄漏。
可达性分析(Reachability Analysis):
定义:可达性分析是JVM中常用的另一种垃圾收集策略。它基于一个基本假设,即如果一个对象无法从一组预定义的根对象(GC Roots)通过引用链到达,那么这个对象就是不可达的,因此可以被安全地回收3。GC Roots通常包括活动线程、静态变量、本地方法栈等。
工作原理:可达性分析从GC Roots开始,遍历整个对象图,沿着引用链向下搜索。如果一个对象可以从至少一个GC Root通过引用链到达,那么这个对象被认为是可达的,即它仍然被应用程序中的活动对象所引用,因此不应该被回收。
反之,如果一个对象无法通过任何引用链与任何GC Roots相关联,那么它就被认为是不可达的,可以被安全地回收。
优点:可达性分析法可以处理循环引用的情况,因为它不依赖于引用计数,而是基于对象之间的引用关系来确定哪些对象是可达的,哪些是不可达的。
在JVM中,特别是在Java堆中,可达性分析是主要的垃圾收集策略,它通常与各种垃圾收集器(如CMS、G1等)一起使用。然而,引用计数法在某些情况下也可能被用作辅助手段或用于特定的内存管理场景。
10、jvm中一个对象如何才会被垃圾收集器回收?
1.对象不再被引用:当一个对象没有任何引用指向它时,它就被认为是无用的。这通常通过可达性分析算法来判断。
可达性分析算法从一组称为“GC Roots”的对象开始,搜索这些对象所引用的所有对象。如果某个对象到GC Roots之间没有任何引用链相连,那么该对象就被认为是不可达的,因此可以被回收。
2.对象所属的内存区域需要被回收:JVM的内存被划分为不同的区域,如新生代、老年代等。每个区域都有自己的垃圾收集策略。例如,新生代中的对象主要通过Minor GC进行回收,而老年代中的对象则通过Old GC或Full GC进行回收。
当某个内存区域中的对象数量达到某个阈值,或者该区域需要被整理时,就会触发相应的垃圾收集。
在垃圾收集过程中,JVM会暂停所有应用线程(Stop-The-World),然后根据选定的垃圾收集算法(如标记-复制、标记-清除或标记-整理)来回收无用对象所占用的内存。释放的内存可以被其他新对象使用。
需要注意的是,即使一个对象被判定为不可达,它也不会立即被回收。在可达性分析之后,至少还需要经历两次标记过程,对象才会真正被宣告死亡。
11、有哪些常见的类加载器?
启动类加载器(Bootstrap Class Loader):也称为根类加载器,它负责加载Java虚拟机的核心类库,如java.lang.Object等。启动类加载器是虚拟机实现的一部分,通常是由本地代码实现的,不是Java类。
扩展类加载器(Extension Class Loader):它用来加载Java扩展类库的类加载器。扩展类库包括javax和java.util等包,它们位于jre/lib/ext目录下。
应用程序类加载器(Application Class Loader):也称为系统类加载器,它负责加载应用程序的类。它会搜索应用程序的类路径(包括用户定义的类路径和系统类路径),并加载类文件。
12、调优命令有哪些?
JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
jps,JVM Process Status Tool,显示指定系统内所有的Hotspot虚拟机进程。
jstat ,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap ,JVM Memory Map命令用于生成heap dump文件。
jhat ,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。
jstack,用于生成java虚拟机当前时刻的线程快照。.
jinfo,JVM Configuration info 这个命令作用是实时査看和调整虚拟机运行参数。
13、常用的JVM调优参数设置指令?
堆参数设置:
-XX:+PrintGC:在每次垃圾收集发生时打印相关信息。
-XX:+UseSerialGC:配置使用串行回收器。
-Xms:设置JVM启动时初始化堆的大小。
-Xmx:设置JVM能获得的最大堆大小。
-XX:+PrintGCDetails:打印详细的垃圾收集日志,包括各个区的情况。
-XX:+PrintCommandLineFlags:输出传给虚拟机的隐式或显式参数。
-Xmx200m -Xms50m
新生代参数配置:
-Xmn:设置新生代的大小。
-XX:SurvivorRatio=:设置新生代中Eden空间与Survivor空间的比例。
-XX:NewRatio=:设置新生代和老年代的比例。
-Xms200m -Xmx200m -Xmn10m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
堆溢出参数配置:
-XX:+HeapDumpOnOutOfMemoryError:在内存溢出时导出整个堆信息。
-XX:HeapDumpPath=
-Xms20m -Xmx200m -XX:+HeapDumpOnOutOfMemoryError //指定JVM的初始堆大小为20MB,JVM的最大堆大小为200MB,当JVM发生OutOfMemoryError错误时,自动生成堆转储文件(Heap Dump)。
栈参数配置:
-Xss:指定线程的最大栈空间。
-Xss10m
14、Stop The World是什么?
进行垃圾收集时,必须暂停其他所有工作线程,Sun将这种事情叫做"Stop The World"。
15、什么是空闲列表?
如果ava堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
16、一个对象头具体都包含哪些内容?
常用的Hotspot虚拟机中:
1.对象头
2.实例数据
3.对齐填充
17、描述Java对象在堆内存中的分配过程?
1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor Gc。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
4.动态判断对象的年龄。如果Surivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则G进行一次Full GC,如果小于检査HandlePromotionFailure设置,如果true则只进行Monitor Gc,如果false则进行Full GC。
18、指针碰撞是什么?
一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果]ava堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是指针碰撞。
19、如何触发垃圾回收?
1.自动触发:在大多数情况下,Java虚拟机(JVM)会自动触发垃圾回收。当JVM的堆内存接近满或者达到某个特定的阈值时,JVM的垃圾回收器会自动运行以释放不再使用的内存。
2.手动触发:虽然JVM通常会自动管理内存,但有时你可能希望手动触发垃圾回收。在Java中,你可以通过System.gc()或Runtime.getRuntime().gc()来建议JVM进行垃圾回收。但请注意,这只是向JVM发出一个建议,是否立即执行垃圾回收由JVM决定。
20、什么是GC引入的对象模型?
在Java的GC(垃圾回收)机制中,引入的对象模型主要涉及对象在内存中的表示和管理。在GC的上下文中,对象通常被视为“通过应用程序利用的数据的集合”。这些对象被配置在内存空间中,GC根据情况将配置好的对象进行移动或者销毁。
具体主要由以下部分组成:
对象头(Header):对象头保存了对象本身的信息,如对象的大小、种类等。此外,对象头还包含了运行GC所需要的信息,例如在采用标记-清除算法时,对象头会设置一个标志(flag)来记录对象是否已经被标记。
域(Field):域是对象使用者可访问的部分,类似于C语言中结构体的成员。对象使用者可以引用或替换对象的域值,但通常无法直接更改对象头的信息。域中的数据类型大致分为指针类型和非指针类型。
在Java中,对象的生命周期和内存管理是由JVM(Java虚拟机)自动管理的。当对象不再被引用时,JVM的垃圾回收器会将其占用的内存空间回收,以便用于其他对象的创建。这个过程就是垃圾回收(GC)。
21、谈谈你对Java内存模型(JMM)的理解?
Java内存模型(Java Memory Model,简称JMM)是Java虚拟机(JVM)规范中定义的一种内存访问模型,它规定了Java程序中各种变量(包括实例字段、静态字段和数组)的访问方式,以及线程之间如何通过主内存进行通信和如何访问主内存中的共享变量。JMM的主要目标是解决由于多线程并发访问共享数据导致的数据不一致问题。
JMM定义了主内存和工作内存的概念。主内存是所有线程共享的,它存储了Java堆中的对象实例以及所有类的字段(包括静态变量,但不包括局部变量与方法的返回值等),是线程之间共享变量的存储区域。而工作内存是每个线程私有的,它存储了线程对变量的所有操作(包括读/写共享变量)以及线程计算过程中的中间计算结果。
JMM的三大特性包括原子性、可见性和有序性:
原子性:原子性是指一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。Java内存模型只保证了基本读取或者赋值操作是原子性的,如果要实现更大范围操作的原子性,需要通过synchronized和Lock等同步手段来保证。
可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来保证多线程之间的可见性的。
有序性:有序性是指程序执行的顺序按照代码的先后顺序执行。但是在Java内存模型中,为了效率,编译器和处理器可能会对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java中,可以通过volatile关键字来保证多线程之间操作的有序性,也可以通过synchronized和Lock来保证指令不会被重排序。
此外,JMM还定义了8种原子操作来直接与主内存交互,包括read(读取主内存变量到工作内存)、load(工作内存变量就绪)、use(线程使用工作内存变量)、assign(线程给工作内存变量赋值)、store(将工作内存变量的值写入主内存)、write(将store的值覆盖主内存的值)等。这些操作为Java提供了一种在并发编程环境中的内存可见性保证,避免了因为多线程之间的数据不同步而导致的各种难以排查的问题。
22、自定义类加载器在哪些场景下会被使用?
1.实现类似进程内隔离:类加载器实际上可以用作不同的命名空间,以提供类似容器、模块化的效果。例如,当两个模块依赖于某个类库的不同版本时,如果它们分别被不同的类加载器加载,就可以互不干扰。Java EE、OSGi和JPMS等框架就使用了这种技术。
2.从非标准的来源加载代码:如果你的字节码是放在数据库、网络或其他非标准位置,你可以使用自定义类加载器从指定的来源加载类。
3.加密和解密:Java代码可以轻易地被反编译。如果你需要保护自己的代码,可以先将编译后的代码用某种加密算法加密,然后在加载类之前使用自定义类加载器解密。
4.热部署和动态加载:自定义类加载器允许你在运行时动态地加载新的类,从而实现热部署。这对于需要频繁更新代码的应用程序(如Web应用程序)非常有用。
5.扩展Java的类加载机制:在某些情况下,你可能需要扩展Java的默认类加载机制。例如,你可能需要自定义类的加载顺序,或者实现更复杂的类加载策略。
6.加载非Java代码:虽然Java虚拟机主要用于执行Java代码,但理论上它可以执行任何遵循Java字节码规范的代码。通过自定义类加载器,你可以加载和执行用其他语言(如Groovy、Kotlin等)编写的代码。