# Skevin **Repository Path**: kevin8079/skevin ## Basic Information - **Project Name**: Skevin - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-06-24 - **Last Updated**: 2025-06-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 常见试题 ### Jdk和Jre和JVM的区别 * Jdk【Java Development ToolKit】就是java开发工具箱,JDK是整个JAVA的核心里边包含了jre,它除了包含jre之外还包含了一些javac的工具类,把java源文件编译成class文件,java文件是用来运行这个程序的 * Jre【Java Runtime Enviromental】是java运行时环境,就是为了保证java程序能够运行时,所必备的一基础环境 包括JVM和JAVA核心类库和支持文件。与JDK相比,它不包含开发工具——编译器、调试器和其它工具。Jre里边包含jvm。 * Jvm:【Java Virtual Mechinal】因为jre是java运行时环境,java运行靠什么运行,而底层就是依赖于jvm,即java虚拟机,java虚拟机用来加载类文件,java中之所以有跨平台的作用,就是因为我们的jvm。 三者关系:J2se是基于jdk和jre,JDK是整个JAVA的核心里边包含了jre,Jre里边包含jvm。 ### JDK常用的包 java.lang:这个是系统的基础类,比如String、Math、Integer、System和Thread,提供常用功能。 [java.io](http://java.io/): 这里面是所有输入输出有关的类,比如文件操作等。 [java.net](http://java.net/): 这里面是与网络有关的类,比如URL,URLConnection等。 java.util : 这个是系统辅助类,特别是集合类Collection,List,Map等。 java.sql: 这个是数据库操作的类,Connection,Statememt,ResultSet等。 ### Java中的基本数据类型 | 数据类型 | 大小 | | --------------- | ------- | | byte(字节) | 1(8位) | | shot(短整型) | 2(16位) | | int(整型) | 4(32位) | | long(长整型) | 8(32位) | | float(浮点型) | 4(32位) | | double(双精度) | 8(64位) | | char(字符型) | 2(16位) | | boolean(布尔型) | 1位 | > String是基本数据类型吗?(String不是基本数据类型) > String的长度是多少,有限制?(长度受内存大小的影响) ### Java中switch接受的几种数据类型 short, int, byte,char enum(JDK1.5以后支持) String(JDK1.7以后支持) ### String、StringBuffer、StringBuilder区别 | 名字 | 字符串常量/变量 | 是否可变 | 线程安全 | | ------------- | --------------- | -------- | ------------------------------------------ | | String | 字符串常量 | 不可变 | 线程安全,使用字符串拼接时是不同的2个空间。 | | StringBuffer | 字符串变量 | 可变 | 线程安全,字符串拼接直接在字符串后追加。 | | StringBuilder | 字符串变量 | 可变 | 非线程安全,字符串拼接直接在字符串后追加。 | | 其它 | | | | 1. StringBuilder执行效率高于StringBuffer高于String。 2. String是一个常量,是不可变的,所以对于每一次+=赋值都会创建一个新的对象,StringBuffer和StringBuilder都是可变的,当进行字符串拼接时采用append方法,在原来的基础上进行追加,所以性能比String要高,又因为StringBuffer是线程安全的而StringBuilder是线程非安全的,所以StringBuilder的效率高于StringBuffer。 3. 对于大数据量的字符串的拼接,采用StringBuffer,StringBuilder ### Char型变量能不能存储一个汉字? char型变量是用来存储Unicode编码的字符的,unicode编码字符集中包含了汉字,补充说明:unicode编码占用两个字节,所以,char类型的变量也是占用两个字节。 ### ==和equals方法的区别 **==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址) **equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 - 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 ### 静态变量和实例变量的区别 在语法定义上的区别:静态变量前要加static关键字,而实例变量前则不加。 在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。 总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。 ### Integer和int的区别 int是java提供的8种原始数据类型之一,意思整型,占用4字节。 Integer是java为int提供的封装类,是引用数据类型。 int的默认值为0,而Integer的默认值为null,即Integer可以区分出未赋值和值为0的区别,int则无法表达出未赋值的情况。 ### 构造器是否可以被重写 构造器Constructor不能被继承,因此不能重写Override,但可以被重载Overload。 ### 新建对象有几种方式 1. 使用new关键字 2. 使用反射,调用newInstance 3. 使用clone方法 4. 使用序列化与反序列化 5. 动态代理(Proxy类和CGLIB) ### &和&&的区别 &运算符有两种用法:(1)按位与;(2)逻辑与。 &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。 ### 面向对象什么特性 对于面向对象的特性,一般有两种说法:一种是有三大特性,分别是封装,继承,多态,一种说法是有四大特性,封装,继承,多态,抽象。讲三大特性的时候更多一些,注意,这里的提问方式是面向对象的特性,而不是Java的特性。 ### 线程和进程的区别 1. 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元 2. 同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进行至少包括一个线程。 3. 进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束 4. 线程是轻两级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的 5. 线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源 ### 堆和栈的区别 Java的内存分为两类,一类是栈内存,一类是堆内存。 栈中存储的是当前线程的方法调用、基本数据类型和对象的引用,栈是有序的。 堆中存储的是对象的值,堆是无序的。 ### java 常见Exception 1、 运行时异常和非运行时异常 a) 运行时异常: 都是RuntimeException类及其子类异常: i. IndexOutOfBoundsException 索引越界异常 ii. ArithmeticException:数学计算异常 iii. NullPointerException:空指针异常 iv. ArrayOutOfBoundsException:数组索引越界异常 v. ClassNotFoundException:类文件未找到异常 vi. ClassCastException:造型异常(类型转换异常) 这些异常是不检查异常(Unchecked Exception),程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的。 非运行时异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如: IOException、文件读写异常 FileNotFoundException:文件未找到异常 EOFException:读写文件尾异常 MalformedURLException:URL格式错误异常 SocketException:Socket异常 SQLException:SQL数据库异常 ### 抽象类和接口的区别 1. java是单继承和多实现 2.抽象类由public abstract修饰,接口由public interface修饰。 2. 抽象类表示的是,这个对象是什么(is a)。接口表示的是,这个对象能做什么(has)。是否使用抽象类还是接口,主要看想要实现什么样的事情,如果侧重于描述事务,应该选择抽象类,如果侧重于定义功能,建议选择使用接口 3. 接口和抽象类都不能实例化,继承或实现的类必须实现抽象方法。 4. 接口中没有构造器 抽象类中有构造器, 5. 属性:接口没有普通属性,只有静态属性,并且只能用public final static 修饰 而抽象类可以有普通属性,可以有静态属性(类属性) 6. 方法:接口中的方法都没有方法体 并且都是默认使用public abstracrt 修饰,不能定义静态方法。而抽象类可以有普通方法 也可以有没有抽象方法 也可以定义静态方法 ### 抽象类和接口的对比 - 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。 - 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 **相同点** - 接口和抽象类都不能实例化 - 都位于继承的顶端,用于被其他实现或继承 - 都包含抽象方法,其子类都必须覆写这些抽象方法 **不同点** | 参数 | 抽象类 | 接口 | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 | | 实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 | | 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | | 访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected | | 多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 | | 字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 | **备注**:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 ``` 现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。 ``` - 接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则: - 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。 - 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。 ### 修饰符的作用 | 修饰符 | 同一个类中 | 同一个包中 | 子类中 | 全局 | | --------- | ---------- | ---------- | ------ | ---- | | private | Yes | | | | | Default | Yes | Yes | | | | protected | Yes | Yes | Yes | | | public | Yes | Yes | Yes | Yes | ### HashMap和Hashtable的区别 1. HashMap和Hashtable是Map接口下的两个实现类,因为Map对象是键值对的,所以此两类也是键值对的。 2. HashMap是线程非安全的,Hashtable是线程安全的,所以HashMap的效率高于Hashtable。 3. HashMap允许键或值为null,键最多只可以有一个为null,值不受限制。而Hashtable键或值都不许为null。 注意:Hashtable中的“t”是小写的。 ### 字节流和字符流的区别 以stream结尾都是字节流,以reader和writer结尾都是字符流,两者的区别就是读写的时候一个是按字节读写,一个是按字符。在实际使用时差不多。 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。只是读写文件,和文件内容无关的,一般选择字节流。 ### java 中 IO 流分为几种? - 按照流的流向分,可以分为输入流和输出流; - 按照操作单元划分,可以划分为字节流和字符流; - 按照流的角色划分为节点流和处理流。 > Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。 - InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 按操作对象分类结构图: ![在这里插入图片描述](assets/171744c479a04121tplv-t2oaga2asx-zoom-in-crop-mark1304000.webp) ### 运行异常和检查异常有何不同 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。 error和exception的区别 error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出,不可能指望程序能处理这样的情况。exception表示一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。 ### Java 中,throw 和 throws 有什么区别 * throw 用于手动抛出 java.lang.Throwable 类的一个实例化对象 * throws 的作用是作为方法声明和签名的一部分,方法被抛出相应的异常以便调用者能处理。(谁调用谁捕获) ### 举例最常用的五个运行时异常 * NullPointerException * ArrayIndexOutOfBoundsException * ClassCastException * 算术异常 ArithmeticException * IllegalArgumentException * NumberFormatException > * IllegelArgumentException > * SecurityException ### 启动一个线程用run还是start 启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。 ### List、Set、Map 是否继承自 Collection 接口? List、Set 是 ,Map 不是。Map 是键值对映射容器,与 List 和 Set 有明显的区别,而 Set 存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。 ### List、Map、Set 三个接口存取元素时,各有什么特点? List 以特定索引来存取元素,可以有重复元素。Set 不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map 保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。Set 和 Map 容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为 O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。 ### List,Set,Map的区别? - `List`是一个有序的集合,里面可以存储重复的元素。 - `Set`是一个不能存储相同元素的集合。 - `Map`是一个通过键值对的方式存储元素的,键不能重复。 - Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。 - Collection集合主要有List和Set两大接口 - List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 - Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 - Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 - Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap ### 集合框架底层数据结构 - Collection 1. List - Arraylist: Object数组 - Vector: Object数组 - LinkedList: 双向循环链表 2. Set - HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 - LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 - TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) - Map - HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 - LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 - HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 - TreeMap: 红黑树(自平衡的排序二叉树) ### 哪些集合类是线程安全的? - Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。 - hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。 - ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用) ### HashMap具体如何实现的? HashMap底层是基于数组+链表实现的,通过添加键的hashcode与上数组的长度来得到这个元素在数组中的位置,如果这个位置没有数据,那么就把这个数据当做第一个节点。如果这个位置有了链表,那么在JDK1.7的时候使用的是头插法,在JDK1.8的时候使用尾插法。 HashMap在JDK1.8的版本中引入了红黑树结构做优化,当链表元素个数大于等于8时,链表转换成树结构;链表元素个数小于等于6时,树结构还原成链表。 ### HashMap的扩容机制? HashMap底层是数组,在第一次put的时候会初始化,发生第一次扩容到16。它有一个负载因子是0.75,下一次扩容的时候就是当前数组大小*0.75。扩大容量为原来的2倍。 ### List和Map的区别 一个是存储单列数据的集合,另一个是存储键和值的双列数据的集合,List中存储的数据是有顺序,并且允许重复;Map中存储的数据是 没有顺序的,其键是不能重复的,它的值是可以有重复的。 1、 List有重复值,Map没有重复key,但可以有重复值 2、 List有序,Map不一定有序 3、 List只能存单列值,Map可以存双列值 ### List和Set比较,各自的子类比较 **对比一:****Arraylist与LinkedList的比较** 1、ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。 2、因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。 3、LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。 4、因为LinkedList要移动指针,所以查询操作性能比较低。 **适用场景分析:** 当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。 **对比二:****ArrayList与Vector的比较** 1、Vector的方法都是同步的,是线程安全的,而ArrayList的方法不是,由于线程的同步必然要影响性能。因此,ArrayList的性能比Vector好。 2、当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样。ArrayList就有利于节约内存空间。 3、大多数情况不使用Vector,因为性能不好,但是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性。 4、Vector可以设置增长因子,而ArrayList不可以。 **适用场景分析:** 1、Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。 2、如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。 **对比三:****HashSet与TreeSet的比较** 1.TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值 。 2.HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束 。 3.HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例。 **适用场景分析:** HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。 ### 说一下 HashSet 的实现原理? - HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。 ### HashSet如何检查重复?HashSet是如何保证数据不可重复的? - 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。 - HashSet 中的add ()方法会使用HashMap 的put()方法。 - HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。 - 以下是HashSet 部分源码: ```java private static final Object PRESENT = new Object(); private transient HashMap map; public HashSet() { map = new HashMap<>(); } public boolean add(E e) { // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; } ``` ### HashMap和ConcurrentHashMap的区别 1、HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。 2、ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。 3、ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。 ### HashSet与HashMap的区别 > | HashMap | HashSet | > | ------------------------------------------------------ | ------------------------------------------------------------ | > | 实现了Map接口 | 实现Set接口 | > | 存储键值对 | 仅存储对象 | > | 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 | > | HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false | > | HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 | ### JVM的内存结构 根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。 **1、Java虚拟机栈:** 线程私有;每个方法在执行的时候会创建一个栈帧,存储了局部变量表,操作数栈,动态连接,方法返回地址等;每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。 **2、堆:** 线程共享;被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例。 **3、方法区:** 线程共享;被所有线程共享的一块内存区域;用于存储已被虚拟机加载的类信息,常量,静态变量等。 **4、程序计数器:** 线程私有;是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。 **5、本地方法栈:** 线程私有;主要为虚拟机使用到的Native方法服务。 ### 强引用,软引用和弱引用的区别 **强引用:** 只有这个引用被释放之后,对象才会被释放掉,只要引用存在,垃圾回收器永远不会回收,这是最常见的New出来的对象。 **软引用:** 内存溢出之前通过代码回收的引用。软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。 **弱引用:** 第二次垃圾回收时回收的引用,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。 ### 如何对list中的一个数据进行排序 使用Collections.sort() ### list map 默认大小是多少 List 元素是有序的、可重复 ArrayList、Vector默认初始容量为10 #### Vector * 线程安全,但速度慢 * 底层数据结构是数组结构 * 加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容 扩容增量:原容量的 2倍 > 如 Vector的容量为10,一次扩容后是容量为20 #### ArrayList * 线程不安全,查询速度快 * 底层数据结构是数组结构 * 扩容增量:原容量的 0.5倍+原容量 > 如 ArrayList的容量为10,一次扩容后是容量为15 #### Set(集) * 无序的 * 不重复的 * 底层 HashMap * value值存储到Hashmap 的 key中 #### HashSet * 线程不安全,存取速度快 * 底层实现是一个HashMap(保存数据),实现Set接口 * 默认初始容量为16(因为HashMap的初始容量 为16) * 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容 * 扩容增量:原容量的 1 倍 > 如 HashSet的容量为16,一次扩容后是容量为32 #### Map * 是一个双列集合 * 底层是 数组+链表(Node)+红黑树 > **transient** Node[] table; * 初始容量是16 * 加载因子0.75 阈值= 容量*加载因子 扩容 元素达到 阈值时增加2倍 * 数组长度大于等于64,链表长度大于8时 ,转化为红黑树结构 ,链表元素个数小于等于6时,树结构还原成链表。 * HashMap:默认初始容量为16 > 如 HashSet的容量为16,一次扩容后是容量为32 ### Java序列化 * 序列化是把内存Java对象保存到存储介质中 * 反序列化就是把存储介质中的数据转化为Java对象。 * 需要进行序列化的对象的类必须实现Serializable接口 * Java通过ObjectInputStream和ObjectOutputStream实现序列化和反序列化 ### 什么时候用断言 assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。在实现中,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序必须保证这个boolean表达式的值为true;如果该值为false,说明程序已经处于不正确的状态下,assert将给出警告或退出。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion检查通常是关闭的。 ### Java反射机制 #### 什么是反射机制? - JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 - 静态编译和动态编译 - 静态编译:在编译时确定类型,绑定对象 - 动态编译:运行时确定类型,绑定对象 #### 反射机制优缺点 - **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 - **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。 #### 反射机制的应用场景有哪些? - 反射是框架设计的灵魂。 - 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 举例 - ①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序; - ②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性 #### Java获取反射的三种方法 1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制 ```java public class Student { private int id; String name; protected boolean sex; public float score; } public class Get { //获取反射机制三种方式 public static void main(String[] args) throws ClassNotFoundException { //方式一(通过建立对象) Student stu = new Student(); Class classobj1 = stu.getClass(); System.out.println(classobj1.getName()); //方式二(所在通过路径-相对路径) Class classobj2 = Class.forName("fanshe.Student"); System.out.println(classobj2.getName()); //方式三(通过类名) Class classobj3 = Student.class; System.out.println(classobj3.getName()); } } ``` ### 重载与重写的区别 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 * 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分 * 重写:发生在父子类中 1. 方法名、参数列表必须相同,返回值小于等于父类 2. 抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则) 3. 如果父类方法访问修饰符为private则子类中就不是重写。 1. override 重写: 1. 方法名、参数、返回值相同 2. 子类不能缩小 3. 子类不能抛出新的异常 4. 存在于父类和子类中间 5. 方法被定义为final的不能被重写。 2. overload重载: 1. 方法名相同,参数的类型、个数、顺序至少有一个不同 2. 返回类型可以相同也可以不同。不能重载只有返回值不同的方法名。 3. 存在于父类、子类、同类中 ### Collection 和 Collections的区别 * Collection是集合类的上级接口,继承与他的接口主要有Set 和List * Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。 ### final, finally, finalize的区别 * final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 * finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 * finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。 ### error和Exception有什么区别? * Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出OutOfMemoryError,不可能指望程序能处理这样的情况; * Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况 ### 什么是反射,反射的作用 * JAVA反射机制是在运行状态中,对于任意一个类,获取到类所在位置后,都能够知道这个类的所有属性和方法; * 对于任意一个对象,都能够调用它的任意一个方法和属性; * 这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 Java反射机制主要提供了以下功能: 1. 在运行时判断任意一个对象所属的类; 2. 在运行时调用任意一个对象的方法;生成动态代理。 3. 在运行时构造任意一个类的对象; 4. 在运行时判断任意一个类所具有的成员变量和方法; ### 线程的锁是如何实现的,在加锁时如何避免死锁 synchronized 关键字和 Lock 类 a. 避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。 b. 具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。 c. 使用定时锁。程序在调用 acquire() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。 ### 你知道的线程安全的集合有哪些 1. Vector:就比Arraylist多了个同步化机制(线程安全)。 2. Hashtable:就比Hashmap多了个线程安全。 3. ConcurrentHashMap:是一种高效但是线程安全的集合。 ### 用最有效率的方法计算 2 乘以 8 - 2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。 ### short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗 - 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。 - 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。 ### break ,continue ,return 的区别及作用 - break 跳出总上一层循环,不再执行循环(结束当前的循环体) - continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) - return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) ### 面向对象三大特性 - **抽象**:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。 - **封装**把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - **继承**是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - 关于继承如下 3 点请记住: - 子类拥有父类非 private 的属性和方法。 - 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 - 子类可以用自己的方式实现父类的方法。(以后介绍)。 - **多态**:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 ### hashCode 与 equals (重要) #### **hashCode()介绍** - hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。 - 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) #### 为什么要有 hashCode > 我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: - 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。 #### **hashCode()与equals()的相关规定** - 如果两个对象相等,则hashcode一定也是相同的 - 两个对象相等,对两个对象分别调用equals方法都返回true - 两个对象有相同的hashcode值,它们也不一定是相等的 > 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖 > hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) ### 为什么在重写 equals 方法的时候需要重写 hashCode 方法? * 重写equals方法 是为了 比较两个内容相同的对象 比较时相等 * 重写hashCode是因为在使用散列数据结构时,比如哈希表,我们希望相等的对象具有相等的哈希码。 > 在Java中,哈希表使用哈希码来确定存储对象的位置。如果两个相等的对象具有不同的哈希码,那么它们将被存储在哈希表的不同位置,导致无法正确查找这些对象。 ### BIO,NIO,AIO 有什么区别? - 简答 - BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 - NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。 - AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。 - 详细回答 - **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 - **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 - **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 **适用场景分析:** * BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解,如之前在Apache中使用。 * NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持,如在 Nginx,Netty中使用。 * AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持,在成长中,Netty曾经使用过,后来放弃。 ### Files的常用方法都有哪些? - Files. exists():检测文件路径是否存在。 - Files. createFile():创建文件。 - Files. createDirectory():创建文件夹。 - Files. delete():删除一个文件或目录。 - Files. copy():复制文件。 - Files. move():移动文件。 - Files. size():查看文件个数。 - Files. read():读取文件。 - Files. write():写入文件。 ### 数组有没有 length()方法?String 有没有 length()方法 - 数组没有 length()方法 ,有 length 的属性。 - String 有 length()方法 > JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。 ### 创建线程的四种方法 1. 继承 Thread类 2. 实现 Runable 接口 3. 实现 Callable 接口(通过泛型确定返回值的类型) 4. 使用线程池来创建 ~~~java ExecutorService service= Executors.newCachedThreadPool(); service.execute(new Runnable() { @Override public void run() { } }); ~~~ 1. Runable 和 Callable 的区别 | 实现接口 | 实现方法 | 返回值 | 可否抛出异常 | | ------------------------------------------------------------ | -------- | -------- | ------------ | | Runable 接口 | run() | 无 | 不能 | | Callable 接口 | call() | 依据泛型 | 可以 | | 1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread(); 2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行; 3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; 4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态 5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期 | | | | 对于多个在逻辑上互不依赖的耗时任务而言,并行化执行效率要比串行化执行效率高。对于这些任务往往在其执行完毕后要取得任务的执行结果。Callable和FutureTask就实现了这一机制。 本质上,创建线程的方式就是继承 Thread ,即使线程池,内部也是创建好的线程对象来执行。 ### 线程具有五中基本状态 ![20191022100239169](assets/20191022100239169.png) 1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread(); 2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行; 3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; 4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态 5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期 ### 线程的常用方法的 1. join() :t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。 2. yield() :t.yield()让当前线程从“运行状态”进入到“就绪状态”,和让其它线程获一起回到起跑线取 cpu 执行权, 3. wait 和 sleep区别 | name | 释放锁 | 唤醒条件 | 作用对象 | 方法属性 | 使用范围 | | ----- | ------ | --------------------------------------- | --------------- | ------------------------------------------------------------ | -------------------------------- | | wait | 是 | 其它线程调用对象的notify()或notifyAll() | Object 类的方法 | 实 例方法 | 同步方法或同步代码块,否则报异常 | | sleep | 否 | 超时或interrupt() | Thread 类的方法 | 静态方法 | 无限制 | ### 为什么 wait() 定义在 object 里,而不是 Thread类里 Java的锁是对象级别的锁,而不是线程级别,所以 wait 是 Object 的方法。sleep 是线程级别的,所以存在于 Thread 类中 ### 为什么 wait() 要放在同步代码块 防止在 cpu 切换线程的时候,其它线程先执行了 notify() ,后执行 wait(),线程永远无法被唤醒, 所以要放在同步代码块 ### 有几种线程池?并且详细描述一下线程池的实现过程 1. newFixedThreadPool创建一个指定大小的线程池。每当提交一个任务就创建一个线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到等待队列中。 2. newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是: * 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。 - 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 3. newSingleThreadExecutor创建一个单线程的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。 4. newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。 ### 线程池的主要参数和处理流程 主要参数有: 1. 线程池核心线程数大小 2. 最大线程数 3. 存储的队列 4. 拒绝策略 5. 空闲线程存活时长 当需要任务大于核心线程数时候,就开始把任务往存储任务的队列里,当存储队列满了的话,就开始增加线程池创建的线程数量,如果当线程数量也达到了最大,就开始执行拒绝策略,比如说记录日志,直接丢弃,或者丢弃最老的任务,或者交给提交任务的线程执行。 当一个线程完成时,它会从队列中取下一个任务来执行。当一个线程无事可做,且超过一定的时间(keepAliveTime)时,如果当前运行的线程数大于核心线程数,那么这个线程会停掉了。 ### 线程池的优点? 1)重用存在的线程,减少对象创建销毁的开销。 2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。 3)提供定时执行、定期执行、单线程、并发数控制等功能。 ### 为什么要用线程池 **那先要明白什么是线程池** 线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。 **使用线程池的好处** 1、线程池改进了一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。 2、线程池节省了CLR 为每个短生存周期任务创建一个完整的线程的开销并可以在任务完成后回收资源。 3、线程池根据当前在系统中运行的进程来优化线程时间片。 4、线程池允许我们开启多个任务而不用为每个线程设置属性。 5、线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。 6、线程池可以用来解决处理一个特定请求最大线程数量限制问题。 ### synchronized的作用? 在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。 synchronized既可以加在一段代码上,也可以加在方法上。 ### volatile关键字的作用 - 保证变量的可见性 volatile关键字的作用就是保证共享变量的**可见性**。什么是可见性呢,就是一个线程读变量,总是能读到它在内存中的最新的值,也就是说不同的线程看到的一个变量的值是相同的。CPU都是有行缓存的,volatile能让行缓存无效,因此能读到内存中最新的值。 - 保证赋值操作的原子性 原子性就是不能被线程调度打断的操作,是线程安全的操作,对于原子性操作,即使在多线程环境下,也不用担心线程安全问题或者数据不一致的问题。有些变量的赋值本身就是原子性的,比如对boolean,对int的赋值,但是像对于long或者double则不一定,如果是32位的处理器,对于64位的变量的操作可能会被分解成为二个步骤:高32位和低32位,由此可能会发生线程切换,从而导致线程不安全。如果变量声明为volatile,那么虚拟机会保证赋值是原子的,是不可被打断的。 - 禁止指令重排 正常情况下,虚拟机会对指令进行重排,当然是在不影响程序结果的正确性的前提下。volatile能够在一定程度上禁止虚拟机进行指令重排。还有就是对于volatile变量的写操作,保证是在读操作之前完成,假设线程A来读变量,刚好线程B正在写变量,那么虚拟机会保证写在读之前完成。 比如: ~~~java private volatile boolean flag; public void setFlag(boolean flag) { this.flag = flag; } public void getFlag() { return flag; } ~~~ 假设线程A来调用setFlag(true),线程B同时来调用getFlag,对于一般的变量,是无法保证B能读到A设置的值的,因为它们执行的顺序是未知的。但是像上面,加上volatile修饰以后,虚拟机会保证,线程A的写操作在线程B的读操作之前完成,换句话,B能读到最新的值。当然了,用锁机制也能达到同样的效果,比如在方法前面都加上synchronized关键字,但是性能会远不如使用volatile。 [参考](https://zhuanlan.zhihu.com/p/633426082) ### 什么是乐观锁和悲观锁 1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。 2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。 ### ThreadLocal是什么?有什么用? ThreadLocal叫做***线程变量***,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。 应用场景: - 每个线程需要有自己单独的实例 - 实例需要在多个方法中共享,但不希望被多线程共享 ### ThreadLocal与Synchronized的区别 ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。 但是ThreadLocal与synchronized有本质的区别: * Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。 * Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。 一句话理解ThreadLocal,threadlocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。 ### Lock锁 Lock 是 java.util.concurrent.locks 包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。Lock提供了比synchronized更多的功能。 * Lock和ReadWriteLock是两大锁的根接口 1. Lock代表实现类是ReentrantLock(可重入锁) 2. ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。 * Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。 * ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现 ReentrantReadWriteLock * Lock是可重入锁,可中断锁,可以实现公平锁和读写锁,写锁为排它锁,读锁为共享锁。ReentrantLock也是一种排他锁 ### synchronized 与 Lock 的区别 * synchronized是关键字,是JVM层面的,而Lock是一个接口,是JDK提供的API。 * 当一个线程获取了synchronized锁,其他线程便只能一直等待直至占有锁的线程释放锁。当发生以下情况之一线程才会释放锁:   a.占有锁的线程执行完了该代码,然后释放对锁的占有。   b.占有锁线程执行发生异常,此时JVM会让线程自动释放锁。   c.占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。 但是如果占有锁的线程由于要等待IO或者因为其他原因(比如调用sleep方法)而使线程阻塞了,但是又没有释放锁,那么线程就只能一直等待,那么这时我们可能需要一种可以不让线程无期限的等待下去的方法,比如只等待一定的时间(tryLock(long time, TimeUnit unit)或者能被人为中断lockInterrup0tibly(),这种情况我们需要Lock。 * 当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象,但是如果采用synchronized进行同步的话,就会导致当多个线程都只是进行读操作时也只有获取锁的线程才能进行读操作,其他线程只能等待锁释放后才能读,Lock则可以实现当多个线程都只是进行读操作时,线程之间不会发生冲突,例如:ReentrantReadWriteLock()。 * 可以通过Lock得知线程有没有成功获取到锁 (例如:ReentrantLock) ,但这个是synchronized无法办到的。 * 锁属性上的区别:synchronized是不可中断锁和非公平锁,ReentrantLock可以进行中断操作并别可以控制是否是公平锁。 * synchronized能锁住方法和代码块,而Lock只能锁住代码块。 * synchronized无法判断锁的状态,而Lock可以知道线程有没有拿到锁。 * 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时Lock的性能要远远优于synchronized。 [参考](https://blog.csdn.net/xyy1028/article/details/107333451) ### Lock锁方法 | 方法名称 | 方法描述 | | --------------------------------- | ------------------------------------------------------------ | | lock | 用来获取锁,如果锁已被其他线程获取,则进行等待 | | tryLock | 表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待 | | tryLock(long time, TimeUnit unit) | 和tryLock()类似,区别在于它在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true | | lockInterruptibly | 获取锁,如果获取锁失败则进行等到,如果等待的线程被中断会相应中断信息 | | unlock | 释放锁的操作 | | newCondition | 获取Condition对象,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件wait()方法,而调用后,当前线程释放锁 | ### 多线程同步有哪几种方法? Synchronized关键字,Lock锁实现,分布式锁等。 ### synchronized 和 lock 区别 | 类别 | synchronized | lock | | -------- | -------------------------- | ------------------------------------------ | | 存在层次 | Java关键字,jvm层面 | 是一个接口 | | 锁的释放 | JVM 将确保锁会获得自动释放 | lock 必须在 finally 块中释放 | | 锁状态 | 无法判断 | 可以判断 | | 锁类型 | 可重入,不可中断、非公平 | 可重入、可判断、可公平(两者皆可默认非公平) | | 性能 | 少量同步 | 大量同步 | ### 多线程的使用场景 1. 常见的浏览器,webfuwu,web处理请求,各种专用服务器(如游戏服务器) 2. servlet 多线程 3. FTP下载,多线程操作文件 4. 分布式计算 5. tomcat(内部采用多线程) 6. 异步任务:如发微博,记录日志 7. 自动作业处理:如定期备份日志,定期备份数据库 ### 简述一下你了解的设计模式。 所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。 在 GoF 的《Design Patterns: Elements of Reusable Object-OrientedSoftware》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共 23 种设计模式,包括 * Abstract Factory(抽象工厂模式) * Builder(建造者模式) * Factory Method(工厂方法模式) * Prototype(原始模型模式) * Singleton(单例模式) * Facade(门面模式) * Adapter(适配器模式) * Bridge(桥梁模式) * Composite(合成模式) * Decorator(装饰模式) * Flyweight(享元模式) * Proxy(代理模式) * Command(命令模式) * Interpreter(解释器模式) * Visitor(访问者模式) * Iterator(迭代子模式) * Mediator(调停者模式) * Memento(备忘录模式) * Observer(观察者模式) * State(状态 模式 ) * Strategy(策略 模式 ) * Template Method(模板方法模式) * Chain Of Responsibility(责任链模式)。 面试被问到关于设计模式的知识时,可以拣最常用的作答,例如: (1)工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。 (2)代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache 代理、防火墙代理、同步化代理、智能引用代理。 (3)适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。 (4)模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections 工具类和 I/O 系统中都使用装潢模式)等,反正基本原则就是拣自己最熟悉的、用得最多的作答,以免言多必失。 ### 用 Java 写一个单例类。 (1)饿汉式单例 ```java public class Singleton { private Singleton(){ } private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } } ``` (2)懒汉式单例 ```java public class Singleton { private static Singleton instance = null; private Singleton() { } public static synchronized Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; } } ``` 实现一个单例有两点注意事项: ①将构造器私有,不允许外界通过构造器创建对象; ②通过公开的静态方法向外界返回类的唯一实例 ### 说一下垃圾回收机制? Java 语言中一个显著的特点就是引入了垃圾回收机制,在编写程序的时候不再需要考虑内存管理。垃圾回收机制可以有效的防止内存泄露,提高内存的内存率。 垃圾回收器通常是作为一个单独的低级线程运行,不可预知的情况下对堆中已经死亡的或者长时间没有使用的对象进行清理和回收。 回收机制的算法有:标记清除算法、复制算法、标记压缩算法等等。 ### 描述一下垃圾回收的流程? 首先有三个代,新生代、老年代、永久代。 在新生代有三个区域:一个Eden区和两个Survivor区。当一个实例被创建了,首先会被存储Eden 区中。 具体过程是这样的: - 一个对象实例化时,先去看Eden区有没有足够的空间。 - 如果有,不进行垃圾回收,对象直接在Eden区存储。 - 如果Eden区内存已满,会进行一次minor gc。 - 然后再进行判断Eden区中的内存是否足够。 - 如果不足,则去看存活区的内存是否足够。 - 如果内存足够,把Eden区部分活跃对象保存在存活区,然后把对象保存在Eden区。 - 如果内存不足,查询老年代的内存是否足够。 - 如果老年代内存足够,将部分存活区的活跃对象存入老年代。然后把Eden区的活跃对象放入存活区,对象依旧保存在Eden区。 - 如果老年代内存不足,会进行一次full gc,之后老年代会再进行判断 内存是否足够,如果足够 还是那些步骤。 - 如果不足,会抛出OutOfMemoryError(内存溢出异常)。 ### 解释一下JVM的内存模型? Java内存模型决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,定义了线程和主内存之间的抽象关系。 线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(并不真实存在),本地内存中存储的是在主内存中共享变量的副本。 有两条规定: 1. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。 2. 线程的工作内存是私有的,其他线程无法访问,线程变量值的传递通过主内存来完成。 ### GC回收的是堆内存还是栈内存? 主要管理的是堆内存。 # 参考题库 ### 什么Java注释 **定义**:用于解释说明程序的文字 **分类** - 单行注释 格式: // 注释文字 - 多行注释 格式: /* 注释文字 */ - 文档注释 格式:/** 注释文字 */ ### this关键字的用法 - this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。 - this的用法在java中大体可以分为3种: - 1.普通的直接引用,this相当于是指向当前对象本身。 - 2.形参与成员名字重名,用this来区分: ```java public Person(String name, int age) { this.name = name; this.age = age; } ``` - 3.引用本类的构造函数 ```java class Person{ private String name; private int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } } ``` ### super关键字的用法 - super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 - super也有三种用法: - 1.普通的直接引用 与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。 - 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分 ```java class Person{ protected String name; public Person(String name) { this.name = name; } } class Student extends Person{ private String name; public Student(String name, String name1) { super(name); this.name = name1; } public void getInfo(){ System.out.println(this.name); //Child System.out.println(super.name); //Father } } public class Test { public static void main(String[] args) { Student s1 = new Student("Father","Child"); s1.getInfo(); } } ``` - 3.引用父类构造函数 - super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。 - this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 ### this与super的区别 - super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) - this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名) - super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。 - super()和this()均需放在构造方法内第一行。 - 尽管可以用this调用一个构造器,但却不能调用两个。 - this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。 - this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。 - 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。 ### Math.round(11.5) 等于多少?Math.round(-11.5)等于多少 - Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。 ~~~java float f=3.9f; System.out.println(Math.round(f));//返回最拼接f的整 四舍五入 4 System.out.println(Math.floor(f));//返回小于当前数的最大整数 3 System.out.println(Math.ceil(f));//返回大于当前数的最小整数 4 ~~~ ### float f=3.4;是否正确 - 不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。 ### static存在的主要意义 - static的主要意义是在于创建独立于具体对象的域变量或者方法。**以致于即使没有创建对象,也能使用属性和调用方法**! - static关键字还有一个比较关键的作用就是 **用来形成静态代码块以优化程序性能**。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 - 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。 ### static的独特之处 - 1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法**不属于任何一个实例对象,而是被类的实例对象所共享**。 > 怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩? - 2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。 - 3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的! - 4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。 ### static应用场景 - 因为static是被类的实例对象所共享,因此如果**某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量**。 - 因此比较常见的static应用场景有: > 1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包 ### static注意事项 - 1、静态只能访问静态。 - 2、非静态既可以访问非静态的,也可以访问静态的。 ### 什么是Java - Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。 ### 面向对象和面向过程的区别 - **面向过程**: - 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 - 缺点:没有面向对象易维护、易复用、易扩展 - **面向对象**: - 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 - 缺点:性能比面向过程低 > 面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。 > 面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。 > 面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。 ### 什么是多态机制?Java语言是如何实现多态的? - 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。 - 多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。 **多态的实现** - Java实现多态有三个必要条件:继承、重写、向上转型。 - 继承:在多态中必须存在有继承关系的子类和父类。 - 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 - 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。 > 只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。 > 对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。 ### 面向对象五大基本原则是什么 - 单一职责原则SRP(Single Responsibility Principle) 类的功能要单一,不能包罗万象,跟杂货铺似的。 - 开放封闭原则OCP(Open-Close Principle) 一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。 - 里式替换原则LSP(the Liskov Substitution Principle LSP) 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~ - 依赖倒置原则DIP(the Dependency Inversion Principle DIP) 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。 - 接口分离原则ISP(the Interface Segregation Principle ISP) 设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。 ### 普通类和抽象类有哪些区别? - 普通类不能包含抽象方法,抽象类可以包含抽象方法。 - 抽象类不能直接实例化,普通类可以直接实例化。 ### 抽象类能使用 final 修饰吗? - 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类 ### 什么是跨平台性?原理是什么 - 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 - 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 ### Java语言有哪些特点 - 简单易学(Java语言的语法与C语言和C++语言很接近) - 面向对象(封装,继承,多态) - 平台无关性(Java虚拟机实现平台无关性) - 支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的) - 支持多线程(多线程机制使应用程序在同一时间并行执行多项任) - 健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等) - 安全性好 ### 什么是字节码?采用字节码的最大好处是什么 - **字节码**:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。 - **采用字节码的好处**: Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。 - **先看下java中的编译器和解释器**: Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。 Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。 ### session 和 cookie 当用户访问到一个服务器,如果服务器启用session,服务器就会为该用户创建一个session, 在创建session的时候,服务器首先检查这个用户发送来的请求里是否包含sessionID, 如果有,说明该用户之前登陆过,并创建了session,服务器会按照sessionID把这个session在服务器的内存中找出来。 如果没有,则创建一个新的session,并生成相关的sessionID,这个SessionID是唯一、不重复、不容易找到规律的字符串。这个sessionID会作为cookie保存在发送给客户端进行保存。这样在交互过程中,浏览器可以自动按照这个规律把标识发送给浏览器。 区别:1.存放位置不同。 2.cookie不能存放中文,大小只有4K 3.session更加安全 4.对服务器造成的压力不同 5.cookie支持跨域访问 6.session的CookieJESSIONID的过期时间默许为-1,只要关闭浏览器,session就会失效。而cookie可以在浏览器上保存很长时间 ### switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上 - 在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。 ### 静态变量和实例变量区别 - 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。 - 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。 ### 静态方法和实例方法有何不同? - 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 - 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 ### 在一个静态方法内调用一个非静态成员为什么是非法的? - 由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 ### 成员变量与局部变量的区别有哪些 - 变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域 - 成员变量:方法外部,类内部定义的变量 - 局部变量:类的方法中的变量。 - 成员变量和局部变量的区别 **作用域** - 成员变量:针对整个类有效。 - 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内) **存储位置** - 成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。 - 局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。 **生命周期** - 成员变量:随着对象的创建而存在,随着对象的消失而消失 - 局部变量:当方法调用完,或者语句结束后,就自动释放。 **初始值** - 成员变量:有默认初始值。 - 局部变量:没有默认初始值,使用前必须赋值。 ### 在Java中定义一个不做事且没有参数的构造方法的作用 - Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 ### 构造方法有哪些特性? - 名字与类名相同; - 没有返回值,但不能用void声明构造函数; - 生成类的对象时自动执行,无需调用。 ### 什么是方法的返回值?返回值的作用是什么? - 方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! ### 什么是内部类? - 在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是**内部类**。内部类本身就是类的一个属性,与其他属性定义方式一致。 ### 内部类的分类有哪些 内部类可以分为四种:**成员内部类、局部内部类、匿名内部类和静态内部类**。 ### 内部类的优 - 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据! - 内部类不为同一包的其他类所见,具有很好的封装性; - 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。 - 匿名内部类可以很方便的定义回调。 ### 内部类有哪些应用场景 1. 一些多算法场合 2. 解决一些非面向对象的语句块。 3. 适当使用内部类,使得代码更加灵活和富有扩展性。 4. 当某个类除了它的外部类,不再被其他的类使用时。 ### 构造器(constructor)是否可被重写(override) - 构造器不能被继承,因此不能被重写,但可以被重载。 ### 是否可以继承 String 类 - String 类是 final 类,不可以被继承。 ### String str="i"与 String str=new String(“i”)一样吗? - 不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。 ### String 类的常用方法都有那些? - indexOf():返回指定字符的索引。 - charAt():返回指定索引处的字符。 - replace():字符串替换。 - trim():去除字符串两端空白。 - split():分割字符串,返回一个分割后的字符串数组。 - getBytes():返回字符串的 byte 类型数组。 - length():返回字符串长度。 - toLowerCase():将字符串转成小写字母。 - toUpperCase():将字符串转成大写字符。 - substring():截取字符串。 - equals():字符串比较。 ### 在使用 HashMap 的时候,用 String 做 key 有什么好处? - HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。 ### 阐述 ArrayList、Vector、LinkedList 的存储性能和特性。 ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector 中的方法由于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但性能上较ArrayList 差,因此已经是 Java 中的遗留容器。LinkedList 使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector 属于遗留容器(Java 早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,但是由于 ArrayList 和 LinkedListed 都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。 **补充:**遗留容器中的 Properties 类和 Stack 类在设计上有严重的问题,Properties是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个Hashtable 并将其两个泛型参数设置为 String 类型,但是 Java API 中的Properties 直接继承了 Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是 Has-A 关系而不是 Is-A 关系,另一方面容器都属于工具类,继承工具类本身就是一个错误的做法,使用工具类最好的方式是 Has-A 关系(关联)或Use-A 关系(依赖)。同理,Stack 类继承 Vector 也是不正确的。Sun 公司的工程师们也会犯这种低级错误,让人唏嘘不已。 ### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。Collections 工具类的 sort 方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。 ### Thread 类的 sleep()方法和对象的 wait()方法都可以让线程暂停执行,它们有什么区别? sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第 66 题中的线程状态转换图)。wait()是 Object 类的方法,调用对象的 wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 notify()方法(或 notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。 补充:可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是 CPU 调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的 CPU 资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费 CPU 时间。时下很时髦的 Node.js就采用了单线程异步 I/O 的工作模式。 ### 线程的 sleep()方法和 yield()方法有什么区别? (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; (2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态; (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性。 ### 什么是线程池(thread pool)? 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示: (1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 (2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 (3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。 (4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 (5)newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。 第 60 题的例子中演示了通过 Executors 工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池,强烈建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。 ### 什么是线程死锁?死锁如何产生?如何避免线程死锁? **死锁的介绍:** 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。 **死锁的产生的一些特定条件:** 1、互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 。 2、请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 3、不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。 4、循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。 **如何避免:** 1、加锁顺序: 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。 2、加锁时限: 加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。 3、死锁检测: 死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。 ### 阐述 JDBC 操作数据库的步骤。 下面的代码以连接本机的 Oracle 数据库为例,演示 JDBC 操作数据库的步骤。 (1) 加载驱动。 ```java Class.forName("com.mysql.cj.jdbc.Driver"); ``` (2) 创建连接。 ```java Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/crm", "root", "root"); ``` (3) 创建语句。 ```java PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?"); ps.setint(1, 1000); ps.setint(2, 3000); ``` (4)执行语句。 ```java ResultSet rs = ps.executeQuery() ``` (5)处理结果。 ```java while(rs.next()) { System.out.println(rs.getint("empno") + " - " + rs.getString("ename")); } ``` (6) 关闭资源。 ```java finally { if(con != null) { try { con.close(); } catch (SQLException e) { e.printStackTrace(); } } } ``` **提示:**关闭外部资源的顺序应该和打开的顺序相反,也就是说先关闭 ResultSet、再关闭 Statement、在关闭 Connection。上面的代码只关闭了 Connection(连接),虽然通常情况下在关闭连接时,连接上创建的语句和打开的游标也会关闭,但不能保证总是如此,因此应该按照刚才说的顺序分别关闭。此外,第一步加载驱动在 JDBC 4.0 中是可以省略的(自动从类路径中加载驱动),但是我们建议保留。 ### Statement 和 PreparedStatement 有什么区别?哪个性能更好? 与 Statement 相比, ①PreparedStatement 接口代表预编译的语句,它主要的优势在于可以减少 SQL 的编译错误并增加 SQL 的安全性(减少 SQL 注射攻击的可能性);②PreparedStatement 中的 SQL 语句是可以带参数的,避免了用字符串连接拼接 SQL 语句的麻烦和不安全; ③当批量处理 SQL 或频繁执行相同的查询时,PreparedStatement 有明显的性能上的优势,由于数据库可以将编译优化后的SQL 语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)。 补充:为了提供对存储过程的调用,JDBC API 中还提供了 CallableStatement 接口。存储过程(Stored Procedure)是数据库中一组为了完成特定功能的 SQL 语句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。虽然调用存储过程会在网络开销、安全性、性能上获得很多好处,但是存在如果底层数据库发生迁移时就会有 ### 使用 JDBC 操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能? 要提升读取数据的性能,可以指定通过结果集(ResultSet)对象的 setFetchSize()方法指定每次抓取的记录数(典型的空间换时间策略);要提升更新数据的性能可以使用 PreparedStatement 语句构建批处理,将若干 SQL 语句置于一个批处理中执行。 ### JDBC 中如何进行事务处理? Connection 提供了事务处理的方法,通过调用 setAutoCommit(false)可以设置手动提交事务;当事务完成后用 commit()显式提交事务;如果在事务处理过程中发生异常则通过 rollback()进行事务回滚。除此之外,从 JDBC 3.0 中还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。 ### JDBC 能否处理 Blob 和 Clob? Blob 是指二进制大对象(Binary Large Object),而 Clob 是指大字符对象(Character Large Objec),因此其中 Blob 是为存储大的二进制数据而设计的,而 Clob 是为存储大的文本数据而设计的。JDBC 的 PreparedStatement 和ResultSet 都提供了相应的方法来支持 Blob 和 Clob 操作。 ### 获得一个类的类对象有哪些方式? (1)方法 1:类型.class,例如:String.class (2)方法 2:对象.getClass(),例如:”hello”.getClass() (3)方法 3:Class.forName(),例如:Class.forName(“java.lang.String”) ### 如何通过反射创建对象? 方法 1:通过类对象调用 newInstance()方法,例如:String.class.newInstance() 方法 2:通过类对象的 getConstructor()或 getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其 newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance(“Hello”); ### 你能保证 GC 执行吗? 不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证 GC的执行。 ### Java 中怎么打印数组? 你可以使用 Arrays.toString() 和 Arrays.deepToString() 方法来打印数组。由于数组没有实现 toString() 方法,所以如果将数组传递给 System.out.println()方法,将无法打印出数组的内容,但是 Arrays.toString() 可以打印每个元素。 ### Java 中的 LinkedList 是单向链表还是双向链表? 是双向链表 ~~~java private static class Node { E item; Node next; Node prev; Node(Node prev, E element, Node next) { this.item = element; this.next = next; this.prev = prev; } } ~~~ ### Java 中,Comparator 与 Comparable 有什么不同? Comparable 接口用于定义对象的自然顺序,而 comparator 通常用于定义用户定制的顺序。Comparable 总是只有一个,但是可以有多个 comparator 来定义对象的顺序。 ### 除了单例模式,你在生产环境中还用过什么设计模式? 这需要根据你的经验来回答。一般情况下,你可以说依赖注入,工厂模式,装饰模式或者观察者模式,随意选择你使用过的一种即可。不过你要准备回答接下的基于你选择的模式的问题。 ### ConcurrentHashMap为什么是线程安全的? ConcurrentHashMap大部分的逻辑代码和HashMap是一样的,主要通过synchronized和来保证节点在插入扩容的时候是线程安全的。 ConcurrentHashMap的扩容核心逻辑主要是给不同的线程分配不同的数组下标,然后每个线程处理各自下表区间的节点。同时处理节点复用了hashMap的逻辑,通过位运行,可以知道节点扩容后的位置,要么在原位置,要么在原位置+oldlength位置,最后直接赋值即可。 ### ConcurrentHashMap的原理? ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成的。HashEntry封装的就是每一个键值对。,每一个Segment元素存储的是HashEntry数组 + 链表。Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment本身可以充当锁的角色。ConcurrentHashMap在put的时候需要进行两次hash,第一次需要确定在Segment数组的位置,第二次hash是确定在HashEntry数组中的位置。同样在get的时候也需要经过两次hash。 ### 迭代器 Iterator 是什么? - Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。 - 因为所有Collection接继承了Iterator迭代器 ### Iterator 怎么使用?有什么特点? - Iterator 使用代码如下: ```java List list = new ArrayList<>(); Iterator it = list. iterator(); while(it. hasNext()){ String obj = it. next(); System. out. println(obj); } ``` - Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。 ### 如何边遍历边移除 Collection 中的元素? - 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下: ```java Iterator it = list.iterator(); while(it.hasNext()){ *// do something* it.remove(); } ``` 一种最常见的**错误**代码如下: ```java for(Integer i : list){ list.remove(i) } ``` - 运行以上错误代码会报 **ConcurrentModificationException 异常**。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。 ### Iterator 和 ListIterator 有什么区别? - Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。 - Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。 - ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 ### 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? - 遍历方式有以下几种: 1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 - 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 - 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。 - 如果没有实现该接口,表示不支持 Random Access,如LinkedList。 - 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 ### 说一下 ArrayList 的优缺点 - ArrayList的优点如下: - ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。 - ArrayList 在顺序添加一个元素的时候非常方便。 - ArrayList 的缺点如下: - 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。 - 插入元素的时候,也需要做一次元素复制操作,缺点同上。 - ArrayList 比较适合顺序添加、随机访问的场景。 ### 如何实现数组和 List 之间的转换? - 数组转 List:使用 Arrays. asList(array) 进行转换。 - List 转数组:使用 List 自带的 toArray() 方法。 - 代码示例: ```java // list to array List list = new ArrayList(); list.add("123"); list.add("456"); list.toArray(); // array to list String[] array = new String[]{"123","456"}; Arrays.asList(array); ``` ### ArrayList 和 LinkedList 的区别是什么? - 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。 - 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 - 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。 - 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 - 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; - 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 - LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 ### ArrayList 和 Vector 的区别是什么? - 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 - 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。 - 性能:ArrayList 在性能方面要优于 Vector。 - 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 - Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 - Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 ### 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? - ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。 - Vector 中的方法由于加了 synchronized 修饰,因此 **Vector** **是线程安全容器,但性能上较ArrayList差**。 - LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 **LinkedList** **插入速度较快**。 #### 多线程场景下如何使用 ArrayList? - ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样: ```java List synchronizedList = Collections.synchronizedList(list); synchronizedList.add("aaa"); synchronizedList.add("bbb"); for (int i = 0; i < synchronizedList.size(); i++) { System.out.println(synchronizedList.get(i)); } ``` #### 为什么 ArrayList 的 elementData 加上 transient 修饰? - ArrayList 中的数组定义如下: private transient Object[] elementData; - 再看一下 ArrayList 的定义: ```java public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable ``` - 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: ```java private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ *// Write out element count, and any hidden stuff* int expectedModCount = modCount; s.defaultWriteObject(); *// Write out array length* s.writeInt(elementData.length); *// Write out all elements in the proper order.* for (int i=0; i ​ ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。 > ​ 为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。 > > ### 什么是Hash算法 - 简单来说就是把任意输入 通过特定方式(hash函数) 处理后 生成一个值。这个值等同于存放数据的地址,这个地址里面再吧输入的数据进行存储。 - 这个hash函数又叫散列函数,会有一些**常用的构造散列函数的方法,**但是处理结果值可能相同,那就叫冲突,冲突也有常用的冲突**常用的冲突解决方法。** - 散列算法(Hash Algorithm),又称哈希算法,杂凑算法,是一种从任意文件中创造小的数字「指纹」的方法。 ### 什么是链表 - 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。 - 链表大致分为单链表和双向链表 1. 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针 2. 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针 ![在这里插入图片描述](assets/17173551e73f80b0tplv-t2oaga2asx-zoom-in-crop-mark1304000.webp) - 链表的优点 - 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素) - 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间) - 大小没有固定,拓展很灵活。 - 链表的缺点 - 不能随机查找,必须从第一个开始遍历,查找效率低 ### 说一下HashMap的实现原理? - HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 - HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 - HashMap 基于 Hash 算法实现的 1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 2. 存储时,如果出现hash值相同的key,此时有两种情况。 ​ (1)如果key相同,则覆盖原始值; ​ (2)如果key不同(出现冲突),则将当前的key-value放入链表中 3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 - 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) ### HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 - 在Java中,保存数据有两种比较简单的数据结构:数组和链表。 - 数组的特点是:寻址容易,插入和删除困难; - 链表的特点是:寻址困难,但插入和删除容易; 所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。 #### HashMap JDK1.8之前 - JDK1.8之前采用的是拉链法。**拉链法**:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 ![在这里插入图片描述](assets/17173551e78f59a7tplv-t2oaga2asx-zoom-in-crop-mark1304000.webp) #### HashMap JDK1.8之后 - 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 ![在这里插入图片描述](assets/17173551e7c6af15tplv-t2oaga2asx-zoom-in-crop-mark) ### HashMap的put方法的具体流程? - 当我们put的时候,首先计算 `key`的`hash`值,这里调用了 `hash`方法,`hash`方法实际是让`key.hashCode()`与`key.hashCode()>>>16`进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:**高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞**。按照函数注释,因为bucket数组大小是2的幂,计算下标`index = (table.length - 1) & hash`,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 - putVal方法执行流程图 ![在这里插入图片描述](assets/1717355218a84ee7tplv-t2oaga2as) ```java public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //实现Map.put和相关方法 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; // 步骤①:tab为空则创建 // table未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步骤②:计算index,并对null做处理 // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素 else { Node e; K k; // 步骤③:节点key存在,直接覆盖value // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 将第一个元素赋值给e,用e来记录 e = p; // 步骤④:判断该链为红黑树 // hash值不相等,即key不相等;为红黑树结点 // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null else if (p instanceof TreeNode) // 放入树中 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); // 步骤⑤:该链为链表 // 为链表结点 else { // 在链表最末插入结点 for (int binCount = 0; ; ++binCount) { // 到达链表的尾部 //判断该链表尾部指针是不是空的 if ((e = p.next) == null) { // 在尾部插入新结点 p.next = newNode(hash, key, value, null); //判断链表的长度是否达到转化红黑树的临界值,临界值为8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //链表结构转树形结构 treeifyBin(tab, hash); // 跳出循环 break; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 步骤⑥:超过最大容量就扩容 // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null; } ``` 1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; 2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; 3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; 4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5; 5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; 6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 ### HashMap的扩容操作是怎么实现的? 1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容; 2. 每次扩展的时候,都是扩展2倍; 3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。 - 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上 ```java final Node[] resize() { Node[] oldTab = table;//oldTab指向hash桶数组 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空 if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值 threshold = Integer.MAX_VALUE; return oldTab;//返回 }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold } // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂 // 直接将该值赋给新的容量 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 新的threshold = 新的cap * 0.75 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 计算出新的数组长度后赋给当前成员变量table @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap];//新建hash桶数组 table = newTab;//将新数组的值复制给旧的hash桶数组 // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散 if (oldTab != null) { // 遍历新数组的所有桶下标 for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收 oldTab[j] = null; // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树 if (e.next == null) // 用同样的hash映射算法把该元素加入新的数组 newTab[e.hash & (newCap - 1)] = e; // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排 else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); // e是链表的头并且e.next!=null,那么处理链表中元素重排 else { // preserve order // loHead,loTail 代表扩容后不用变换下标,见注1 Node loHead = null, loTail = null; // hiHead,hiTail 代表扩容后变换下标,见注1 Node hiHead = null, hiTail = null; Node next; // 遍历链表 do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead // 代表下标保持不变的链表的头元素 loHead = e; else // loTail.next指向当前e loTail.next = e; // loTail指向当前的元素e // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时, // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next..... // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。 loTail = e; } else { if (hiTail == null) // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素 hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } ``` ### HashMap是怎么解决哈希冲突的? - 答:在解决这个问题之前,我们首先需要知道**什么是哈希冲突**,而在了解哈希冲突之前我们还要知道**什么是哈希**才行; #### 什么是哈希? - Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 #### 什么是哈希冲突? - **当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)**。 #### HashMap的数据结构 - 在Java中,保存数据有两种比较简单的数据结构:数组和链表。 - 数组的特点是:寻址容易,插入和删除困难; - 链表的特点是:寻址困难,但插入和删除容易; - 所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突 - 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; - 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 - **但相比于hashCode返回的int类型,我们HashMap初始的容量大小`DEFAULT_INITIAL_CAPACITY = 1 << 4`(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表**,所以我们还需要对hashCode作一定的优化 #### hash()函数 - 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于**参与运算的只有hashCode的低位**,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为**扰动**,在**JDK 1.8**中的hash()函数如下: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) } ``` - 这比在**JDK 1.7**中,更为简洁,**相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)**; ### Array 和 ArrayList 有何区别? - Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。 - Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。 - Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。 # 面试实战 ## Java对象内存布局 [参考]([__https://blog.csdn.net/weixin_45505313/article/details/126436824__](https://blog.csdn.net/weixin_45505313/article/details/126436824)) ![](assets/112v5105227cdc787ad18338468b5fa1f94e "") ![](assets/112va73778dc04fafe90060a5094f315502b "") ![](assets/112v47d0c189c0b2818f5beee80d056a00bf "") ## JDK1.8 JVM 内存结构(高薪) ![](assets/112t17f327b82ef54940bd8c42bebda91a89 "") ![](assets/112ve1efffb7cdae63f94230292c95dc808d "") ### 程序计数器 线程私有的(每个线程都有一个自己的程序计数器),是一个指针.代码运行,执行命令.而每个命令都是有行号的,会使用程序计数器来记录命令执行到多少行了.记录代码执行的位置. ### Java虚拟机栈 线程私有的(每个线程都有一个自己的Java虚拟机栈).一个方法运行,就会给这个方法创建一个栈帧,栈帧入栈执行代码,执行完毕之后出栈(弹栈)存引用变量,基本数据类型 ### 本地方法栈 线程私有的(每个线程都有一个自己的本地方法栈),和Java虚拟机栈类似,Java虚拟机栈加载的是普通方法, 本地方法加载的是native修饰的方法. native:在java中有用native修饰的,表示这个方法不是java原生的. ### 堆 线程共享的(所有的线程共享一份).存放对象的,new的对象都存储在这个区域 ### 元空间 存储.class信息,类的信息,方法的定义,静态变量等.而**常量池**放到元空间 JDK1.8和JDK1.7的jvm内存最大的区别是: 在1.8中方法区是由元空间(元数据区)来实现的。 1.8不存在方法区,将方法区的实现给去掉了.而是在本地内存中,新加入元数据区(元空间)。 > 堆和元空间是线程共享的,在Java虚拟机中只有一个堆、一个元空间,并在JVM启动的时候就创建,JVM停止才销毁。 > 栈、本地方法栈、程序计数器是每个线程私有的,随着线程的创建而创建,随着线程的结束而死亡。 ![](assets/112t03c58e2bdc7a88c68b69aceb9d4a6d22 "") ## Gc垃圾回收(高薪常问) JVM的垃圾回收动作可以大致分为两大步,首先是「如何发现垃圾」,然后是「如何回收垃圾」。说明一点,线程私有的不存在垃圾回收,只有线程共享的才会存在垃圾回收,所以堆中存在垃圾回收. ### 如何发现垃圾 Java语言规范并没有明确的说明JVM使用哪种垃圾回收算法,但是常见的用于「发现垃圾」的算法有两种,引用计数算法和根搜索算法。 ### 引用计数算法 该算法很古老(了解即可)。核心思想是,堆中的对象每被引用一次,则计数器加1,每减少一个引用就减1,当对象的引用计数器为0时可以被当作垃圾收集。 > 优点:快。 > 缺点:无法检测出循环引用。如两个对象互相引用时,他们的引用计数永远不可能为0。 ### 根搜索算法(也叫可达性分析) 根搜索算法是把所有的引用关系看作一张图,从一个节点GCROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即可以当作垃圾 Java中可作为GCRoot的对象有: 1.虚拟机栈中引用的对象 2.本地方法栈引用的对象 3.元空间中静态属性引用的对象 4.元空间中常量引用的对象 ### 如何回收垃圾 Java中用于「回收垃圾」的常见算法有4种: #### 1.标记-清除算法(markandsweep) 分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。 缺点:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片 #### 2.标记-整理算法 是在标记-清除算法基础上做了改进,标记阶段是相同的,但标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的 对象,这个过程叫做整理。 优点:内存被整理后不会产生大量不连续内存碎片 #### 3.复制算法(copying) 将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了, 就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。 缺点:可使用的内存只有原来一半。 #### 4.分代收集算法(generation) 当前主流JVM都采用分代收集(GenerationalCollection)算法,这种算法会根据对象 存活周期的不同将内存划分为年轻代、年老代,不同生命周期的对象可以采取不同 的回收算法,以便提高回收效率。 ![](assets/112k2fbbcb3b098388cf476a25d8590a6a11 "") ##### 1)年轻代(YoungGeneration) 1.所有新生成的对象首先都是放在年轻代的。 2.新生代内存按照8:1:1的比例分为一个eden区和两个Survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空,如此往复. 3.当一个大对象不足于存放到eden区时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次FullGC,也就是新生代、老年代都进行回收。 4.新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高 ##### 2)年老代(OldGeneration) 1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。 2.内存比新生代也大很多(大概是2倍),当老年代内存满时触发MajorGC即FullGC, FullGC发生频率比较低,老年代对象存活时间比较长,存活率比较高。 ##### 3)持久代(PermanentGeneration) 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,从JDK8 以后已经废弃,将存放静态文件,如Java类、方法等这些存储到了元数据区. ## JVM调优参数(了解) 这里只给出一些常见的性能调优的参数及其代表的含义。 ```text -Xms2g -Xmx4g -Xmn4g -Xss256m -XX:NewRatio=2 -XX:SurvivorRatio=4 -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=0 ``` **-Xms2g: 设置JVM最小内存为2g。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。** **-Xmx8g:设置JVM最大可用内存为8g。** **-Xmn4g:设置年轻代大小为4G。** -XX:NewRatio=2 设置年轻代(包括Eden和两个Survivor区)与年老代的比值。设置为2,则年轻代与年老代所占比值为1:2,年轻代占整个堆栈的1/3。 -XX:SurvivorRatio=8 ,所以默认值 Eden:S0:S1=8:1:1。 -Xss256m:设置每个线程的栈大小 **-XX:MaxMetaSpaceSize=256m: 设置元空间大小为256m** -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。 对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率。 ## java常见的引用类型 java的引用类型一般分为四种:**强引用**、**软引用**、**弱引用**、**虚引用** **强引用**:普通的变量引用 ```java hljs User user = new User(); ``` **软引用**:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。**软引用可用来实现内存敏感的高速缓存。** 1. 当所剩内存空间不够我们新的对象存储的时候,直接干掉软引用。 1. 当所剩内存空间够我们新对象的存储的时候,不会删除我们的软引用对象。 通俗讲: 下雨天,如果有人来进我们店铺避雨,不影响我们正常营业的话,我们是允许他们进来避雨的.如果人太多,影响了我们营业,这时候我们就是让蹭空调人离开. ```java SoftReference user = new SoftReference(new User()); ``` **弱引用**:将对象用WeakReference弱引用类型的对象包裹,弱引用跟没引用差不多,**GC会直接回收掉,只要GC执行了,他就会被回收掉.** 小心眼: 欺负人 ![](assets/112kf1cdf949e5ca95d2171165d71188a694 "") ```java hljs public static WeakReference user = new WeakReference(new User()); ``` **虚引用:**虚引用也**称为幽灵引用或者幻影引用,**它是最弱的一种引用关系,几乎不用。