序言
主要分为两篇,一篇基础篇,涵盖Java基础,数据库,JVM,计算机网络等知识
另一篇为框架篇,主要为流行框架,如Spring、SpringMVC、Mybatis、SpringBoot、SpringCloud、Redis、Linux等知识
文章目录
- 序言
- 基础篇
- 谈谈对面向对象思想的理解
- JDK,JRE,JVM有什么区别?
- Java的基本数据类型有哪些?
- ==和equals的区别 | &和&&的区别
- final的作用
- String,StringBuffer,StringBuilder区别
- String s = "java"与String s = new String("java")
- 接口和抽象类的区别
- 算法题-求N的阶乘
- 什么是向上转型?什么是向下转型?
- Int和Integer的区别(重点)
- 方法的重写和重载的区别
- List和Set的区别
- 谈谈ArrayList和LinkedList的区别
- 如何在双向链表A和B之间插入C
- Object类中的方法介绍
- equals()与 hashcode() 的区别
- 除了NEW还有什么创建类的方式
- Exception 和 Error 的区别
- 反射原理及使用场景
- Java会发生内存泄漏问题吗?有哪些场景会发生内存泄漏
- ArrayList的扩容机制
- ConcurrentHashMap 的 put 方法过程
- HashMap的扩容机制
- 为什么HashMap长度都是2的次幂
- HashMap为什么使用红黑树而不使用别的树?
- 为什么链表长度大于阈值 8 时才将链表转为红黑树?
- CopyOnWriteArrayList
- Servlet和Filter的区别
- 悲观锁和乐观锁
- CAS
- Java对象头存放什么内容
- Volatile 关键字以及底层实现
- 线程池,线程池的作用
- AQS
- 谈谈HashSet的存储原理
- 谈谈LinkedHashMap和HashMap的区别(重点)
- 谈谈ConcurrentHashMap,HashMap,Hashtable的区别
- ArrayList vs Vector vs CopyOnWriteArrayList
- 开发一个自己的栈,你会怎么写?
- 补充:集合知识小结
- 谈谈IO流的分类及选择
- serialVersionUID的作用是什么
- 请描述下Java的异常体系
- 罗列常见的5个运行时异常和非运行时异常
- throw跟throws的区别
- 一道关于try catch finally返回值的问题
- 创建线程的方式
- 一个普通main方法的执行,是单线程模式还是多线程模式?为什么
- 请描述线程的生命周期
- 谈谈你对ThreadLocal的理解
- 谈谈AJax的工作原理
- 谈谈Servlet的生命周期
- 描述Session跟Cookie的区别(重要)
- 补 Get请求与Post请求的区别
- 转发和重定向的区别
- Iterator和ListIterator的区别?
- 并发和并行的区别
- 什么是序列化?
- 说说synchronized底层原理
- synchronized和volatile的区别
- synchronized和lock的区别
- 什么是死锁?如何防止死锁?
- 什么是反射?可以解决什么问题?
- 什么是悲观锁,什么是乐观锁?
- 数据库说明
- 计算机网络
- JVM虚拟机
基础篇
谈谈对面向对象思想的理解
面向对象是一种编程思想。面向对象程序设计的核心思想是以对象为核心。除了面向对象之外还有面向过程,二者是两种不同的开发思想。
当我们需要完成生成随机数这一个功能时,如果是以面向过程的思想进行开发,则更加专注于设计的这个实现的算法;但是以面向对象的特性来完成这个功能时,我们更强调的是对象,通过找一个能够生成随机数功能的对象来帮我们完成(如Random),作为开发者并不需要关注这个代码是怎么实现的,找到合适的对象,然后调用对象的方法即可。
面向对象的四大特性:封装、集成、多态、抽象
封装性:封装简单的说就是将细节隐藏起来,外界只管调用,无需在意细节,这也提高了程序的安全性;在Java当中最常见的两个封装就是定义方法、private关键字,其中定义一个类,也是封装。
继承:就是从已有的类中得到继承关系的过程。通过继承,能够使得代码在保持原有基础上,能得到更多拓展。通常将对象的共有特征进行抽取得到父类。
- 父类和子类的关系:子类也是父类。如,讲师、助教也是属于员工的范畴。
- this代指当前的对象,而super代指父类对象(超类)
多态:是指运行不同子类型的对象对同一消息作出不同的响应。
讲师和助教都属于员工,他们都有共同的行为,如:工作。但是讲师的工作是上课,助教的工作是帮助教学,虽然他们都是工作,但是工作内容是不一样的
多态的格式:超类型的引用指向派生类 或 父类型的引用指向子类
//类: 父类 对象名 = new 子类()
Employee one = new Teacher(); //此时将教师向上转型为员工
//接口: 接口 接口名 =new 接口的实现类()
使用多态的最大好处就是能够降低代码的耦合性;
JDK,JRE,JVM有什么区别?
分别是都是三个单词的缩写
JDK: Java Development Kit ,指的是Java开发的工具包,提供了Java开发环境和运行环境
JRE:Java Runtime Environment,Java运行环境,包含Java虚拟机及一些基础类库
JVM:Java Virtual Machine,Java虚拟机,提供执行字节码文件的能力
JVM实现跨平台的流程:
将编写好的.java文件进行编译成.class文件,此时不同平台的JVM就会去运行这个.class文件,从而实现跨平台的特性。
Java的基本数据类型有哪些?
整形:byte、short、int、long
浮点型:double,float
布尔型:boolean
字符型:char
一共八种基本数据类型。
String是引用类型
以下是占用字节情况:
名称 | 占用字节 |
---|---|
byte | 1 |
short | 2 |
int | 4 |
long | 8 |
double | 8 |
float | 4 |
char | 2 |
==和equals的区别 | &和&&的区别
==和equals的区别:
-
== 是比较运算符,equals是Object的一个方法
-
== 在比较在基本类型使用中,是判断数值是否相等,而在比较引用类型时,则比较引用类型的地址值是否相等
-
equals是用于比较内容是否相等,但是在Object的源码当中可以发现其实内部也是使用 == 运算符的
public boolean equals(Object obj) {
return (this == obj);
}
因此在没有重写equals方法前,它的作用是与 == 一样的。
为了实现内容相同,通常需要对equals进行重写,如String类就对该方法进行了重写,从而实现了内容上的比对。
以下是String类重写了equals的源码:
public boolean equals(Object anObject) {
if (this == anObject) {//判断是否是当前对象
return true;//是同一个对象的话就返回true
}
if (anObject instanceof String) {//判断是否都是String类或子类
String anotherString = (String)anObject;//是的话就对内容就行逐个比对
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
//不是当前类或子类或接口实现类,就返回false;
return false;
}
&和&&的区别:
&和&&都能够进行逻辑与运算
if(a==b && a>0) //当左边a==b的结果为false时,会直接短路掉右边,因此右边的a>0是不会继续执行的
if(a==b & a>0) //当左边a==b的结果为false时,不会短路掉右边,因此右边的a>0会继续执行的
final的作用
final的中文意译为 最终的;最后的
因此被final修饰的类、方法、变量也都是’最终的‘
也就是说:
- 被final修饰的变量为常量,不可再变。
- 被final修饰的方法不可以被重写
- 被final修饰的类不可以再变,也不能被继承。
如果修饰的是引用变量,则引用变量的地址值不能发生改变。
但是可以修饰该变量下类的属性。
//例如
final Person person = new Person(30,"tomcat");
//此时仍然可以对Person的属性进行赋值
person.setAge(35);
String,StringBuffer,StringBuilder区别
查看源码可以发现,String类在内部使用了final关键字,因此一旦创建对象就意味着不可以改变自身,在重新赋值时等于重新生成了一个新的字符串对象。
StringBuffer是可以改变自身对象的,因此在推荐在需要经常对字符串内容的场景下使用。
线程安全性:
String是由final修饰的,定义后对象的内容不可变,因此是线程安全的。
StringBuffer在内部的方法下加入了syschronized,执行的效率低,但是是线程安全的。
StringBuilder跟StringBuffer一样,区别在于StringBuilder没有使用同步锁,执行效率高,是线程不安全的类。
String s = "java"与String s = new String(“java”)
通过第一种形式:String s = "java"来得到的引用类型s,此时JVM会将这个变量放入常量池当中,当有别的变量的值也为’java’时,就会共享这一份String
通过String snew = new String(“java”)这种形式得到的变量snew ,是通过new的形式得到的,此时会在虚拟机的堆内存中开辟内存空间。
一道关于String和数组的笔试题
public static void main(String[] args) {
int[] a = {1,2,3,4,5};
String str = "abc";
test(a,str);
System.out.print(str+" and ");
System.out.println(Arrays.toString(a));
}
private static void test(int[] a, String str) {
a[0] = 10;
str = "def";
}
显示的结果为?
abc and [10, 2, 3, 4, 5]
在JVM运行时内存区域中,变量参数存在栈中,new出来的对象或数组存放在堆中。
在Java当中方法参数传递为值传递,当传递的参数是基本类型时,就把基本类型的值传递给方法;当传递的参数是引用类型时,此时会把对象在堆内存中的引用地址传递给方法
当把字符串和数组当参数调用方法时,实际传递的是其在堆内存中的地址,如此处为 0x666、0x999
因此当修改数组时,实际就是修改该堆内存的数组
然而,String为final修饰的内容,实际上是不可以更改的,因此仍然显示“abc”
接口和抽象类的区别
抽象类与接口之间的区别:
从语法来说:
-
抽象类:方法可以有抽象的,也可以有非抽象, 有构造器
- 接口:方法都是抽象,属性都是常量,默认有public static final修饰
-
JDK1.8之后:
-
接口里面可以有实现的方法,注意要在方法的声明上加上default或者static
从开发的角度:
抽象类:
抽象类可以有抽象方法也可以有具体的方法,如在MVC三层架构中,Dao层编写多个UserDao,此时可以抽象类,抽取常用的CRUD方法作为每一个dao的通用方法,而需要同一个方法的不同实现时,就在抽象类中编写抽象方法。
接口:
接口更像是一种开发的规范,虽然在JDK1.8之后也可以写实例方法,但是一般不这么采用。
在MVC三层架构的实际开发中,service调用dao,开发中需要将实现类注入到IOC容器当中,实际调用是由接口调用,由接口指向实现类。
当接口的实现类不能满足功能时,可以随时将新的实现类注入到IOC容器中,由于是用接口调用实现类,此时接口作为一个种规范,并不需要调整相关的代码,从而实现低耦合高内聚的目的。
算法题-求N的阶乘
递归就是自己调用自己,在JVM中每调用一次方法就会将该方法压入栈内,调用的次数过多还未来得及出栈,此时就会发送溢栈——栈内存溢出,所以递归的次数不能过多。
做递归题重要的就是要找到跳出递归的突破口
public class DgTest {
public static void main(String[] args) {
Integer integer = test01(10);
System.out.println("结果为"+integer);
}
/**
* 使用递归计算n的阶乘
*/
public static Integer test01(Integer count){
if(count <= 0){
throw new RuntimeException("非法参数");
}
if(count==1){
return 1;
}
return test01(count-1)*count;
}
}
总结递归调用:
- 递归调用要注意出口位置
- 递归调用次数不要过多,防止栈内存溢出
什么是向上转型?什么是向下转型?
向上向下转型发生在父子类当中。
如医生和老师都是继承于员工类,员工又继承于Object类,此时的关系如图所示。
当类的类型又Object往下变化时,就为向上转型,反之则为向上转型。
用代码的形式描述:
Employee teacher = new Teacher();//由Teacher向上转型为Employee
Docter docter = new Employee();//由Employee向下转型为Docter
在继承的描述关系中,A继承于B,也可以称B就是A;所以我们可以说医生、老师就是员工,员工就是Object。因此发生向下转型时,是不会报错的。
但是反过来,员工可以有很多种,既可以为老师也可以为医生,如果需要向下转型, 如果父类不能确定是指定的子类,则父类是不可以随意转型为子类的,不然就会报 ClassCastException 。
因此在需要向下转型时,需要先用instanceof来判断是否是子类。
Employee employee = new Docter();
if(employee instanceof Docter){
Docter docter = new Employee();//由Employee向下转型为Docter
}
Int和Integer的区别(重点)
看以下例子:
Integer a = new Integer(3);Integer b = 3; //自动装箱,会调用Integer的valueOf()方法int c = 3;System.out.print(a==b);//false,new出来的对象在堆内存中会开辟新的空间System.out.print(a==c);//true,a会自动拆箱为int,然后跟c进行比较
自动装箱:Integer型的对象直接赋值一个int数值时,会自动调用Integer的valueOf()方法,将int转换为Integer;
自动拆箱:当一个Integer与int进行比较时,Integer类型的对象会自动拆箱为int,然后跟c进行比较。
以下例子:
Integer i3 = 126;Integer i4 = 126;
System.out.println(i3 == i4);//true
----------------------
Integer i6 = 128;
Integer i7 = 128;
System.out.println(i6 == i7);//false
以上两个例子都是自动装箱,因此都会自动调用Integer的valueOf()方法,但是两个结果却是不一样的。
翻看valueOf的源码:
static final int low = -128;static final int high = 127;public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high)//先进行范围比较 //如果在[-128,127]之间,就从缓存中返回 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);//否则就直接new}
可以看到首先会对i进行范围筛选,如果是在[-128,127],就会将常量池中的对象返回,否则就会new新的对象。
当取值在这个范围内时就是true,否则就是false
方法的重写和重载的区别
重载发生在一个类中,同名的方法如果有不同的参数列表则视为重载。与返回值类型无关
以下不构成重载public double add(int a,int b)public int add(int a,int b)
重写发发生在子类和父类之间,重写要求子类的重写方法与父类的方法有相同的方法名、参数列表。返回值类型也要一致
注意:
- 重载与方法的返回值无关,存在于父类和子类以及同一个类中
- 在父子类中,构造方法不能被重写,被final、static修饰的类不能重写
List和Set的区别
最直观的区别就是
LIST是有序的,可以存储重复元素
SET是无序的,不可以存储重复元素。
补充:
list包含有
- ArrayList:底层是数组,查询,快连续内存空间方便寻址;增删慢,会发送数据迁移。—是线程不安全类
- LinkList:底层是双向链表,查询慢,需要通过指针寻找,增删快,改变其前后节点即可。—是线程不安全类
- vertor:底层是数组,查询快,增删慢—是线程安全类
set包含有:
- HashSet:底层使用Hash排序,先比对Hash值,如果Hash值一样再比对内容是否一致
- TreeSet:会对Set的内容进行排序
set因为使用HashMap作为底层,因此是线程不安全的。
**TreeSet是指排序,set的无序是指存入取出的顺序不一样 **
谈谈ArrayList和LinkedList的区别
学过数据结构的话应该很容易明白,其实就是数组和链表之间的区别。
ArrayList底层是数组,会在内存中开辟一段连续空间,增删慢,查找快——不够严谨的表述
LinkedList底层是双向链表,在内存中的空间是不连续的,特点是增删快,查找慢——不够严谨的表述
严谨表述:
数组的增删慢也有特殊情况,如果在数组的头部和尾部进行增改删操作的话这个速度也是不慢的。
LinkedList使用了双向链表,在头部和尾部都有指针,因此当访问头部和尾部时,速度也是不慢的。
ArrayList和LinkList查询和增删速度的说明
当进行查找时,ArrayList底层使用数组,在内部开辟了连续的内存空间,因此当需要按位序查找时,速度比LinkList快是没的说的,但是当需求发送变化,如果是按值查找,此时二者都需要进行遍历查找,二者的速度就显得半斤八两。
当进行增删操作时,如果发生的位置在集合的中间部分,由于ArrayList需要对数据进行迁移,而LinkList在内存当中只需要更改指针位置,此时LinkList在增删操作时是有很大优势的。但如果是对末尾进行增删操作,ArrayList不需要做数据迁移(未超出数组长度),LinkList为双向链表,此时二者的速度也是半斤八两。
ArrayList的扩容机制
ArrayList初始的数组长度为10,当对集合的操作达到预警值时,就会将创建新数组,通过位运算,将数组的长度扩大为原数组的1.5倍,之后将原数组的数据迁移到新数组当中。
如何在双向链表A和B之间插入C
一个数据结构的题目。—> 双链表A、B节点之间插入一个C节点
双链表:一个节点由三个部分组成,分别为前置指针、数据、后置指针。
前后指针分别指向前一个节点、后一个节点的地址值。
伪代码:
c.next = a.next;a.next.pre = c;a.next = c;c.pre = a;
Object类中的方法介绍
public final native Class<?> getClass();
getClass使用了native 方法,内部使用了C/C++实现,用于返回当前运行时对象的 Class 对象
public native int hashCode();
hashCode用于返回对象的哈希码,重写了Equals方法就需要重写hashCode方法
public boolean equals(Object obj) {
return (this == obj);
}
equals用于比较 2 个对象的内存地址是否相等,在需要比较内容是否相同时需要重写该方法
protected native Object clone() throws CloneNotSupportedException;
tclone方法hrows CloneNotSupportedException//naitive 方法,用于创建并返回当前对象的一份
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
toString返回类的名字@实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public final native void notify()
notify()使用了native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()
notifyAll()使用了native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException
native 方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 。timeout 是等待时间。
protected void finalize() throws Throwable { }
finalize方法表示实例被垃圾回收器回收的时候触发的操作
equals()与 hashcode() 的区别
hashCode介绍:
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址转换为整数之后返回。
为什么重写 equals 时必须重写 hashCode 方法?
如果两个对像是相等的,它的哈希吗一定也是相同的,此时调用equals方法也能返回true,但是哈希码相同的对象它们不一定会相等;
当我们重写equals时,判断对象是否相等的逻辑有原来的==
变为了判断属性是否一致,因此为了对象的一致性,就必须重写hashCode方法
重写equals时,是为了用自身的方式去判断两个自定义对象是否相等,然而如果此时刚好需要我们用自定义的对象去充当hashmap的健值使用时,就会出现我们认为的同一对象,却因为hash值不同而导致hashmap中存了两个对象,从而才需要进行hashcode方法的覆盖。
什么情况下可以不重写 hashCode ?
当所在类不使用 HashSet、Hashtable、HashMap 等散列集合进行存储的时候,可以不使用 hashcode。
除了NEW还有什么创建类的方式
创建类的方式:
- 使用new关键字来创建类
- 使用反射来创建类
- 使用反序列化来创建类
使用反射来创建类
public void test(){
String str = "com.smoyu.blog.Test";
Class class = Class.forName(str);
Test t = (Class)class.newInstance();
}
Exception 和 Error 的区别
Error 类和 Exception 类的父类都是父类都是 throwable 类
Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防**,遇到这样的错误,建议让程序终止。
Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
反射原理及使用场景
Java 反射机制是在运行状态中,对于任意一个类可以知道它的所有属性和方法;对于任意一个对象,可以调用它的所有属性和方法;这种动态获取信息以及动态调用对象的方法的功能称为 Java 的反射机制。
使用反射机制的前提条件是获得代表的字节码的 Class 文件(两种方式):
1.知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象
2.通过 Class.forName()传入类的路径获取:
Class alunbarClass1 = Class.forName(“cn.javaguide.TargetObject”);
**优缺点:优点:**运行期类型的判断,动态加载类,提高代码灵活度。
缺点:
- 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
- 安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。
应用场景:
- JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;
- Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
- 动态配置实例的属性;
Java会发生内存泄漏问题吗?有哪些场景会发生内存泄漏
举例部分常用场景:
- 使用单例模式也有可能造成内存泄漏,这是因为单例模式 static 的对象是存放在方法区中的。不会被回收。
- 没有释放的资源连接,如数据库连接、网络连接等。这种资源的连接只有代码中显示的调用 close()才能够关闭资源,如果没有调用 close()方法,资源连接是不会被 GC 垃圾回收器回收的。
ArrayList的扩容机制
总结:ArrayList底层源码是基于数组来实现的,并且默认初始的容量为10,扩容时使用位运算的形式,每次以原size的1.5倍创建新数组,通过调用 elementData = Arrays.copyOf(elementData, newCapacity); 将原数据移动到新数组中
源码分析:
transient Object[] elementData; // 定义一个数组
private int size;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;//初始容量为10
get(int index)方法:
public E get(int index) {
rangeCheck(index); //检查数组是否越界
return elementData(index);//直接返回数组的下标
}
当调用add(E e) 方法时,会判断是否扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 判断是否需要扩容
elementData[size++] = e; //执行添加操作
return true;
}
此时,add方法中调用了ensureCapacityInternal(size + 1); ,继续翻阅该方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); //此处嵌套了两个方法
}
此时又继续调用方法ensureExplicitCapacity方法,且内部执行了calculateCapacity用于扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0) //判断是否需要执行扩容,如果该长度大于数组的长度,就执行扩容,且每次扩容按1.5进行
grow(minCapacity);
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
最终调用了grow方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
此处第三四行代码:
- int oldCapacity = elementData.length;
- int newCapacity = oldCapacity + (oldCapacity >> 1); //
oldCapacity >> 1)执行了右移操作,相当于 oldCapacity / 2
这里就是扩容大小确定的地方,相当于新的最大容量是 旧的数组长度+旧数组的0.5长度
ConcurrentHashMap 的 put 方法过程
- 根据 key 计算出 hashcode 。
- 判断数组桶是否为空,为空则初始化数组桶。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
- 如果都不满足,则利用 synchronized 锁写入数据。判断是否为链表,为链表则遍历链表加入新数据,如果相等则
- 覆盖节点。判断为红黑树,则进行一个红黑树的插入。
- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
HashMap的扩容机制
1、首先判断 OldCap 有没有超过最大值。
2、当 hashmap 中的元素个数超过数组大小*loadFactor 时,就会进行数组扩容,loadFactor 的默认值为 0.75,也就是说,默认情况下,数组大小为 16,那么当 hashmap中元素个数超过 160.75=12的时候,就把数组的大小扩展为 216=32,即扩大一倍。
3、然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 hashmap 中元素的个数,那么预设元素的个数能够有效的提高 hashmap 的性能。比如说,我们有 1000 个元素 new HashMap(1000), 70但是理论上来讲 new HashMap(1024)更合适,不过上面已经说过,即使是 1000,hashmap 也自动会将其设置为 1024。但是 new HashMap(1024)还不是更合适的,因为 0.751000 < 1000, 也就是说为了让 0.75 size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了 resize 的问题。
为什么HashMap长度都是2的次幂
原因在于当长度是2次幂时满足以下公式:
hash%length==hash&(length-1)
使用& 运算的速度比直接取余更快。
HashMap为什么使用红黑树而不使用别的树?
因为红黑树不追求完美的平衡,只要求达到部分平衡,可以减少增删结点时的左旋转和右旋转的次数
为什么链表长度大于阈值 8 时才将链表转为红黑树?
因为树结点占用的存储空间是普通结点的两倍。因此红黑树比链表更加耗费空间。结点较少的时候,时间复杂度上链表不会比红黑树高太多,但是能大大减少空间。
当链表元素个数大于等于 8 时,链表换成树结构;若桶中链表元素个数小于等于 6 时,树结构还原成链表。因为红黑树的平均查找长度是 log(n),长度为 8 的时候,平均查找长度为 3,如果继续使用链表,查找长度为 8/2=4,这才有转换为树的必要。
CopyOnWriteArrayList
它的实现就是写时复制,在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是 get 等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,同时写入的时候怎么办呢?当然果断还是加锁。
适用场景:copyonwrite 的机制虽然是线程安全的,但是在 add 操作的时候不停的拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作,而且在迭代的时候数据也是不及时的,数据量少还好说,数据太多的时候,实时性可能就差距很大了。在多读取,少添加的时候,他的效果还是不错的(数据量大无所谓,只要你不添加,他都是好用的)。
CopyOnWriteArrayList 的 get 方法:get 的方法就是普通集合的 get 没有什么特殊的地方,但是成员变量的声明还是有讲究的,是个用 volatile 声明的数组,这样就保证了读取的那一刻读取的是最新的数据。
CopyOnWriteArrayList 的 add 方法: add 方法了,可以明显看出是明显需要 reentrantlock 加锁的,接下来就是复制数据和添加数据的过程,在 setArray 的过程中,把新的数组赋值给成员变量 array(这里是引用的指向,java 保证赋值的过程是一个原子操作)。
Servlet和Filter的区别
作用不同
Servlet 是一个运行在 web 服务端的 java 程序, 用于接收和响应请求。我们通常写的各种 Java Web 应用本质上就是一个 Servlet 程序。
Tomcat 是一个 Servlet 容器,用于部署 Serlvet 程序。
Filter 是一个运行在 web 服务端的 java 程序, 用于拦截请求和拦截响应
方法不同
Servlet 只能接收请求和处理响应
Filter 可以接收请求和处理响应, 还可以拦截请求
生命周期不同
Servlet:第一次请求访问的时候, 创建对象
Filter:web 应用加载的时候, 创建对象
访问的优先级
Filter 访问优先于 Servlet
悲观锁和乐观锁
CAS
Java对象头存放什么内容
Volatile 关键字以及底层实现
线程池,线程池的作用
AQS
谈谈HashSet的存储原理
HashSet与HashMap非常类似,最明显的区别就是HashSet是属于单列集合,HashMap为双列集合。
翻看HashSet源码,可以发现HashSet底层使用了HashMap
public HashSet() {
map = new HashMap<>();
}
HashSet在调用add(E e)方法时,会将传入的对象作为key存入到HashMap中,统一以PRESENT作为假的value。
public boolean add(E e) { return map.put(e, PRESENT)==null; }
所以HashSet是以哈希表存储的,目的是为了解决唯一性的问题。
哈希表原理:
在JKD1.7以前,哈希表使用数组+链表的结构完成的。
在JDK1.8后,哈希表使用数组+链表的结构,但是当链表的长度>8时就会改用红黑树。
哈希存储确保唯一性过程:
主要用到两个方法,第一个就是Object的hashCode()方法,第二个就是使用了equals()方法进行内容上的比对。
首先第一步会对需要存入的对象进行哈希运算,并且与数组的长度-1进行位运算,从而得到该对象在数组存储的位置。
当发生哈希冲突时,此时就会进行进一步的比较,通过调用equals方法来比较内容,如果是相同的的内容则不会继续存储,如果是不同的的内容,此时就会以链表的形式继续存储。
JDK1.8时做了优化,当链表的长度大于8时,就会将链表转换为红黑树。
谈谈LinkedHashMap和HashMap的区别(重点)
共同点:二者都是属于Map
HashMap使用了哈希表的存储形式:使用数组+链表的结构
HashMap的默认初始化大小为16,最大的装载因子默认为0.75,当HashMap的元素个数达到总容量的0.75时,就会在原先的基础上扩容两倍。
当链表的长度大于8,且数组的容量大于64时就会将链表转化为红黑树。
LinkedHashMap与HashMap的主要不同就在于LinkedHashMap使用了链表+哈希表(散列表)的结构。
谈谈ConcurrentHashMap,HashMap,Hashtable的区别
- Hashtable是线程安全的类,但是效率低
- HashMap是线程不安全的类,效率高
- Hashtable的key|value是不允许NULL,HashMap允许key|value为NUll值
- Hashtable的父类为Dictionary,HashMap的父类为AbstractMap,但是它们都实现了Map接口
ConcurrentHashMap在线程安全和效率上是一个折中的选择,它属于并发包java.util.concurrent下的一个类
ConcurrentHashMap是线程安全的类,它能够兼顾效率的原因是在于它把数据分段,执行分段锁,需要改哪部分就给哪部分上锁,将范围变小,从而提高效率。
ConcurrentHashMap优化:
JDK1.7采用分段锁的方式,而JDK1.8采用CAS和synchronized的组合模式
ArrayList vs Vector vs CopyOnWriteArrayList
ArrayList 和Vector的区别:
ArrayList :是线程不安全的类
Vector:和ArrayList 基本类似,但是在每个方法都使用了synchronized,因此是线程安全的。
在List集合中除了Vector是线程安全的,此外还有java.util.concurrent 包下的CopyOnWriteArrayList
CopyOnWriteArrayList是线程安全的类,与ArrayList 基本类似。其中所有可变操作( add
, set
,等等)通过对底层数组的最新副本实现。
-
也就是说CopyOnWriteArrayList是读写分离的机制,在读的时候不加锁。
-
每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回
去。
开发一个自己的栈,你会怎么写?
-
栈(stack)在数据结构中是属于顺序表的范畴,主要特点为先进后出(First in last Out)
-
队列(Queue)在数据结构中是属于顺序表的范畴,主要特点为先进先出(First in First Out)
栈的底层还是采用数组的形式,但是存和取要实现先进后出的特点
栈主要有两个方法,一个是push(E item)方法,另一个是E peek(),一个存,一个取。
在Java中哪些情况使用了栈?
如JVM当中就使用了栈
补充:集合知识小结
Java集合中主要有两大体系,分别为Collection和Map
主要区别在于Collection是单列集合,Map是双列集合。
Collection主要有List和Set两大类:
- List下的集合主要有ArrayList(数组,线程不安全)、LinkList(链表,线程不安全)、Vector(数组,线程安全)、CopyOnWriteArrayList(数组,线程安全)
- Set主要有HashSet(底层哈希)和LinkedHashSet(底层哈希+链表),LinkedHashSet使用链表可以实现排序(能排序,但不是有序。)
- List是有序的,Set是无序的
Map是双列集合:
- 主要分为HashMap(底层哈希,线程不安全)、HashTable(哈希,线程安全)
- ConcurrentHashMap是一个效率和安全折中的选择,既保证了线程安全,也兼顾了效率。它能够兼顾效率的原因是在于它把数据分段,执行分段锁,需要改哪部分就给哪部分上锁,将范围变小,从而提高效率
- 哈希在JDK1.8之后的优化在于当链表的长度大于8时,会将数组+链表的结构转化为数组+红黑树的结构。
- LinkedHashMap是Map的子类,使用了链表+哈希的数据结构,能够记录插入的顺序。
- HashMap的key|value可以为空,HashTable的key|value都不能为空。
- HashMap的父类是AbstractMap,HashTable的父类是Dictionary,但是它们都实现了Map接口。
谈谈IO流的分类及选择
I指的是In,O指的是Out,即输入输出的意思。
此处输入输出,是站在程序的角度来理解的:
- 从程序中将文件信息写入到本地,就是输出-Out
- 将文件从本地读到程序中,就是输入-In
主要涵盖四个流:InputString、OutPutString、Reader、Writer
InputString和OutPutString为字节流,可以输入输出任何格式的文件。
Reader和Writer为字符流,可以输入输出字符内容的文本。
serialVersionUID的作用是什么
当执行序列化时,我们写对象到磁盘中,会根据当前这个类的结构生成一个版本号ID
当反序列化时,程序会比较磁盘中的序列化版本号ID跟当前的类结构生成的版本号ID是否一致,如果一致则反序列化成功,否则,反序列化失败
加上版本号,有助于当我们的类结构发生了变化,依然可以之前已经序列化的对象反序列化成功
请描述下Java的异常体系
Java中的异常体系都继承了Throwable,因此常见的Error和Exception都是它的子类。
按照异常类型可以划分:
- Error:程序中不可处理的重大错误,常见的有堆内存溢出( OutOfMemoryError,在内存中创建的对象过多 )、栈内存溢出(StackOverFlowError,如递归次数过多)
- Exception,也称为异常,指程序可以捕获处理的的异常。
Exception下又含有运行时异常(RuntimeException)、编译异常(IoException)
RuntimeException:也称为逻辑异常,常见的有NullPointerException、ArrayindexOutBoundsException等。
IoException:在编译时就会提示的异常,当出现该异常时就要对该异常进行处理,否则就会编译不通过。
根据可查(checked Exception)和不可查异常(unchecked Exception)分类:
-
checked Exception:除了RuntimeException外的所有Exception都属于可查异常,当程序编译时发生该异常时,就必须处理,否则编译不通过。
-
unchecked Exception: RuntimeException和Error属于不可查异常,没有try、catch编译时仍然可以通过
罗列常见的5个运行时异常和非运行时异常
运行时异常:
NullPointerException
ArrayIndexOutBoundsException
ArithmeticException (算术异常,如1/0)
IllegaArguementException (参数异常)
NumberFormatException(数字格式异常)
非运行时异常:
IOException 表示发生某种类型的I / O异常。 此类是由失败或中断的I / O操作产生的一般异常类。
SQLException 提供有关数据库访问错误或其他错误的信息的异常
FileNotFoundException 指示尝试打开由指定路径名表示的文件失败。
NoSuchFileException 当尝试访问不存在的文件时抛出检查的异常。
NoSuchMethodException 当无法找到特定方法时抛出
throw跟throws的区别
throw作用在代码块内,throws作用在方法上。
---------throws--------------
public void test01() throws Exception{
//代码块
}
----------throw---------------
public void test02(){
try{
//代码块
}catch(Exception){
//代码块
throw new RuntimeException("发生了异常"); }
}
一道关于try catch finally返回值的问题
public int demo() {
try {
int a = 5/0;
return 1;
}catch (Exception e){
return 2;
}finally {
return 3;
}
}
结果为3,finally的优先级大于catch
创建线程的方式
创建线程有四种方式:
继承Thread
实现Runable接口
实现Callable接口(可以获取线程执行之后的返回值)
从线程池中获取
方式一:继承Thread类
public class Demo01 {
/**
* 继承Thread的方式
*/
@Test
public void test01(){
Demo demo = new Demo();
demo.start();
}
}
//定义一个类,继承Thread,重写run方法
class Demo extends Thread{
@Override
public void run() {
System.out.println("线程创建了:"+Thread.currentThread().getName());
}
}
方式二:实现Runable接口
//使用lambda的形式 /** * 实现 Runable 接口 */ @Test public void test02(){ new Thread(() -> System.out.println("线程创建了:"+Thread.currentThread().getName())).start(); }-----------------//使用匿名内部类 @Testpublic void test03(){ new Thread(new Runnable() { @Override public void run() { System.out.println("线程创建了:"+Thread.currentThread().getName()); } }).start();}
方式三:实现Callable接口(可以获取线程执行之后的返回值)
//继承Callable接口
class Demo2 implements Callable<String>{
@Override public String call() throws Exception {
System.out.println("多线程方法执行了");
return "9999";
}}
---------调用测试方法-----------
@Test
public void test04(){
FutureTask futureTask = new FutureTask(new Demo2());
new Thread(futureTask).start();
}
run方法和start()的区别:
- run方法是Runable接口中的一个抽象方法,start是Thread中的方法
- run是线程需要执行的代码,start是将Thread线程启动起来,会开辟新线程。
- 通过调用start方法,会开辟新线程去执行run方法;直接调用run方法是不会开启新的线程的。
面试提问:选择Thread还是Runable?二者什么区别
- 适合多个相同程序代码的线程去处理同一个资源(多线程内的数据共享)
- 避免java特性中的单根继承限制
- 增加程序健壮性,数据被共享时,仍然可以保持代码和数据的分离和独立
- 更能体现java面向对象的设计特点
一个普通main方法的执行,是单线程模式还是多线程模式?为什么
多线程模式。
除了Main方法的线程外,还有垃圾回收线程GC。
请描述线程的生命周期
线程的生命周期:
- 新建状态 (New): 线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
- 就绪状态(Runnable):当线程对象调用了start方法时,就处于就绪状态
- 运行状态 (Running): 线程处于就绪状态,且得到CPU时间片时,CPU就会执行该线程任务,此时就为运行状态
- 阻塞状态(Blocked):运行状态的线程,调用sleep、wait方法,或未得到synchronized锁时,就会进入阻塞状态
- 终止状态 (Terminated): 当线程任务执行完时,就进入了终止状态。
调用wait方法,需要使用notify()或notifyAll()来唤醒线程
notify:会随机唤醒一个线程
notifyAll:唤醒所有的线程
谈谈你对ThreadLocal的理解
ThreadLocal这个类提供线程局部变量。 这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get
或set
方法)都有自己独立初始化的变量副本。 ThreadLocal
实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。
ThreadLocal能够为每个线程创建一个独立的副本,从而避免了多线程环境下操作共享资源的安全问题
public class ThreadLocalTest {
//定义一个成员变量
static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/** * 定义一个方法,开辟两个线程,打印其值观察是否是同一个线程 * 如果打印的线程是不同的值,就表面threadLocal会为每个线程创建独立副本 **/
public static void main(String[] args) throws Exception {
Task task = new Task();
new Thread(task).start();
Thread.sleep(10);//休眠一下,防止太快看不出效果
new Thread(task).start();
}
static class Task implements Runnable{
@Override
public void run() {
//获取一下当前的threadLocal
Long local = threadLocal.get();
if(local == null){
//如果local为Null就手动赋值
threadLocal.set(System.currentTimeMillis());
//打印查看结果
System.out.println("当前的local:"+threadLocal.get());
}
}
}}
此时可以发现打印出来的结果是不一样的。
ThreadLocal最主要的两个方法get
和set
,通过分析:
可以看到,无论是get还是set,都有最核心的两句话
//获取当前线程 Thread t = Thread.currentThread(); //从ThreadMap中获取当前线程的map ThreadLocalMap map = getMap(t);
也就是说,无论是存还是取使用ThreadLocal操作的都是当前线程的对象,这也就说明了ThreadLocal使同一变量在每一个线程中有各自的副本,只有指定的线程能拿到特点的数据。
案例说明:
Java程序对数据库的连接操作都是基于JDBC来完成的,在开发中根据不同的功能分为了web层、service层,dao层。
其中对于事务的控制是放在service层中解决的。
public class UserService {
private UserDao userDao = new UserDao();
private LogDao logDao = new LogDao();
public void add(){
userDao.add();
logDao.add();
}
}
如此处service调用了两个dao,那么两个dao是如何实现事务的控制的?
在JDBC中,如要实现事务,就必须要保证两个dao是有相同的Connection连接对象,因此只需要保证Connection是相同的即可。
因为整个UserService都是同一个线程去调用userDao
和logDao
,因此就可以使用ThreadLocal来解决这个问题
/** * 使用 ThreadLocal 来存储Connection对象
* 因此service是使用同一个线程来调用dao的
* 因此可以使用ThreadLocal访问当前线程的特有数据
*/
public class ConnectionUtils { p
rivate ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
public static Connection getConnection(){
Connection connection = threadLocal.get();
if(connection == null){
connection = new Connection();
threadLocal.set(connection); } return connection;
}}
通过ThreadLocal
会为每个线程创造一个副本的特点, 因此可以将Connection存储到ThreadLocal对象中,只要是同一个线程访问的,就可以拿到相同的Connection对象。
谈谈AJax的工作原理
Ajax 的全称是Asynchronous JavaScript and XML(异步的JavaScript 和 XML),ajax中最后的x指的是xml,早起都是使用xml作为数据交互,现在json更为轻量,基本都是使用json数据。
在前后端不分离的时代,没有引入Ajax时,客户端发送的请求流程是如下这样的:
客户端发起请求,服务器将整个HTML+CSS数据都响应给客户端
引入Ajax的工作模式是这样的:
相比较传统模式,引入了一个AJax引擎,通过使用XMLHttpRequest发送异步请求到服务器,服务器将数据响应到AJAX引擎,利用回调函数进行数据的渲染,响应HTML+CSS数据,从而实现异步刷新。
谈谈Servlet的生命周期
Servlet的生命周期
Servlet创建 -> 初始化 -> 调用service方法 ->调用doGet/doPost方法 -> 销毁
Servlet是一种单例类型的,从创建到销毁只会被执行一次
创建 - > 执行一次
初始化 ->执行一次
销毁 -> 执行一次
其中,Servlet默认是在第一次访问时才被创建,也可以通过配置的形式让servlet在容器启动时就创建。
Servlet不是线程安全的类,在并发条件下会有线程不安全的情况,因此因避免操作共享数据。
描述Session跟Cookie的区别(重要)
- Session存储在服务器中,Cookie存储在浏览器(客户端)中
- Session存储的对象为键值对形式,Cookie储存的数据格式为字符串(可转为JSON)
- Session生命默认为30分钟, Cookie 默认随着浏览器的关闭而关闭,也可以通过 设置有效期来控制Cookie的生命。
理解:
在日常过程中,小明问小军:“你喜欢吃什么” ,小军回答:“我喜欢吃四饭烧鸭饭”,那么这个过程
其实就是一问一答的过程,或者换句话说,就是一次会话(Session)
当客户端发起请求,浏览器就会响应结果,那么我们可以说是一次请求,一次响应,整个过程就是
一次通信会话。
由于HTTP通信协议是无状态的,服务器无法直接鉴别当前客户信息,我们就需要一种技术,让服
务器能够记住当前用户。因为我们不可能登录A页面后,需要访问B页面时又重新登录,访问C页面的时
候又重新登录。
我们需要的效果是,当我登录了当前的网站,随后用户的相关信息都能被记录,服务器能够识别当
前用户信息,登录了A页面后,无论访问B页面还是C页面,服务器都能记住我,那么就需要这种会话技
术Session了。
所以,当我关闭浏览器时,服务器就会认为你跟它的会话已经结束,那么存储在Session的用户信
息也会随之而删除,这也是为什么关闭浏览器下次就需要重新登录的原因了。
Cookie在百科是的为:“储存在用户本地终端上的数据”,也就是说,Cookie是能信息保存到当前
的客户端本地(当然也包括用户信息了),而Session存储的用户信息保存到服务器上,这是二者的不同。
当保存到本地时,即便关闭浏览器,当我下次打开网站,浏览器就能在请求服务器的同时将Cookie
一起发送给服务器,从而让服务器能够正常鉴别用户信息,所以有时候也可以做长时间的登录这个功
能。
由于Cookie是存储在客户端本地的,所以就会有被攻击和篡改的可能,因此这也让Cookie存储用
户信息变得不安全。比如有可能会遭到CSRF的攻击,这里没有详细的解释
补 Get请求与Post请求的区别
经常会听到,你的这个请求到底是get的请求呢还是post请求呢?
从简单的来讲二者区别:
- post的请求比get请求更安全,因为get的请求直接把参数暴露到URL上。就比如做登录用post而不
是get,get会使你的密码暴露出去,显然更加不安全。 - get的请求参数往往在url中,而post的参数是放在请求体当中的。如果不了解请求头、请求体这些
知识,可以去了解一下HTTP协议。 - GET在浏览器回退时是无害的,而POST会再次提交请求。
主要是这些,百度上还有更详细的,就看你的需求了
转发和重定向的区别
转发:是指发生在服务器内部的跳转,此时只有一次请求
重定向:指发生客户端之间的跳转,有两次请求。
区别:
- 转发请求一次,重定向请求多次
- 转发发生在服务器内部之间,重定向发生在客户端
重定向实例:
如登录时,需要发起一个 POST请求,登录成功时就会返回状态码302,然后重定向到首页(用户中心)
Iterator和ListIterator的区别?
Iterator [ɪtə’reɪtə] :可以遍历Set和List,而ListIterator只能遍历List
Iterator只能单向遍历,而ListIterator可以双向遍历
ListIterator继承与Iterator接口
并发和并行的区别
并发和并行是两个不同的概念
- 并发指的是某一个处理器同时处理多个请求,如同一个CPU处理多个任务时,通过时间片的方式快速的切换完成多个任务。
- 并行是指多个处理器去同时处理多个请求,此时好比有多个CPU去同时处理多个任务,使用多核处理器,处理多任务的能力自然而然强。
什么是序列化?
序列化是为了保持对象在内存中的状态,并且可以把保存的对象状态再读出来。
什么时候需要用到java序列化?
1,需要将内存的对象状态保存到文件中
2,需要通过socket通信进行对象传输时
3,我们将系统拆分成多个服务之后,服务之间传输对象,需要序列化
说说synchronized底层原理
这个我们要分情况来分析:
1,JDK1.6之前
synchronized是由一对monitor-enter和monitor-exit指令实现的。
这对指令的实现是依靠操作系统内部的互斥锁来实现的,期间会涉及到用户态到内存态的切换,所以这个操作是一个重量级的操作,性能较低。
2,JDK1.6之后
JVM对synchronized进行了优化,改了三个经历的过程
偏向锁-》轻量级锁-》重量级锁
偏向锁:
在锁对象保存一个thread-id字段,刚开始初始化为空,当第一次线程访问时,则将thread-id设置为当前线程id,此时,我们称为持有偏向锁。
当再次进入时,就会判断当前线程id与thread-id是否一致
如果一致,则直接使用此对象
如果不一致,则升级为轻量级锁,通过自旋锁循环一定次数来获取锁
如果执行了一定次数之后,还是没能获取锁,则会升级为重量级锁。
锁升级是为了降低性能的消耗。s
synchronized和volatile的区别
1,作用的位置不同
synchronized是修饰方法,代码块
volatile**[ˈvɒlətaɪl]** 是修饰变量
2,作用不同
synchronized,可以保证变量修改的可见性及原子性,可能会造成线程的阻塞
volatile仅能实现变量修改的可见性,但无法保证原子性,不会造成线程的阻塞
volatile的实现:
volatile是一个轻量级的线程同步机制。它的特性之一,是保证了变量在线程之间的可见性。
所谓的可见性是指当一个线程修改了变量的值之后,其他线程可以感知到该变化。
为什么会有可见性问题?
是因为由于硬件速度的不同,CPU的速度要明显快于主内存。
所以为了解决速度不匹配的问题,在CPU到主内存之间就会有多级的缓存。
那么这个时候就会发生,一个线程修改了数据,数据还没有及时刷到主内存,那么其他线程读取到的数据就依然还是旧的,这就是可见性问题发生的根源。
通过给变量设置volatile关键字修饰,可以保证变量在线程修改完之后,会刷新到共享内存,这样其他线程就可以读取到最新的内容
volatile保证了在多个线程之间是可见的,但不能保证原子性操作。
volatile vs synchronized
synchronized也是保证了线程的可见性,同时也具备了多线程之间的互斥性
3,如何使用volatile ?
直接修饰变量即可
private volatile int count;
4,volatile底层实现原理
当变量被声明为volatile后,线程每次都会都从主内存去读取,而不是读取自己的工作内存,这样就实现了线程之间的可见性
synchronized和lock的区别
1,作用的位置不同
synchronized可以给方法,代码块加锁
lock只能给代码块加锁
2,锁的获取锁和释放机制不同
synchronized无需手动获取锁和释放锁,发生异常会自动解锁,不会出现死锁。
lock需要自己加锁和释放锁,如lock()和unlock(),如果忘记使用unlock(),则会出现死锁,
所以,一般我们会在finally里面使用unlock().
补充:
//明确采用人工的方式来上锁
lock.lock();
//明确采用手工的方式来释放锁
lock.unlock();
synchronized修饰成员方法时,默认的锁对象,就是当前对象
synchronized修饰静态方法时,默认的锁对象,当前类的class对象,比如User.class
synchronized修饰代码块时,可以自己来设置锁对象,比如
synchronized(this){
//线程进入,就自动获取到锁
//线程执行结束,自动释放锁
}
什么是死锁?如何防止死锁?
如何防止死锁?
减少同步代码块嵌套操作
降低锁的使用粒度,不要几个功能共用一把锁
尽量采用tryLock(timeout)的方法,可以设置超时时间,这样超时之后,就可以主动退出,防止死锁(关键)
什么是反射?可以解决什么问题?
反射是指程序在运行状态中,
1,可以对任意一个类,都能够获取到这个类的所有属性和方法。
2,对于任意一个对象,都可以调用它的任意一个方法和属性
反射是一种能力
一种在程序运行时,动态获取当前类对象的所有属性和方法的能力,可以动态执行方法,给属性赋值等操作的能力
Class代表的就是所有的字节码对象的抽象,类
反射,让我们的java程序具备动态性
这种动态获取类信息及调用对象方法的功能称为反射
这种能力带来很多的好处,在我们的许多框架的背后实现上,都采用了反射的机制来实现动态效果。
框架是提供一种编程的约定
比如@Autowrie 就能实现自动注入
@Autowrie
private IUserService userService;
注解的解析程序,来扫描当前的包下面有哪些属性加了这个注解,一旦有这个注解,就要去容器里面获取对应的类型的实现,然后给这个属性赋值。
什么是悲观锁,什么是乐观锁?
1,悲观锁是利用数据库本身的锁机制来实现,会锁记录。
实现的方式为:select * from t_table where id = 1 for update
2,乐观锁是一种不锁记录的实现方式,采用CAS模式,采用version字段来作为判断依据。
每次对数据的更新操作,都会对version+1,这样提交更新操作时,如果version的值已被更改,则更新失败。
3,乐观锁的实现为什么要选择version字段,如果选择其他字段,比如业务字段store(库存),那么可能会出现所谓的ABA问题
数据库说明
数据库的完整知识可以参考我的这篇文章,这是我总结的一个比较完整的知识点全集,字数在1W+
https://blog.csdn.net/wq2323/article/details/111768680
谈谈数据库设计的三大范式及反范式
数据库的三大范式
第一范式:列不可分
第二范式:要有主键
第三范式:不可存在传递依赖
比如商品表里面关联商品类别表,那么只需要一个关联字段product_type_id即可,其他字段信息可以通过表关联查询即可得到
如果商品表还存在一个商品类别名称字段,如product_type_name,那就属于存在传递依赖的情况,第三范式主要是从空间的角度来考虑,避免产生冗余信息,浪费磁盘空间
反范式设计:(第三范式)
为什么会有反范式设计?
原因一:提高查询效率(读多写少)
比如上述的描述中,显示商品信息时,经常需要伴随商品类别信息的展示,
所以这个时候,为了提高查询效率,可以通过冗余一个商品名称字段,这个可以将原先的表关联查询转换为单表查询
原因二:保存历史快照信息
比如订单表,里面需要包含收货人的各项信息,如姓名,电话,地址等等,这些都属于历史快照,需要冗余保存起来,
不能通过保存用户地址ID去关联查询,因为用户的收货人信息可能会在后期发生变更
左连接,右连接,内连接,如何编写SQL,他们的区别是什么?
左连接:以左表为主
select a.,b. from a left join b on a.b_id = b.id;
右连接:以右表为主
select a.,b. from a right join b on a.b_id = b.id;
内连接:只列出两张表关联查询符合条件的记录
select a.,b. from a inner join b on a.b_id = b.id;
如何解决SQL注入?
SQL注入,是指通过字符串拼接的方式构成了一种特殊的查询语句
比如:select * from t_user where usename=’’ and password=’’
’ or 1=1 #
select * from t_user where usename=’’ or 1=1 # ’ and password=’’
解决方案
采用预处理对象,采用PreparedStatement对象,而不是Statement对象
可以解决SQL注入的问题
另外也可以提高执行效率,因为是预先编译执行
SQL执行过程(语法校验->编译->执行)
延伸
MyBatis如何解决了SQL注入的问题?采用#
MyBatis的#和KaTeX parse error: Expected 'EOF', got '#' at position 5: 的差异,#̲可以解决SQL注入,而号不能解决
JDBC如何实现对事务的控制及事务边界
JDBC对事务的操作是基于Connection来进行控制的,具体代码如下:
try {
//开启事务
connection.setAutoCommit(false);
//做业务操作
//doSomething();
//提交事务
connection.commit();}catch(Exception e){
//回滚事务
try {
connection.rollback(); }
catch (SQLException e1) {
e1.printStackTrace();
}
}
但,注意,事务的边界我们是放在业务层进行控制,因为业务层通常包含多个dao层的操作。
谈谈事务的特点
原子性是基础,隔离性是手段,一致性 是约束条件,而持久性是我们的目的。
简称,ACID
原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )
原子性:
事务是数据库的逻辑工作单位,事务中包含的各操作要么都完成,要么都不完成
(要么一起成功,要么一起失败)
一致性:
事务一致性是指数据库中的数据在事务操作前后都必须满足业务规则约束。
比如A转账给B,那么转账前后,AB的账户总金额应该是一致的。
隔离性:
一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
(设置不同的隔离级别,互相干扰的程度会不同)
持久性:
事务一旦提交,结果便是永久性的。即使发生宕机,仍然可以依靠事务日志完成数据的持久化。
日志包括回滚日志(undo)和重做日志(redo),当我们通过事务修改数据时,首先会将数据库变化的信息记录到重做日志中,然后再对数据库中的数据进行修改。这样即使数据库系统发生奔溃,我们还可以通过重做日志进行数据恢复。
谈谈事务的隔离级别
有以下4个级别:
l READ UNCOMMITTED 读未提交,脏读、不可重复读、幻读有可能发生。
l READ COMMITTED 读已提交,可避免脏读的发生,但不可重复读、幻读有可能发生。
l REPEATABLE READ 可重复读,可避免脏读、不可重复读的发生,但幻读有可能发生。
l SERIALIZABLE 串行化,可避免脏读、不可重复读、幻读的发生,但性能会影响比较大。
特别说明:
幻读,是指在本地事务查询数据时只能看到3条,但是当执行更新时,却会更新4条,所以称为幻读
InnoDB是如何支持事务的 ?
通过日志的形式来确保事务。
redo log 与 undo log 实现的。
介绍一下MVCC
多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
索引如何避免全表扫描(索引失效)
为了避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
列举几个比较常见的:
- 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描, 任何在 where 子句中使用 is null 或 is not null 的语句优化器是不允许使用索引
- 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。使用LIKE操作时要确保不是以%或_开头的,否则索引将失效进行全表扫描。
- 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
- in 和 not in 也要慎用,否则会导致全表扫描,如select id from t where num in(1,2,3)将导致索引失效,如果量不多的话可以考虑使用BETWEEN
- 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超5条索引。
- 如果需要使用like ‘%关键字%’ 去模糊查询,建议使用全文索引来提高效率,否则索引失效导致全文检索。
最左前缀
最左前缀是针对联合索引来说,例如三个字段组成的联合索引,abc、ab、a=ac。使用 abc 会完全使用联合索引的 abc三列,使用 ab 只会使用联合索引中的两列,使用 a 或者 ac 只会使用联合索引中的 a 列。
需要满足两个条件:需要查询的列和组合索引的列顺序一致 查询不要跨列
之所以会有最左原则,是因为联合索引的 B+树是按照第一个关键字进行索引排列的。
查询很慢怎么解决 ?
1、 开启慢查询日志,根据日志找到哪条 SQL 语句执行的慢。要调用 set 将 slow_query_log 参数设置成 1,开启这个慢查询日志。(日志分析工具 mysqldumpslow)
2、 用 Explain 关键字分析这条 SQL 语句,查看是否建立了索引。如果没有,就可以考虑建立索引。如果建立了索引,但是并没有使用到,就分析它为什么没有使用到索引。
计算机网络
说说TCP和UDP的区别
首先,两者都是传输层的协议。其次,
tcp提供可靠的传输协议,传输前需要建立连接,面向字节流,传输慢
udp无法保证传输的可靠性,无需创建连接,以报文的方式传输,效率高
TCP/IP协议基础
层次 | 各层协议 | 端口 |
---|---|---|
TCP 20 | FTP | 文件传输中的数据传输 |
TCP 21 | FTP | 文件传输中的控制命令 |
TCP 22 | SSH | 安全远程登录 |
TCP 23 | Telnet[ˈtelnet] | 远程登录 |
TCP 25 | SMTP | 电子邮件传输 |
TCP 80 | HTTP | WWW服务 |
TCP 110 | POP3 | 邮件接收 |
TCP 139 | NETBIOS SESSION SERVICE | Samba服务 |
TCP/UDP 53 | DNS | 域名解析服务 |
UDP 69 | TFTP | 简单的文件传输 |
计算机网络层次
思维导图:
1、链路层
链路层有时也称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。
2、网络层
网络层处理分组在网络中的活动,例如分组的选路。在TCP/IP协议族中,网络层协议包括ARP(Address Resolution Protocol,地址解析协议)、IP协议(Internet Protocol,网际协议)、ICMP协议(Internet Control Message Protocol,网际控制报文协议)和IGMP协议(Internet Group Management Protocol,网际组管理协议)。RARP(Reverse Address Resolution Protocol,逆地址解析协议)。
3、传输层
传输层主要为两台主机上的应用程序提供端到端的通信。在TCP/IP协议族中,有两个互不相同的传输协议:TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议)。
4、应用层
应用层负责处理特定的应用程序细节。几乎各种不同的TCP/IP实现都会提供下面这些通用的应用程序:Telnet远程登录(23)、SMTP(25)、FTP(21/20)、HTTP(80)、POP3(110)DNS(53)
SSL概述
概述
SSL (Secure Sockets Layer)由Netscape公司1996年开发,其目的是在客户端和服务器端之间建立安全通道,从而提高WEB数据传输的安全性。
目前主流浏览器都支持SSL
SSL的作用
- 保密性 SSL采用的对称加密技术保证信息的机密性。
- 完整性。通信的发送方通过散列函数产生消息验证码(MAC),接收方通过验证MAC来保证信息的完整性。SSL 提供完整性校验服务,使所有经过SSL协议处理的业务都能全部准确、无误地到达目的地。
- 身份认证(鉴别) SSL采用数字证书进行身份认证。
- SSL协议在可靠的传输层协议(如:TCP)之上。SSL协议的优势在于它是与应用层协议独立无关的。
即:保密性、完整性、身份认证,没有不可否认性
HTTP 和 HTTPS 的区别
端口: HTTP 的 URL 由“http”起始且默认的端口是 80,而 HTTPS 的 URL 由“https”起始且默认端口是 443;
安全性和资源消耗:
HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务端都无法验证对方的身份。
HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议, SSL/TLS 是运行在 TCP 之上。 所有传输的内容都经过加密,加密
采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密(采用对称和非对称两种加密方式相结合
的加密方式) 。所以说, HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
对称密钥加密:这种加密方式的加密算法是公开的,密钥是保密的,加密和解密采用同一个密钥,也就是任何人得到了
密钥就能进行加密和解密
弊端:在进行通信时,也必须把密钥发给对方,否则对方无法解密,而在把密钥发给对方的过程中,就存在安全问题23
非对称密钥加密:这种加密方式有两把密钥,一把公钥,可以公开告诉任何人,一把私钥,只能自己持有。
弊端:使用公钥对消息加密速度比较慢
谈谈什么是TCP的三次握手,可不可以只握手两次?
谈谈什么是TCP的四次挥手?
浏览器输入 URL 并回车的过程以及相关协议,DNS 查询过程
JVM虚拟机
JVM内存区域是怎么划分的?
JVM内存主要有三个部分划分:线程私有的、线程共享的、直接内存
程序计数器
程序计数器在内存中分配了一块较小的区域,它可以看作是当前线程执行行数的指示器。
Java多线程的实现通常采用CPU时间片的方式进行快速切换,为了恢复CPU再次执行该线程时的位置,就需要一个能够记住该线程执行行号的计数器。
虚拟机栈(Stack):
虚拟机栈的生命周期和线程的生命周期是相同的,当程序每调用一个方法时,就会产生一个栈帧;虚拟机栈主要存储的信息有:局部变量表、方法出口等,只有当方法调用完成时,压入虚拟机的栈帧才会从栈中退出。
局部变量表:主要存放的是在编译时已知的基本类型变量和引用类型变量的引用地址,在内部存储是以局部变量槽(Slot)来表示的。
本地方法栈(Native Stack)
与虚拟机栈类似,区别在于本地方法栈执行的是被 native修饰的本地方法,而虚拟机栈为普通的Java程序服务。
堆(heap)
Java的堆内存主要存放的是对象实例和数组,是虚拟机内存区域中最大的一块区域,是线程共享的区域,也是垃圾回收器进行垃圾回收的一块重要区域。
根据GC的角度来划分的话,堆内存又划分为新生代和老年代。
新生代分为eden区、from survivor、to survivor区
方法区
JVM中的方法区并不是一个实在的物理概念,方法区它用于存储已经被虚拟机加载的类型信息、常量、静态变量等数据,但是方法区仅仅是一个逻辑上的概念。这就好比接口与实现类的关系一样,方法区仅仅是一个接口,一种规范,而永久代便是它的实现类。
在JDK1.6时代,使用永久代来实现方法区
到了JDK1.8的时候,摒弃了永久代的概念,采用了元空间的概念,即使用计算机的物理内存作为元空间的内存,同时将永久代的字符串常量池移出到堆内存中。
JDK1.6时期的方法区(永久代)
JDK8时期摒弃永久代的概念,使用元空间实现方法区,同时将常量池里的字符串池也挪到了堆中。
常量池与运行时常量池
常量池是一张表,JVM虚拟机根据常量表来找到需要访问的类名、方法名、变量等信息。
当程序运行时,就会把常量池的信息挪到运行时常量池,此时JVM虚拟机访问的类名、方法名、变量等信息的地址就会变成真实的物理地址,因此把该常量池称为运行时常量池。
StringTable
又称字符串常量池,属于常量池的一部分(jdk1.6时期,1.8时期存放在堆中)
有以下特性:
- 在常量池内出现的字符串仅仅只是一个‘符号’,只有当变量引用时才会变成一个字符串对象。
- 字符串拼接时,实际上使用的是StringBuilder对象的append()方法
如以下字符串 a/b,使用+号时实际上调用的是StringBuilder
String a = "a"; String b = "b"; String c = "ab"; String ab = a+b; System.out.println(c == ab);//false
实际上使用的是:
new StringBuilder().append(a).append(b).toString();
但是我们通过源码发现
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
在StringBuilder内部的toString()方法中,使用了new String()来得到字符串对象,这也就导致了c==ab
为false的原因
该原理是通过编译期优化得到的。
intern
new出来的字符串存放在堆中,而直接引用字符串的变量是引用常量池的,因此可以使用intern方法主动将还没有放入字符串常量池的对象放入该常量池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把字符串常量池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入字符串常量池, 并把该对象返回
说说JVM的运行时内存
根据GC的角度来划分的话,堆内存又划分为新生代和老年代。
新生代:
新生代一般存放新生的内存对象,大约占用堆内存的1/3,如果新生的对象内存过大,将会存放在老年代中。在新生代中会触发MinorGC,新生代分为eden区、from survivor、to survivor区
eden区:
中文翻译为伊甸园,即亚当和夏娃的原住地,因此寓意为Java对象的出生地。当新的Java对象过大时,会在老年代产生新对象;当内存不足时,就会触发MinorGC,此时如果内存还无法容纳新对象,就会发生错误。
from survivor:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
to survivo:保留了一次 MinorGC 过程中的幸存者
谈谈垃圾回收算法
确定对象是否能被回收的方法:引用计数法、可达性分析法
引用计数法
在Java中,对象是通过引用进行关联的。当需要操作一个对象时,就对该对象的引用计数+1,当操作完时,就对该对象的计数-1;这种通过引用计数的方式来判断对象是否可以被回收的方法就称为引用计数法。当一个对象计数为0时,则表明该对象可以被垃圾回收。
弊端:
当对象循环引用时会造成对象无法被垃圾回收,从而发送内存泄漏。因此,Java虚拟机没有采用引用计数法。
可达性分析法
以GC Roots对象作为起始点开始向下搜索,如果一些对象没有被GC Roots所引用,那么就说这些对象是不可达的,因此可以判定该对象为可回收对象。
就好比盘子里的葡萄,用手提起葡萄根,连在根上没有掉落的葡萄就是可达的,散落在盘子上的就称为不可达的,因此在盘子上的葡萄就是可以被回收的。
哪些对象可以作为GC Roots对象呢?
- Java虚拟机栈帧内所引用的对象
- 方法区中被static修饰的对象
- 本地方法栈的对象
- 被上锁的对象
垃圾回收算法:标记清除、标记整理、copy算法
标记清除
该垃圾回收算法分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
标记:如上图,先对需要回收的垃圾对象标记为黑色()
清除:随后将该内存清除
弊端:内存碎片化问题严重
标记整理
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
该算法分为两步:标记、整理
如图,先对可回收的对象进行标记
与标记清除算法不同的是,标记整理第二步会将之前的对象往前挪动,使得对象之间的间隔更为紧凑,从而减少内存碎片的问题。
弊端:虽然减少了内存碎片,但是因为需要挪动对象,造成了性能上的损耗。
复制Copy算法
为了解决效率问题,。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
步骤:
- 将内存区域分为两块,分别为from,to
- 对象一开始在from区域,该区域内存使用完发起垃圾回收时,将存活的对象复制到to区域
- 清除from区域的全部垃圾,同时将from和to的内存区域对调
- 原来的to改为from,from改为to
分代回收算法
分代回收算法的特点就是根据对象的生命周期不同,将存放对象的区域划分为新生代和老年代。
在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择**“标记-清除”或“标记-整理”**算法进行垃圾收集
老生代
特点是每次垃圾回收时只有少量对象需要被回收,存放在老年代的对象一般为频繁使用或占用内存较大的对象
新生代
特点是每次垃圾回收时会有大量的对象需要被回收,绝大部分新生对象在新生代。
新生代划分了三个区域,分别是Eden、from survivor、to survivor
新生代使用了Copy算法对内存对象进行回收。
流程:
- 新生对象在伊甸园出生
- 当触发MinorGc时,会将幸存下来的对象放入from,未被回收的对象的年龄+1
- 对调from和to的内存地址,把原来的from变为to,to变为from
四种引用类型
强引用
在Java中,我们最长见到的就是强引用。所谓强引用,就是把一个对象赋给一个引用变量,该对象就称为被强引用的对象。一个强引用对象,它是处于可达的状态,因此该对象是不能被垃圾回收的。
软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态
总结:
强软弱虚,强的不能被回收,软的当系统内存不足时被被回收,弱的不管内存够不够都会被回收,虚的还需要结合引用队列来回收。
引用类型 | 被回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 不会被回收 | 对象的一般状态 | JVM停止运行时 |
软引用 | 内存不足时被回收 | 对象缓存 | 内存不足时 |
弱引用 | 无论内存是否不足都被回收 | 对象缓存 | 垃圾回收后 |
虚引用 | 未知-需要结合引用队列使用 | 未知 | 未知 |
垃圾回收器
吞吐量:
CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
常见的垃圾回收器有:
Serial,翻译为连续的,是最基本的新生代垃圾收集器,单线程,使用了复制算法,当发生垃圾回收的时候会暂停其他线程的活动(stop the world)
ParNew,Serial垃圾收集器的多线程版本,使用了复制算法
Parallel Scavenge**,是一个新生代垃圾收集器,使用了复制算法,是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量。
Serial Old,与Serial一样是单线程的垃圾收集器,作用于老年代,使用标记整理算法
Parallel Old ,是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供,提供一个关注吞吐量的模式。
CMS ,是一种老年代垃圾收集器,其最主要目标是获取最短垃圾
回收停顿时间,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验
G1,基于标记-整理算法,不产生内存碎片,可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域
Full GC 触发条件
- 调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟
机管理内存。 - 老年代空间不足: 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代
等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟
机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold
调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 - 空间分配担保失败: 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
- JDK1.7 以及以前的永久代空间不足:在 JDK 1.7 及以前, HotSpot 虚拟机中的方法区是用永久代实现的,永久代
中存放的为一些 Class 的信息、常量、静态变量等数据。 当系统中要加载的类、反射的类和调用的方法较多时,
永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,
那么虚拟机会抛出 java.lang.OutOfMemoryError。 为避免以上原因引起的 Full GC,可采用的方法为增大永久代空
间或转为使用 CMS GC。 - Concurrent Mode Failure: 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC
过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC
对象创建过程
- 类加载检验: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号
引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载
过程。 - 分配内存:分配方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是
否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 标记-清理不规整,标记-整理以及复制算法是规整
的。在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,
作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项
操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存, JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,
当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配 - 初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间的对象都初始化为零值(不包括对象头),这一步
操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对
应的零值。85 - 设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、 如何才能找
到类的元数据信息、 对象的哈希码、 对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟
机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 - 执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角
来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会
接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来
对象已经死亡
- 引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减
1;任何时候计数器为 0 的对象就是不可能再被使用的。 很难解决对象之间相互循环引用的问题。 - 可达性分析法: 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜
索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用
的。
两次标记过程:即第一次标记可达性分析法中不可达的对象。第二次的话就要先判断该对象有没有实现 finalize()方
法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优
先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。
类的加载过程
类的加载过程分别为:加载 -> 连接 -> 初始化 -> 使用 -> 卸载
在连接过程中又分为:验证 -> 准备 ->解析
加载:主要完成下面三件事情:通过全类名获得定义此类的二进制字节流,在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
验证:文件格式验证、元数据验证、字节码验证、符号引用验证
准备: 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。 进行内存分配
的对象仅包括类变量(static),不包括实例变量;设置的初始值“通常情况”下是数据类型默认的零值,但是加上 final 之
后,就会在这个阶段赋值具体值。
解析: 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 符号引用就是一组符号来描述目标,可以是
任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化:必须进行初始化的五种情况:
-
当遇到 new 、 getstatic、 putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静
态字段(未被 final 修饰)、或调用一个类的静态方法时。 -
使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("…"),newInstance()
-
初始化一个类, 如果其父类还未初始化,则先触发该父类的初始化。
-
当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类), 虚拟机会先初始化这个类。
-
MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用
findStaticVarHandle 来初始化要调用的类
谈谈类加载过程的双亲委托机制?
类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自
java.lang.ClassLoader:
BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib 目录下的 jar 包
和类或者或被 -Xbootclasspath 参数指定的路径中的所有类。 加载一些通用的类: Object 类等。
ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs
系统变量所指定的路径下的 jar 包。
AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
Main 类是在 AppClassLoader 里面加载的
双亲委派模型
每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当应当有自己的父类加载器。这里的类加载
器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父类加载器的代码。
AppClassLoader 的父类加载器为 ExtClassLoader
ExtClassLoader 的父类加载器为 null, null 并不代表 ExtClassLoader 没有父类加载器,而是 BootstrapClassLoader。
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载
器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加
载器 BootstrapClassLoader 作为父类加载器
双亲委派机制的好处:
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载
保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型 ,而是每个类加载器加载自己的话就会出现一些问题 ,比如我们编写一个称为java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。
如何破坏:
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法
JVM的内存模型
Java 内存区域和内存模型是不一样的东西,内存区域是指 Jvm 运行时将数据分区域存储,强调对内存空间的划分。
内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。
Java 内存模型(Java Memory Model,JMM)是 java 虚拟机规范定义的,用来屏蔽掉 java 程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现 java 程序在各种不同的平台上都能达到内存访问的一致性。可以避免像 c等直接使用物理硬件和操作系统的内存模型在不同操作系统和硬件平台下表现不同,比如有些 c/c程序可能
在 windows 平台运行正常,而在 linux 平台却运行有问题
JVM性能调优常用命令
工具: JConsole: Java 监视与管理控制台
Visual VM:多合一故障处理工具
命令: jps (JVM Process Status) : 类似 UNIX 的 ps 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟
机参数等信息;
jstat( JVM Statistics Monitoring Tool) : 用于收集 HotSpot 虚拟机各方面的运行数据;
jinfo (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息;
jmap (Memory Map for Java) :生成堆转储快照;
jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在
浏览器上查看分析结果;
jstack (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的
方法堆栈的集合