1.JVM & JDK & JRE
- JVM是java虚拟机,运行java字节码(.class文件)。不同平台对应不同的虚拟机,运行相同的字节码可以给出相同的结果。实现一次编译,随处运行。
- JRE是java运行时环境,包括JVM以及一些java库
- JDK是java开发工具,包括java编译、debug等工具,还包括JRE
2.Java为什么“编译和解释”并存
java文件通过编译生成字节码文件,然后可以通过解释器解释一句,执行一句;之后引入了JIT(运行时编译),先将java源文件一次性编译,然后再执行。
解释:源文件-编译-解释-执行
编译:源文件-一次性编译好-执行
3.Java与C++的区别
- java无法通过指针访问内存,更加安全
- java有垃圾自动回收机制,而C++需要手动释放指针
- C++支持方法重载和操作符重载,而java只支持方法重载
4.java基本类型与包装类型的区别
- java基本类型通常用于常量或局部变量中,一般方法参数和成员变量很少用基本类型
- 与包装类型相比,基本类型所需存储空间较小
- 基本类型有默认值且不为null,包装类型默认值为null
- 基本类型用==比较值是否相等,包装类型 = =比较的是内存地址,一般用equals方法比较值是否相等
- 包装类型属于对象类型,一般存储在堆中,基本类型的局部变量一般存储在虚拟机栈中,基本类型 的成员变量存储在堆中
5.重载和重写的区别
重载发生在同一个类中,重载方法名相同,参数、返回类型等可以不同。
重写一般发生在父类和子类中,方法名、参数要相同,返回类型可以是父类或者其子类,访问修饰符要大于等于父类的访问修饰符,抛出异常的范围要小于父类。
6.面向对象三大特征
- 封装:将成员属性封装在内部,只能通过提供给外部访问的方法来获取
- 继承:子类继承父类的属性和方法,同时还可以自己扩展属性和方法
- 多态:父类应用指向不同子类时执行不同的动作
7.接口类和抽象类的区别
相同:都不能被实例化、都可以包含抽象方法
不同:一个类可以实现多个接口,但是只能继承一个类;接口主要用于对类的行为进行约束,实现一个接口就有了对应的功能,抽象类主要用于代码复用,强调了所属关系。
8.浅拷贝和深拷贝
浅拷贝时会在堆上创建一个新的对象,如果对象内部有引用,则会复制引用,原引用和新引用指向同一个对象
深拷贝完全的复制对象。
9.String
(1)String不可修改:String内部使用priavte final char[]
final既不能被继承也无法重写。所以String对象修改时会创建一个新的String对象,原指针指向新的对象
(2)StringBuilder:该对象的修改是修改的StringBuilder对象本身,使用同步锁机制,是线程安全的
(3)StringBuffer:也是修改的对象本身,但是线程不安全
10.HashCode()有什么用
当使用HashMap的put时候,会先计算对象对应的HashCode,找到对应的位置,用equals方法比较该位置上的对象与所要加入的对象是否相等,相等则不加入,否则加入。
11.字符串常量池是否了解
字符串常量池可以避免字符串的重复创建,提高性能。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
12.异常
所有异常都有一个公共祖先:Throwable类
它有两个子类:Exception和Error。
- Exception:程序本身可以处理的异常,又分为不受检查异常和受检查异常。不受检查异常如数组越界、空指针等。
- Error:系统内部错误,如虚拟机运行错误等。
13.泛型
就是将原来指定的类型参数化,使用时再传入具体的类型。
14.反射
在运行过程中,对于任何一个类都可以知道这个类的属性和方法。要解剖一个类,就要获取到该类对应的字节码对象。
参考博客
15.序列化和反序列化
(1)序列化是将数据结构或对象转化为二进制流的过程,反序列化则是相反。
序列化主要目的是通过网络对象传输数据或者将数据结构和对象存储在文件、内存、数据库中。
(2)如果不想被序列化,可以在关键字上使用transient。注意transient只能用于变量,不能用于类和方法上。被transient修饰的变量,反序列化之后会恢复默认值,如int->0。
16.动态代理和静态代理
静态代理是指代理类在编译期就已经确定,即需要事先手动编写一个代理类。动态代理则是在运行时动态生成代理类。
JDK动态代理只能代理实现了接口的类,而CGLIB动态代理任何未实现接口的类。
17.流包括字节流和字符流。字节流常见的有InputStream、OutputStream、FileInputStream、FileOutputStream。字符流有Reader、Writer、BufferReader、BufferWriter。
- AIO (Asynchronous I/O):异步非阻塞,通过事件通知模型,无需等待I/O操作完成,应用线程可以继续执行其他任务。
- BIO (Blocking I/O):同步阻塞,发起I/O操作时线程会阻塞,直到操作完成,适合低并发场景。
- NIO (Non-blocking I/O):同步非阻塞,使用选择器监控多个通道,当事件发生时,应用线程进行处理,提高了并发性能。
18.Java内存
主要有堆、方法区(JDK1.7,1.8及之后放置在元空间)等共享区域。堆用来存放实例对象,方法区存放加载的类信息,常量、静态变量、方法信息等。运行时常量也存放在方法区中,用来存放一些字面量和符号引用的常量池表。
而每个线程都有自己独立的虚拟机栈、本地方法栈和程序计数器。虚拟机栈里面存放的每个栈帧代表一个方法,包含方法使用的操作数、局部变量表、返回地址等。本地方法栈则存放的Native方法操作数等。程序计数器存放的是下一条指令执行的地址。
- 对象的访问定位
reference在局部变量表中。
- 句柄:java堆会划出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄包括对象实例数据和对象类型数据各自的具体地址信息。
- 直接指针:reference直接指向对象地址。
19.jvm垃圾回收
垃圾收集器管理的主要区域是java堆。
java堆通常分为新生代、老生代和永久代(jdk8之后被元空间取代)。
首先创建对象会在Eden区域进行分配,如果Eden区域已经满了,会触发Minor GC,在Minor GC中存活的对象会被移到Survivor(1岁)中。当Survivor空间不足的时候会触发Minor GC,将存活的对象移到老年代(15岁)。对于大对象和长期存活的对象将会进入老年代。
20.Minor GC、Major GC、Mixed GC、Full GC
- Minor GC:新生代的垃圾收集
- Major GC:老生代的垃圾收集
- Mixed GC:新生代和部分老生代的垃圾收集
- Full GC:整个java堆和方法区的垃圾收集
21.死亡对象判断:可达性分析
通过GC roots判断可达到的对象,如果对象不可达则进行回收。GC Roots可以是虚拟机栈中引用的对象、本地方法栈引用 的对象、方法区中类静态变量引用的对象、方法区中常量引用的对象等。
- 垃圾收集算法
- 标记——清除算法:标记不需要回收的对象,标记后回收所有没有被标记的对象。效率低易产生碎片。
- 复制算法:把内存大小分为相等的两块,每次使用其中的一块。当这一块使用完内存后,将还存活的对象复制到另一块,然后把使用过的空间一次清理掉。这样每次回收内存都是对内存区间的一半进行回收。但是不适合老年代,存活对象太大的时候,复制性能变差。而且可用内存变为原来的一半。
- 标记——整理算法:和标记——清除算法过程一样,但是后续不是对对象回收,而是让所有存活对象移向一端,然后直接清理掉端边界以外的内存。
- 分代收集算法:新生代可以选择复制算法,老生代使用标记——清除或标记——整理算法。因为老生代的对象存活率较高,而标记类算法效率较低。
23.垃圾收集器
-
Serial:单线程执行
-
Serial Old:
-
ParNew:多线程执行
-
Parallel Scanvenge:
-
CMS:是一种并发的垃圾回收器,在垃圾回收中尽量减少应用程序停顿时间。
初始标记(GC Roots可直达对象,stw)——并发标记(从可直达对象遍历整个对象标记图)——重新标记(重新标记并发标记中产生的垃圾,stw)——并发清除
对cpu资源敏感,无法处理浮动垃圾,“标记——清除”算法会产生大量碎片 -
G1:面向服务端,旨在在有限时间内提高吞吐量。
优点: -
并发和并行加快吞吐量
-
分代收集
-
空间整合:使用的是标记整理算法
-
可预测的停顿
G1 收集器将堆划分为大小相等的独立区域,在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。
初始标记(stw)——并发标记——最终标记(stw)——筛选回收(制定回收计划,选择多个Region构成回收集,把回收集中的Region存活对象复制到空的Region中,再清理掉整个旧的Region空间。stw) -
更好处理碎片化问题
-
缩小单次垃圾回收时间
-
可预测性
24.类加载过程
- 加载:加载类的字节码到内存中
- 检查:检查该字节码是否符合java虚拟机规范
- 准备:为类的静态变量(类变量)分配内存,并设置初始值。
- 解析:虚拟机将符号引用转化为直接引用
- 初始化
- 使用
- 卸载
25.双亲委派机制
当一个类加载器加载一个类的时候,首先委派父类加载器尝试加载,只有当父类加载器无法加载的时候,才会自己尝试加载。这种委派加载机制保证了类的加载不会重复,并保证类加载的安全性和一致性。
java集合源码解析
- ArrayList:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组(用于空实例)。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于默认大小空实例的共享空数组实例。
//我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 保存ArrayList数据的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList 所包含的元素个数
*/
private int size;
/**
* 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传入的参数大于0,创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果传入的参数等于0,创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//其他情况,抛出异常
throw new IllegalArgumentException("Illegal Capacity: " +
initialCapacity);
}
}
/**
* 默认无参构造函数
* DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
*/
public ArrayList(Collection<? extends E> c) {
//将指定集合转换为数组
elementData = c.toArray();
//如果elementData数组的长度不为0
if ((size = elementData.length) != 0) {
// 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
if (elementData.getClass() != Object[].class)
//将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 其他情况,用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
/**
* 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//下面是ArrayList的扩容机制
//ArrayList的扩容机制提高了性能,如果每次只扩充一个,
//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。
/**
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
*
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
//如果是true,minExpand的值为0,如果是false,minExpand的值为10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
//如果最小容量大于已有的最大容量
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则直接返回最小容量
return minCapacity;
}
// 确保内部容量达到指定的最小容量。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量,
//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
//比较minCapacity和 MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
/**
* 返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {
for (int i = 0; i < size; i++)
//equals()方法比较
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。.
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size - 1; i >= 0; i--)
if (elementData[i] == null)
return i;
} else {
for (int i = size - 1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。)
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
//Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// 这不应该发生,因为我们是可以克隆的
throw new InternalError(e);
}
}
/**
* 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
* 返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
* 因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
从源码可以看出,如果没有给容量,会默认初始化一个空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,当添加第一个元素的时候,数组初始化容量为10,如果给了容量为0,初始化为空数组EMPTY_ELEMENTDATA
,扩容机制是每次扩容为原来的1.5倍:
int newCapacity = oldCapacity + (oldCapacity >> 1);
- LinkedList
- HashMap:
如果节点数超过阈值(容量*负载因子),就会动态扩容为原来的两倍。因为算索引是(n-1)&hash这种方法,hash值范围是-231~231-1,但是内存放不下这么大的空间,所以要先对数组长度取模。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素(处理hash冲突)
else {
Node<K,V> e; K k;
//快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
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);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
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值与插入元素相等的结点
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.如果定位到的数组位置没有元素 就直接插入。
2.如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
JDK1.7的时候链表采用头插法,所以要先遍历链表,看key是否相等,相等则覆盖。否则插入头部。
hashmap每一次动态扩容都是原来的2倍。
- ConcurrentHashMap