自己在面试之前总结了一些Java知识点,现在已经找到工作了,所以把这些知识点放到网上分享,希望不吝指教,如果能帮到你那最好不过。我会不定时补充更新该文章。
韩老师视频课:https://www.bilibili.com/video/BV1fh411y7R8?spm_id_from=333.999.0.0
Java基础
一、集合
集合框架图:
1.1 Collection 接口
public interface Colletcion<E> extends Iterable<E>
特点:
- Collection 接口没有直接的实现子类,是通过它的子接口 set 和 list 来实现的。
- Collection 实现子类可以存放多个元素,每个元素可以是 对象(Object)。
接口的常用方法:
add: 添加单个元素
remove:删除指定元素
contains:查找元素是否存在
size:获取元素个数
isEmpty:判断是否为空
clear:清空
addAll:添加多个元素
containsAll:查找多个元素是否存在
removeAll:删除多个元素
1.2 List 接口
特点:
- 元素有序、可重复
- 每个元素都有对应的顺序索引。
- 每个元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
基本方法:
void add(int index, Object ele): 在index位置插入ele元素
boolean addAll(int index, Collection eles): 从index位置开始将 eles中所有的元素添加进来。
Object get(int index): 获取指定index位置的元素
int indexOf(Object obj): 返回obj在集合中首次出现的位置
int lastIndexOf(Object obj): 返回obj在集合中最后出现的位置
Object remove(int index): 移除指定index位置的元素,并返回该元素
Object set(int index, Object ele): 替换在index位置的元素为ele
List subList(int fromIndex, int toIndex): 返回从fromIndex到toIndex位置的子集合。
(*)实现的类有:
1.2.1 ArrayList
- ArrayList是由数组实现数据存储的
- 基本等同于Vector,但ArrayList是线程不安全的,多线程情况下不建议使用ArrayList
底层源码解析:
-
ArrayList中维护了一个Object类型的数组elementData
transient Object[] elementData;
-
当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加,则扩容elementData为10,如需再次扩容,则扩容elementData的1.5倍
-
如果使用的是指定大小的构造器,则初始elementData容量为指定大小。
执行add时,先确定是否要扩容:
1.2.2 Vector
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
-
Vector 底层是一个数组对象:
protected Object[] elementData;
-
Vector 是线程同步的,即线程安全,Vector类的操作方法带有 synchronized
和ArrayList的比较:
1.2.3 LinkedList
- 底层是一个双向链表
和ArrayList的比较:
如何选择LinkedList 和 ArrayList:
- 如果改查的比较多,选择 ArrayList
- 如果增删的比较多,选择 LinkedList
1.3 Set 接口
Set<E>
特点:
- 无序,不重复
- 只能存入一个null值
1.3.1 HashSet
-
HashSet实现了Set接口
-
实际上是一个HashMap
public HashSet() { map = new HashMap(); }
-
底层为 数组 + 单向链表 + 红黑树
-
默认初始大小为 16 ,临界值为 初始大小 * 装填因子(0.75),当实际大小到达临界值时,就会 调用 resize()方法 扩容 2倍。
-
当HashMap 的数组容量大于等于64,且某个链表的元素个数大于8时,就会将链表转换成 红黑树。 如果只是某个链表的元素个数大于8,则不会转换,而是将HashMap扩容2倍。
源码解析:
HashSet添加元素:
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// hash()方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// putValue()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put()流程: 调用 putValue(hash(key), key, onlyIfAbsent=false, evict=true) 方法。
首先计算 key 的hash值,然后带入 putValue() 方法, 进入方法后,先判断HashMap中的 table 是否为空, 为空的话就 初始化,接着 计算 **(table长度-1) & hash(key),**得到对象在 table中的位置。若该位置为空,则直接插入该值。否则就继续判断 该位置的对象 与 插入的对象 hash是否相等 并且 判断地址,或者 调用equals() 方法 计算 key值是否相等, 相等则插入并先覆盖value,否则, 判断该链表是否转为树,若是则调用 putTreeVal(this,tab,hash,key,value). 否则进行下一步,遍历链表上的节点, 如果存在对象相等的情况,则插入对象,否则将对象插入链表最后。
跳出循环,之后 判断新插入的对象是否覆盖原有对象, 若是, 判断 onlyIfAbsent的值 即:是否能够覆盖原有表中的值。
最后会 判断是否满足扩容要求。
put() 方法 和 **putIfAbsent() 方法 **的区别就是,如果插入的对象的key在表中已存在,则 是否覆盖原有对象的值value。
1.3.2 LinkedHashSet
-
LinkedHashSet是 HashSet 子类
-
LinkedHashSet 底层是一个 LinkedHashMap,底层数据结构是 数组 + 双向链表
-
LinkedHashSet 根据元素的 hashCode 值来决定 元素的存储位置,同时使用链表来维护元素的次序,这使得元素看起来是以插入顺序保存的。
-
LinkedHashSet 不允许添加重复元素。
结构如下所示:
1.3.3 TreeSet
1.4 Map 接口
特性:
- Map 集合 是 无序、互异的,它存储的数据类型是 键值对 key-value 。
1.4.1 HashMap
特性:
-
key 可以为null,但只能有一个,value 可以有多个null值。
-
hashMap 是非同步,线程不安全的。
-
Map存放数据是键值对形式,一对key-value 是放在一个 Node 中的
1.4.2 HashTable
特性:
-
键和值都不能为 null
-
使用方法上和 hashMap一致
-
hashTable 是线程安全的,hashMap是线程不安全的。
-
底层是数组+链表, 数组的初始大小为 11 装填因子为0.75 ,扩容机制如下:
protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; // overflow-conscious code int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } }
每次扩容为 2n + 1;
1.4.3 ConcurrentHashMap
-
底层数据结构:JDK1.8 之前 底层采用 分段数组 + 链表,JDK1.8 及之后采用 数组 + 链表 + 红黑树
-
实现线程安全的方式:
-
JDK1.7 ConcurrentHashMap 对整个桶数组进行分割分段(Segment),每一把锁只锁容器中的一部分数据。多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高访问效率。
-
JDK1.8 ConcurrentHashMap 摒弃 Segment 概念,直接采用 数组 + 链表 + 红黑树,并发控制使用 synchronized + CAS算法
CAS: Compare and Swap ,是一种无锁算法
CAS算法 比较 - 设置
例如现在有三个数 : 内存值V , 旧的预期值A , 要修改成B
那么当且仅当 A = V 时,才会修改内存V的值为B并返回true,否则什么都不做返回false,CAS一定要volatile 关键字的支持,利用volatile关键字的可见性 , 保证每次从主内存中拿到的都是最新的那个值,否则旧的预期值A对于某条线程来说永远都是一个不会变的值
-
二、反射
2.1 基本概念
- 反射:通过外部文件配置,在不修改源码情况下,来控制程序,符合设计模式的ocp(开闭原则)
- 反射机制允许程序在运行期借助于Reflection API 取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到。
- 加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。
注:
- Class 是类,继承Object类
- Class类对象不能new,而是由系统创建
- 通过Class对象可以完整地得到一个类的完整结构
- Class类对象存放在堆中。
- 类的字节码二进制数据,存放在方法区中。
1. Class a = Class.forName(""); // 加载类
2. Object o = a.newInstance(); // 获取实例
3. java.lang.reflect.Method //代表类的方法
4. java.lang.reflect.Field // 代表类的成员变量
5. java.lang.reflect.Constructor // 代表类的构造方法
反射原理图:
2.2 反射的使用
-
获取Class类对象的方法
-
已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法 forName()获取, 可能抛出 ClassNotFoundException
应用: 多用于配置文件,读取类全路径,加载类
-
若已知具体的类,通过类的class 获取,该方法最为安全可靠,性能最高
- Class cls = 类.class;
- 应用: 多用于参数传递,比如通过反射得到对应构造器对象。
-
**已知某个类的实例,**调用该实例的getClass() 方法获取Class对象:
- Class cls = 对象.getClass();
- 应用: 通过创建好的对象,获取Class对象
-
通过类加载器(4种):
-
ClassLoader cls = 对象.getClass().getClassLoader();
Class cls = cls.loadClass(“类的全类名”);
-
基本数据类型的获取
-
基本数据类型对应的包装类,通过 .TYPE 得到。
-
-
-
通过反射创建对象的方式
-
反射调用优化 – 关闭访问检查
- Method, Field , Constructor 对象都有setAccessible(Boolean boolean)方法
- setAccessible(Boolean boolean) : 启用和禁用访问安全检查的开关
- 参数值 true 表示 反射的对象在使用时取消访问检查,提高反射效率。false 表示 反射的对象执行访问检查。
2.3 反射机制的优缺点
- 优点:
- 能够运行时动态获取类的实例,提高灵活性
- 缺点:
- 其无视泛型安全检查机制,造成了安全问题。
- 性能稍微差一些。
2.4 如何提高反射效率
1、缓存Class对象,避免每次反射操作重复获取
2、设置setAccessible(true) 关闭安全访问检查
3、使用第三方库:ReflectASM
三、IO流
3.1 IO流基本介绍
- 文件在程序中以流的形式进行操作
-
创建文件的构造器
1. new File(String pathname); 2. new File(File parent, String child); // 根据父目录文件+子路径构建 3. new File(String parent, String child); // 根据父目录+子路径构建 4. createNewFile(); // 创建新文件
-
获取文件的相关信息
1.getName(); // 文件名 2.getAbsolutePath(); // 文件的绝对路径 3.getParent(); // 文件的父目录 4.length(); // 文件的大小(字节) 5.exists(); // 文件是否存在 6.isFile(); // 是否是文件 7.isDirectory(); //是否是目录
-
目录操作
1.mkdir(); // 创建一级目录 2.mkdirs(); // 创建多级目录 3.delete(); // 删除文件或空目录
-
流的分类
- 按操作数据单位不同分为: 字节流(8bit=1byte)、字符流
- 按数据流的流向不同分为:输入流、输出流
- 按流的角色不同分为:节点流,处理流/包装流
-
InputStream
- FileInputStream 文件输入流
-
ObjectInputStream 缓冲字节输入流
-
BufferedInputStream 对象字节输入流
-
FileReader
FileReader相关方法:
1. new FileReader(File/String); 2. read(); // 每次读取单个字符,返回该字符 (char类型),如果到文件末尾返回-1 3. read(char[]); // 批量读取多个字符到数组 4. new String(char[]); // 将char[]转换成String
-
节点流和处理流
- 节点流:可以从一个特定的数据源读写数据: FileReader、FileWriter
- 处理流:又称包装流,为程序提供更强大的读写功能。
- 处理流的主要功能:以增加缓冲的方法来增加输入输出的效率。
- 添加的一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便。
在使用包装流时,关闭流的时候只需关闭包装流,无需关闭引入的节点流。因为在关闭包装流的时候,默认关闭的是引入的节点流。
1. BufferedInputStream 和 BufferedOutputStream
1. BufferedInputStream
2. BufferedOutputStream 创建一个新的缓冲输出流,以将数据写入指定的底层输出流
-
标准输入输出流
- System.in 标准输入 InputStream
- System.out 标准输出 PrintStream
-
转换流 (将字节流转换成字符流来避免乱码)
- InputStreamReader: 将InputStream包装成 Reader
- OutputStreamWriter: 同上
- 在处理纯文本数据时,如果使用字符流效率更高,并且可以有效解决中文问题。
- 在使用时,可以指定编码格式。
3.2 序列化和反序列化
序列化: 将数据结构或对象转换成二进制字节流的过程。
反序列化:将序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
应用场景:
1、对象在进行网络传输之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化
2、对象存储到文件\数据库\内存 之前需要序列化, 取出来时需要反序列化
序列化的主要目的是通过网络传输对象或者将对象存储到文件系统、数据库、内存中
四、JVM
4.1 运行时数据区
Java虚拟机会在执行Java执行的过程中把它划分成若干个不同的数据区域。
JDK1.8之后:
4.1.1 程序计数器
程序计数器是可以看作当前线程的指令指示器。字节码解释器工作时通过改变计数器的值来选取下一条需要执行的字节码指令。
每个线程都有一个独立的程序计数器,各线程的计数器之间互不影响。 程序计数器是唯一不会出现 OOM 异常的内存区域。
4.1.2 Java虚拟机栈
Java虚拟机栈由一个个栈帧组成,每个栈帧中包含:局部变量表、操作数栈、动态链接、方法出口信息。
局部变量表中主要包括:
- 各种数据类型
- 对象引用类型
4.1.3 堆
Java 堆 是一块线程共享的内存区域,在虚拟机启动时创建。该内存区域存放 数组 和 对象实例。
Java堆是垃圾收集器管理的主要区域,因此也被称为 GC(Garbage Collected) 堆。
4.1.4 方法区
方法区与Java堆一样,是线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量等数据。
4.1.5 运行时常量池
运行时常量池是方法区的一部分。
4.1.6 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
4.2 对象的创建过程
-
类加载检查
虚拟机在遇到一条 new 指令时,首先去检查这个指令的参数是否能在常量池中定位到这个类的引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。若没有,则执行相应的类加载过程。
-
分配内存
在Java堆中为对象分配一块内存,分配的方式由两种:
- 指针碰撞
- 空闲列表
-
初始化零值
虚拟机会将分配到的内存空间初始化,保证实例对象的成员变量无需附初始值就可以直接使用。
-
设置对象头
对象头中包含以下信息:
- 该对象属于哪个类
- 对象的哈希码
- 对象的GC分代年龄
- 等等。
-
执行init方法
4.2.1 对象的内存布局
对象在内存中的布局可以分为3块区域: 对象头、实例数据和对齐填充。
Hotspot 虚拟机的对象头包括两部分信息,
第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
4.2.2 对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有① 使用句柄和② 直接指针两种:
-
句柄: ** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址**信息;
-
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
- 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改
- 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
4.3 垃圾回收机制
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)
堆空间的基本结构:
大部分情况下,对象都会首先在 Eden区域分配,再一次新生代垃圾回收后,如果对象还存活,则会进入 S0或者 s1,并且对象的年龄+1,当它的年龄增加到一定程度(默认为大于15),就会晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 XX:MaxTenuringThreshold 来设置,该值会在虚拟机运行过程中进行调整:
Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值
4.3.1 对象优先进入 eden区分配
主流的垃圾收集器采用分代回收算法。
4.3.2 大对象直接进入老年代
大对象是需要大量连续内存空间的对象(字符串、数组)
4.3.3 长期存活的对象将进入老年代
4.3.4 动态对象年龄判定
Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
//sizes数组是每个年龄段对象大小
total += sizes[age];
if (total > desired_survivor_size) {
break;
}
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}
4.3.5 主要进行GC的区域
4.3.6 空间分配担保
JDK 6 Update 24 之前,在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看 -XX:HandlePromotionFailure
参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 -XX: HandlePromotionFailure
设置不允许冒险,那这时就要改为进行一次 Full GC。
JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC
4.4 判断对象可以回收?
4.4.1 引用计数器法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
4.4.2 可达性分析算法
通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
哪些对象作为 GC Roots 呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
对象可以被回收,就一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize
方法。当对象没有覆盖 finalize
方法,或 finalize
方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
注:
Object
类中的 finalize
方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize
方法会被逐渐弃用移除。忘掉它的存在吧!
4.5 引用
4.5.1 强引用
如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
4.5.2 软引用
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
4.5.3 弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
4.5.4 虚引用
虚引用主要用来跟踪对象被垃圾回收的活动。
4.6 如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收。
4.7 垃圾收集算法
4.7.1 标记-清除算法
该算法分为**“标记”和“清除”阶段:首先标记出所有不需要回收的对象**,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
4.7.2 标记-复制算法
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
4.7.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是**让所有存活的对象向一端移动,**然后直接清理掉端边界以外的内存。
4.7.4 分代收集算法
根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
4.8 类文件结构详解
.class 文件叫做字节码文件,不面向任何处理器,只面向虚拟机。是支持Java跨平台的一个重要原因。
根据Java虚拟机规范,Class文件通过 ClassFile 定义:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
tips: 通过 IDEA插件 jclasslib 查看 Class文件结构,类的基本信息、常量池、接口、属性、函数等信息。
4.9 类加载过程
类的完整声明周期如下:
4.9.1 类加载过程
类加载过程主要分为三步: 加载 --> 连接 --> 初始化。
- 加载
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass()
方法)。
数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
- 验证
-
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
-
这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被
static
关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 -
这里所设置的初始值**“通常情况”**下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了
public static int value=111
,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。**特殊情况:**比如给 value 变量加上了 final 关键字public static final int value=111
,那么准备阶段 value 的值就被赋值为 111。
-
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。也就是得到类或者字段、方法在内存中的指针或者偏移量。
-
初始化
初始化阶段是执行初始化方法
<clinit> ()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。对于
<clinit> ()
方法的调用,虚拟机会确保其在多线程环境中的安全性。因为 () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。 -
卸载
通过GC回收该类的Class对象
详看 4.6章节
4.9.2 类加载器
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
4.9.3 双亲委派模型
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。当父类加载器无法处理时,才由自己来处理。当**父类加载器为 null 时,**会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
实现源码:
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派的好处:
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
若是不使用双亲委派模型:
自定义加载器的话,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法
4.10 Java虚拟机参数总结
4.10.1 显式指定堆内存 -Xms 和 -Xmx
如果我们需要指定最小和最大堆大小(推荐显示指定大小),以下参数可以帮助你实现:
-Xms<heap size>[unit]
-Xmx<heap size>[unit]
-Xms 2g
-Xmx 5g
- heap size 表示要初始化内存的具体大小。
- unit 表示要初始化内存的单位。单位为***“ g”*** (GB) 、“ m”(MB)、“ k”(KB)
4.10.2 显式新生代内存(Young Generation)
默认情况下,YG 的最小大小为 1310 MB,最大大小为无限制。
-XX:NewSize=<young size>[unit]
-XX:MaxNewSize=<young size>[unit]
-Xmn<young size>[unit] // 将 NewSize 和 MaxNewSize 设为一致
tips:
将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,**因此尽可能将对象分配在新生代是明智的做法,**实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
4.10.3 显式指定 永久代/原空间的大小
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是本地内存。
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
4.10.4 垃圾回收器
为了提高应用程序的稳定性,选择正确的垃圾收集算法至关重要。
JVM具有四种类型的GC实现:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS垃圾收集器
- G1垃圾收集器
可以使用以下参数声明这些实现:
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseParNewGC
-XX:+UseG1GC
4.10.5 GC记录
为了严格监控应用程序的运行状况,我们应该始终检查JVM的垃圾回收性能。最简单的方法是以人类可读的格式记录GC活动。
使用以下参数,我们可以记录GC活动:
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=< number of log files >
-XX:GCLogFileSize=< file size >[ unit ]
-Xloggc:/path/to/gc.log
五、并发
5.1 synchronized
5.1.1 synchronized 介绍
synchronized 关键字解决的是 多个线程之间访问资源的同步性, synchronized 可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
原理:
synchronized 底层原理属于 JVM 层面。
-
synchronized 同步语句块:
-
在同步语句块时,使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指向 同步代码块开始的位置, monitorexit 指向同步代码块结束位置。
-
在执行 monitorenter 时 会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。拥有对象锁的线程才可以执行 monitorexit 指令,执行之后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
-
如果对象锁获取失败,那当前线程就阻塞等待,直到锁被另外一个线程释放为止。
-
-
synchronized 同步方法 :
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 **ACC_SYNCHRONIZED ** 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
-
如果是实例方法,JVM 会尝试获取实例对象的锁。
-
如果是静态方法,JVM 会尝试获取当前 class 的锁。
-
: 但本质都是获取对象的监视器monitor
5.1.2 synchronized 的使用
-
修饰实例方法
synchronized void method() { //业务代码 }
-
修饰静态方法
synchronized static void method() { //业务代码 }
-
修饰代码块
synchronized(this) { //业务代码 }
总结:
- synchronized 方法 加到 static静态方法 和 synchronized(class) 代码块上都是 获取当前class类的锁
- 尽量不要使用 synchronized(String a) 。 因为JVM中,字符串常量池具有缓存功能。 (不懂,后续再深究-- 2022/3/4)
- 构造方法不能使用 synchronized 修饰,因为构造方法本身属于线程安全的。
5.1.3 谈谈 synchronized 和 ReentrantLock 的区别?
-
两者都是可重入锁:
可重入锁: 指的是自己可以再次获取自己的内部锁,再次获取时,锁的计数器就+1。 只有当锁的计数器 为0时才释放锁。
-
synchronized 是 依赖于JVM实现的。 而ReentrantLock 是JDK层面实现的(需要lock() 和 unlock() 配合 try/catch/finally 来完成),可以查看它的源代码。
-
ReentrantLock 比 synchronized 增加了一些高级功能:
- 等待可中断: 通过lock.lockInterruptibly() 实现。表示正在等待的线程可以选择放弃等待,进而处理其他事情。
- 公平锁: ReentrantLock 可以指定是公平锁 还是 非公平锁,通过 ReentrantLock(boolean )。 而 synchronized 只能是 非公平锁。 **公平锁:**先等待的线程先获得锁。
5.1.4 谈谈 synchronized 和 volatile 的区别?
- volatile 关键字 能够保证数据的可见性,但不能保证数据的原子性。 而 synchronized 可以保证数据的 可见性和原子性。
- volatile 主要用于 多个线程之间的可见性, 而 synchronized 解决的是 多个线程之间访问资源的同步性。
- volatile 是线程同步的轻量级实现,所以 volatile 性能较synchronized 更好一些。
- volatile 只能修饰变量。
5.2 并发编程的三个重要特性
- 原子性: 要么所有的操作都能得到执行并且不会被中断, 要么不执行。
- 可见性: 当一个线程对共享变量进行了修改,那么其它线程可以立即看到修改后的值。
- 有序性
5.3 ThreadLocal
5.3.1 ThreadLocal介绍
ThreadLocal为解决线程安全问题提供一个思路。它为每个线程分配一个独立的变量副本解决了变量并发访问的冲突问题。
private ThreadLocal<{需要修饰的变量}> threadLocal;
- .set() 方法:将变量绑定到当前线程中
- .get() 方法:获取当前线程绑定的变量
5.4 Atomic 原子类
Java 中 Atomic 表示 一个操作是不可中断的。 该类都存放在 java.util.concurrent.atomic 包下面:
基本数据类型:
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型:
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray: 引用类型数组原子类
5.4.1 AtomicInteger 的使用
常用方法:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
使用 AtomicInteger之后,无须对 increment() 加锁也能保证线程安全。
AtomicInteger 类主要利用 CAS (compare and swap) 算法 、volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
六、网络编程
6.1 OSI 七层模型
6.2 TCP/IP 四层模型
- 应用层
- 传输层
- 网络层
- 网络接口层
-
应用层:应用层位于传输层之上,主要提供两个终端设备上的应用程序之间信息交换的服务,定义了信息交换的格式,消息交给下一层来传输。应用层交互的数据单元为:报文
-
**传输层:**负责向两台终端设备进程之间的通信提供数据传输服务。
包括以下两种协议:
- 传输控制协议 TCP:提供面向连接的,可靠的数据传输服务。
- 用户数据协议 UDP: 提供无连接的,尽最大努力的数据传输
-
网络层:负责为分组交换网上的不同主机提供通信服务。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用IP协议,因此分组也叫IP数据报。
-
网络接口层:
- 数据链路层: 作用就是将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息。
- 物理层:作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异
6.3 TCP 三次握手 和 四次挥手
6.3.1 三次握手
《图解Http》
详细表述:
为什么需要三次握手?
主要的目的就是双方确认自己与对方的发送与接收是正常的。
6.3.2 四次挥手
为什么需要四次挥手?
断开一个 TCP 连接则需要“四次挥手”:
- 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送。
- 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号, 客户端进入半关闭状态。
- 服务器-关闭与客户端的连接,发送一个 FIN 给客户端
- 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1, 此时两端释放关闭 TCP连接。
6.4 HTTP协议
6.4.1 介绍
全称 超文本传输协议。 用来规范超文本传输,是一种无状态协议。
HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准(TCP)。通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel)。
6.4.2 HTTP协议工作原理
HTTP协议采用了请求/响应模型。
客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。
服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
-
客户端连接到Web服务器
一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字(socket)连接
-
发送HTTP请求
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头、空行和请求数据 4部分组成。
-
服务器接受请求并返回HTTP响应
一个响应由状态行、响应头、空行和响应数据 4部分组成。
-
释放TCP连接
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;
若connection 模式为keepalive,则 该连接会保持一段时间,在该时间内可以继续接收请求;
-
客户端浏览器解析HTML内容
客户端浏览器首先解析状态行,查看请求是否成功的状态码,然后解析每一个响应头。
6.4.3 浏览器中输入url后,浏览器的工作流程:
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;
- 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求
- 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
- 释放 TCP连接;
6.4.4 HTTP请求和响应格式
响应格式:
6.4.5 HTTP协议 如何保存用户状态?
HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。
在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。
6.4.6 Cookie 的作用是什么? 和 Session 有什么区别
**Cookie 一般用来保存用户信息: **
① 我们在 Cookie 中保存已经登录过的用户信息,下次访问网站的时候页面可以自动帮你把登录的一些基本信息给填了;
② 一般的网站都会有保持登录, 在用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);
③ 登录一次网站后访问网站其他页面不需要重新登录。
**Session 的主要作用就是通过服务端记录用户的状态: **
典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
6.5 URI 和 URL 的区别是什么?
- URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。
- URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。
6.6 WebSocket
一、场景: 在项目中有个功能需要接收实时数据并展示。开始想到了 长轮询的方式,每隔几秒发送一次请求,查询数据库中的数据。但这种方法耗费的资源较多,数据量大的时候,程序的使用体验不好。然后就考虑了消息队列,但是消息队列的学习成本较高,耗时长。最后在朋友的推荐下了解到了 WebSocket协议,感觉更符合现在项目的情况,所以就选择了WebSocket。
二、是什么 WebSocket是一种在单个TCP连接上进行全双工通信的协议,属于应用层。 WebSocket使得客户端和服务器之间的数据交换变得更简单,允许服务端主动向客户端推送数据。 浏览器和服务器只需要完成一次握手,两者就能够建立持久性连接,并进行双向数据传输。
三、优势(相较于HTTP)
开销少: 相对于HTTP请求每次都要携带完整的头部,WebSocket 用于协议控制的数据包头部相对较小。
**实时性:**WebSocket协议是全双工的,服务器可以随时主动给客户端发送数据。
有状态: WebSocket协议需要先创建连接,使其成为一种有状态的协议。
**更好的二进制支持:**WebSocket定义了二进制帧,相对HTTP,能够更轻松地处理二进制内容。可以扩展协议。更好的压缩效果。
**四、例子:
——客户端请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
——服务器回应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
**——字段:**
connection: 设置 upgrade ,表示希望连接升级
upgrade: 设置为websocket,表示希望升级为websocket协议
设计模式
1.概念
软件设计模式是一套被反复使用、多人知晓的、经过分类编目的、代码设计经验的总结。它描述了再软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。
2.创建型设计模式
创建型模式的主要关注点是 ”怎么创建对象?“,主要特点是将 ”将对象的创建与使用分离“。这样可以降低系统的耦合度,使用者无须关注对象的创建细节。
2.1 单例模式
**单例模式(Singleton):**指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
单例模式有3个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建
- 单例类对外提供一个访问该单例的全局访问点。
应用场景:
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少GC。
- 频繁访问数据库或文件的对象。
- 某些类创建实例时占用资源比较多的时候
- 当对象需要被共享的场合。使用单例模式创建,共享该对象可以节省内存,加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
实现:
-
**懒汉式:**该模式的特点是,**在第一次加载时不生成单例,**只有当第一次调用 getInstance 方法时才去创建这个单例。
public class LazySingleton { private static volatile LazySingleton instance = null; // 保证 instance 在所有线程中同步 private LazySingleton() {} // 创建私有构造器 public static synchronized LazySingleton getInstance() { if(instance == null) { instance = new LazySingleton(); } return instance; } }
在代码中加入 volatile 和 synchronized 确保在多线程程序中是线程安全的。
-
**饿汉式:**类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在。
public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance() { return instance; } }
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,线程安全。
2.2 工厂模式
工厂模式:定义一个创建对象的接口。将对象的创建工作推迟到具体子工厂类中。
在日常开发中,凡是需要生成复杂对象的地方,都可以尝试考虑使用工厂模式来代替。
复杂对象是指 类的构造函数参数过多等对类的构造有影响的情况。
因为类的构造过于复杂,如果在其它类中使用,使得两者耦合过重,后续需要更改时,消耗大量的时间。
2.2.1 简单工厂模式
在简单工厂模式中创建实例的方法通常为静态方法,因此又称 静态工厂模式。
简单工厂模式中每次增加一个产品就要增加相应的工厂类,违背了”开闭原则“。
优点:
- 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。
- 客户端无需知道所创建具体产品的类名,只需知道参数即可。
- 可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。
缺点:
- 系统扩展困难,一旦引入新产品就需要修改代码逻辑。
- 使用static修饰方法,无法被继承。
模式结构:
- 简单工厂:负责创建所有实例的内部逻辑。
- 抽象产品:简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
- 具体产品:实现抽象产品接口。
具体实现:
public class Client { // 简单工厂
public static void main(String[] args) {
}
//抽象产品
public interface Product {
void show();
}
//具体产品:ProductA
static class ConcreteProduct1 implements Product {
public void show() {
System.out.println("具体产品1显示...");
}
}
//具体产品:ProductB
static class ConcreteProduct2 implements Product {
public void show() {
System.out.println("具体产品2显示...");
}
}
final class Const {
static final int PRODUCT_A = 0;
static final int PRODUCT_B = 1;
static final int PRODUCT_C = 2;
}
// 用户可以选择创建哪一种对象,而无需知道创建对象的细节。
static class SimpleFactory {
public static Product makeProduct(int kind) {
switch (kind) {
case Const.PRODUCT_A:
return new ConcreteProduct1();
case Const.PRODUCT_B:
return new ConcreteProduct2();
}
return null;
}
}
}
2.2.2 工厂方法模式
工厂方法模式是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改代码的情况下引进新的产品,满足开闭原则。
优点:
- 典型的解耦框架: 高层模块只需要知道产品的抽象类,无须关心其它实现类。
- 灵活性增强,对于新产品的创建,只需要多写一个相应的工厂类。
缺点:
- 类的个数容易过多,增加复杂度。
- 增加了系统的抽象性和理解难度。
应用:
- 在不关心产品的细节(参数等), 只关心产品的工厂名。
模式结构:
- 抽象工厂:提供创建产品的接口,通过它来访问具体工厂的工厂方法来创建产品。
- 具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品
- 具体产品
2.2.3 抽象工厂模式
工厂方法模式只考虑到某一类产品的创建。
而抽象工厂模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无需指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
首先搞清楚一个概念: 产品族 和 产品等级
使用抽象工厂模式一般要满足以下条件:
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
优点:
- 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,无需修改原代码。
- 可在类的内部对产品族中相关联的多等级产品共同管理,不必引入多个新的类。
缺点:
- 当产品族需要增加一个新产品种类时,所有的工厂类都需要进行修改。
模式结构:
- 抽象工厂
- 具体工厂
- 抽象产品
- 具体产品
==========================
Java基础知识点
1、 深拷贝和浅拷贝的区别? 什么是引用拷贝?
-
浅拷贝:浅拷贝会在堆上创建一个新的对象(这点区别于引用拷贝)。如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址。
-
深拷贝: 完全复制整个对象,包括这个对象所包含的内部对象。
2、 接口和抽象类有什么共同点和区别?
- 接口:是一个特殊的抽象类。
共同点:
- 都无法实例化
- 都可包含抽象方法,在接口中 抽象方法可以省略 abstract 关键字
区别:
- 接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。对“接口为何是约束”的理解,我觉得配合泛型食用效果更佳。
- **而抽象类的设计目的,是代码复用。**当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
- 接口中的成员变量只能是 public static final 类型,不能被修改且必须有初始值。 而抽象类型的 成员变量默认 default 。
3、 final关键字, 以及 finally、 finalize 的作用?
-
final关键字
final: 可以修饰类、属性、方法和局部变量
使用场景:
- 不希望类被继承时,可以使用final修饰
- 当不希望父类的某个方法被子类覆盖/重写时
- final修饰的变量
- 基本类型: 则值不能修改
- 引用类型: 则该变量不能指向其它对象。
-
finally关键字
-
finalize关键字
4 、String、StringBuffer和StringBuilder 三者之间的区别?
先上结论:
String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String 操作都会生成一个新的对象,造成占用大量的内存空间 | StringBuffer是可变类,和线程安全的类,任何对它指向的字符串的操作都不会产生新的对象。 每个StringBuffer对象都有一定的缓冲区容量,当字符串大小超过容量时,会自动增加容量。 | StringBuilder是个可变类,相比StringBuffer,速度更快。主要操作有: append 和 insert |
不可变 | 可变 | 可变 |
线程安全 | 线程不安全 | |
用于 多线程操作字符串 | 用于 单线程操作字符串 |
-
String 值不可变的原因:
首先 String 维护了一个 final修饰的 char[] 来保存数据。 (final修饰引用类型的情况。)其次,该数组使用 private 修饰,并且没有提供修改这个字符串的方法。
-
String的两种创建方式
-
直接赋值 String s = “lby”;
-
调用构造器 String s = new String(“lby”);
对于第一种:先从常量池中查看是否有 “lby” 数据空间,如果有,直接指向;如果没有则重新创建,然后指向。s最终指向的是常量池的空间地址
对于第二种: 先从堆中创建空间,里面维护了value属性,指向常量池的 lby 空间。如果常量池没有 “lby” ,重新创建,如果有,直接通过value指向。 最终指向的是堆中的空间地址。
-
-
-
StringBuilder和StringBuffer 都继承 AbstractStringBuilder类, 使用字符数组保存字符串,但没有使用final 和 private 修饰。
abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; } //... }
-
String 中的对象是不可变的,也就可以理解为常量,线程安全。 StringBuffer 对方法添加了同步锁 ,是线程安全的。而StringBuilder对方法没有添加同步锁,是非线程安全的。
-
字符串拼接用 ”+“ 还是 StringBuilder ?
”+“ 的字符串拼接方式,实际上是使用 StringBuilder 调用 append() 方法实现的。
-
String s1 = new String(“abc”) 创建了几个对象?
- 如果字符串常量池中已存在字符串常量"abc", 则只会在堆空间创建一个字符串常量“abc”。
- 如果字符串常量池中不存在字符串常量"abc", 则先会在字符串常量池中创建一个对象,然后在堆空间创建一个字符串常量“abc”。共2个对象。
5 、Java 泛型、 类型擦除是什么?
Java泛型是 JDK5 引入的一个新特性、泛型提供了编译时类型安全检测机制,允许在编译时检测到非法的类型。
类型擦除是 指在Java运行期间,所有的泛型信息会被清除
List<Integer> list = new ArrayList<>();
list.add(12);
list.add("a"); //这里直接添加会报错, 因为泛型指定的是 Integer 类型。
// 反射: 动态添加。
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加是可以的
//这就说明在运行期间所有的泛型信息都会被擦掉
add.invoke(list, "kl");
System.out.println(list);
泛型的定义:
5.1 自定义泛型类
Class 类名<T,R,......> {
// ... 表示可以有多个泛型
成员
}
- 普通成员可以使用泛型(方法、属性)
- 数组类型的泛型不能初始化。
- 静态方法(static)中不能使用类的泛型
- 如果在创建对象时,没有指定类型,则默认为Object
5.2 自定义泛型接口
interface jiekou<T,R,...> {
}
- 泛型接口的类型,在继承接口或者实现接口时确定
- 没有指定类型,默认为Object
5.3 自定义泛型方法
修饰符 <T,R...> 返回类型 方法名(参数列表) {
}
- 泛型方法,可以定义在普通类中,也可以定义在泛型类中
5.4 泛型的继承和通配符
// 泛型不具备继承性
List<Object> list = new ArrayList<String>(); // 出错, 泛型不具备继承性
<?> : 支持任意泛型类型;
<? extends A>: 支持A类以及A类的子类,规定了泛型的上限;
<? super A>: 支持A类以及A类的父类,不限于直接父类。
? 表示不确定的 Java 类型
T (type) 表示具体的一个 Java 类型
K V (key value) 分别代表 Java 键值中的 Key Value
E (element) 代表 Element
6、 类加载的时机?
-
通过new方法、反射创建对象时
-
当子类被加载时。
-
调用类中的静态成员时。
7 、访问控制符
8、comparable 和 comparator 的区别
-
comparable 出自 java.lang 包, 有一个 **compareTo(**Object object) 方法用来排序。
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 // 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 // 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 public class Person implements Comparable<Person> { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } /** * T重写compareTo方法实现按年龄来排序 */ @Override public int compareTo(Person o) { if (this.age > o.getAge()) { return 1; } if (this.age < o.getAge()) { return -1; } return 0; } }
-
comparator 出自 java.util 包,有一个 compare(Object obj1, Object obj2)
List list = new ArrayList<>(); Collections.sort(list, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; // >0 则o1 大于 o2; =0 则o1 等于 o2; <0 则o1 小于 o2 } });
9、为什么hashcode() 和 equals() 必须相容?
tips:
- 对于八大基本类型, equals 和 hashcode 方法都是根据 内容 执行的。
- 而对于 对象Object,equals 和 hashcode 方法 默认根据对象的 地址 执行的。
注:
- equals() = true 时,hashcode() 必须为 true
- equals() = false 时, hashcode() 可以为true 也可以为 false
- hashcode() = true 时, equals() 不一定为 true
- hashcode() = false 时,equals 一定为 false
原因如下:
集合是 无序、不重复的。
equals() 和 hashcode() 必须相容 是因为将 对象加入到 Hash表中的时候, 需要根据 对象的 hashcode() 来计算 对象的存储位置。 若只重写equals() ,则如果equals()=true,可能 hashcode() = false,(出现存储两个重复的对象)。若只重写 hashcode() , 类似上述。 违背我们开发的意愿。
10、 == 和 equals 的区别?
-
“==”: 既可以判断基本类型,也可以判断引用类型
如果判断 基本类型,则判断值是否相同
如果判断 引用类型,则判断地址是否相同,即判断是不是同一个对象。
-
“equals”: 默认判断地址是否相同,往往会重写该方法,来判断内容是否相同。
11、什么是线程死锁?如何避免死锁?
-
**线程死锁:**指多个线程在资源竞争过程中造成的一种阻塞现象,若无外力推动,将无法进行下去。
形成条件:
- 互斥条件:一个资源同一时刻只能被一个线程占用,直至释放
- 请求与保持条件:线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前,不能被其他线程强行剥夺。
- 循环等待条件:当发生死锁时,锁等待的线程必定会形成一个环路,造成阻塞。
-
预防死锁:
若要预防死锁, 需要破坏死锁的形成条件:
- 破坏请求与保持条件
- 破环不剥夺条件
- 破坏循环等待条件
-
避免死锁:避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态:指系统能够按照某种进程推进顺序(P1、P2、P3…Pn)来为每个进程分配资源,直到满足每个进程对资源的最大需求,使每个进程能够顺利完成。
12、Java八种基本数据类型
-
6种数字类型:
- 4种整数型:byte、short、int、long
- 2种浮点数:float、double
-
字符类型:char
-
布尔型:boolean
8种数据类型的初始化值 及 所占空间:
包装类型的常量池:
-
Byte、Short、Integer、Long 这4种包装类默认创建了[-128, 127] 的相应类型的缓存数据
Integer 缓存源码:
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; ... } }
-
Character 创建了数值在[0, 127] 范围的缓存数据
Character 缓存源码:
public static Character valueOf(char c) { if (c <= 127) { // must cache return CharacterCache.cache[(int)c]; } return new Character(c); } private static class CharacterCache { private CharacterCache(){} static final Character cache[] = new Character[127 + 1]; static { for (int i = 0; i < cache.length; i++) cache[i] = new Character((char)i); ... } }
-
Boolean 直接返回 true 和 false
13、Sleep 和 wait 方法的区别与联系?
- 调用 Sleep() 方法 并不会释放锁, 调用Wait() 方法后释放锁,并将线程加入到等待队列中。
- sleep 是 Thred类的静态本地方法, wait 是Object 的本地方法。
- sleep 通常用于线程休眠, 在休眠结束后,再进入就绪态。 wait方法通常用于多线程之间的通信。
14、#{} 和 ${} 的区别?
- #{}是预编译处理,${}是字符串替换。
- 预编译是提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译
- 使用 #{} 可以有效的防止SQL注入,提高系统安全性。
15、Java动态绑定
- 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
- 当调用对象属性的时候,没有动态绑定机制,哪里声明,就在哪里调用
16、什么是代码块?
代码块:相当于另一种形式的构造器,可以做初始化的操作。
- 如果多个构造器中都有重复的语句,可以抽取到代码块中,提高代码的复用性。
- 在创建一个对象时,代码块在一个类中的调用顺序:
- 调用静态代码块和静态属性初始化(静态代码块和静态属性的优先级一样
- 调用普通代码块和普通属性的初始化(普通代码块和普通属性的优先级一样
- 调用构造函数
- 创建一个子类时,代码块、属性、构造函数的调用顺序:
- 父类的静态代码块和静态属性
- 子类的静态代码块和静态属性
- 父类的普通代码块和普通属性
- 父类的构造函数
- 子类的普通代码块和普通属性
- 子类的构造函数
17、什么是内部类?
**局部内部类:**定义在外部类的局部位置(比如 方法中或者代码块中)
- 可以直接访问外部类的所有成员,包含私有的
- 不能添加访问修饰符。 但可以使用 final修饰
- 作用域:仅在它的方法或代码块中
- 局部内部类访问外部类的成员-》直接访问
- 外部类访问局部内部类的成员-》先创建对象,再访问。
匿名(指隐藏名字,而不是没有名字)内部类:匿名内部类是定义在外部类的局部位置,比如方法中,并且没有类名
new 类或接口(参数列表) { // 该类或接口已创建。
类体
};
**成员内部类:**定义在外部类的成员位置,并且没有static修饰。
- 可以直接访问外部类的所有成员,包含私有的。
- 可以添加任意访问修饰符(public、protected、默认、private) ,因为他的地位就是一个成员。
**静态内部类:**定义在外部类的成员位置,并且有static修饰
- 可以直接访问外部类的所有静态资源,包括私有的,不能访问非静态资源
- 可以添加任意访问修饰符(public、protected、默认、private) ,因为他的地位就是一个成员。
18、重写和重载的区别?
- 重写:(运行时多态)
子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能小于父类的。
-
发生在父类与子类之间
-
方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
-
访问修饰符的限制一定要大于 或等于被重写方法的访问修饰符(public>protected>default>private)
-
重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
-
重载:(编译时多态)
在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
- 重载Overload是一个类中多态性的一种表现
- 重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
- 重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
19、 ++、-- 操作 是线程安全的吗?
不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。
20、说说JDK8 的新特性?
1、JDK8提供了接口static和Default方法。特别是Default修饰的方法,dafault修饰符是我们设计模式中的适配器设计模式的重要实现原理,让我们接口实现类不需要重写全部的抽象方法,default修饰的方法可以选择性的重写
2、JDK8新增了线程安全的日期API,在Java.time包下,如LocalDateTime,ZonedDateTime
3、新增了Stream流的支持, 例如FileInputStream,FileOutPutStream。
4、数组集合增加了支持并行操作
5、Lambda表达式:匿名函数。 允许把函数作为一个方法的参数。
6、方法引用
JDK8支持了四种方式方法引用
类型方法引用引用静态方法ContainingClass::staticMethodName
引用特定对象的实例方法containingObject::instanceMethodName
引用特定类型的任意对象的实例方法String::compareToIngoreCase
引用构造函数ClassName::new
21、关于 instanceOf 比较操作符的作用?
判断对象的运行类型是否为 XX类型 或 XX类型的子类型
22、 位反运算符 “~” ?
"~"是位非运算符 , 作用是将每位二进制取反,如: ~12 = -13
int 在java中占 4个字节,共32bit ,12的表示: 00000000 00000000 00000000 00001100
取反得: 11111111 11111111 11111111 11110011; 该值为负数且为结果的补码。 又因为计算机中的值使用补码表示,
负数的补码 = 原码的反码 + 1, 且符号位不变。(这里要注意 取反 和 反码的区别。) 则:
原码的反码: 11111111 11111111 11111111 11110010,
原码: 10000000 00000000 00000000 00001101 = -13
取非的 快速计算方式: -(a + 1);
23、 什么是this关键字, 什么是 互斥锁?
this关键字
Java虚拟机会给每个对象分配this,代表当前对象
- this关键字用来访问本类的属性、方法、构造器
- this用于区分当前类的属性和局部变量
- 访问构造器语法: this(参数列表); // 只能在构造器中使用,此时 this语句必须放在第一条
互斥锁
- 引入互斥锁来保证共享数据的完整性。
- 每个对象都对应于一个称为“互斥锁”的标记,每个标记保证在同一时刻,只能有一个线程来访问该对象。
- 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。
- 非静态的同步方法的锁可以是this,也可以是其它对象(同一对象)。
- 静态的同步方法的锁为当前类本身。