java常见面试汇总
=
## 某大佬总结的java面试重点
链接:https://www.nowcoder.com/discuss/123525
- Java基础(例如各种集合类,Java的特性,重写重载区别,equals和 hashcode的联系等等)
- 数据库(主要是MySQL,最常问的例如索引,事务隔离级别等等)
- Spring(IOC,AOP的基础和底层例如cglib,jdk proxy等)
- 计网和***作系统(这个有好多,但是重点的也就那些,比如三次握手四 次挥手,死锁,进程线程区别,线程的生命周期等)
- 并发、JVM、线程池(把深入理解JVM的第2、3、7、最后两章看会感觉就足够 了,还有并发编程的艺术(这本没全看--,但是感觉面试经常问,比如原子 类,并发包工具等)
- 算法(面试个人感觉就是剑指offer的难度,遇到了好多原题,但是笔试 就得会贪心动态规划各种了虽然我不怎么会尴尬)
- Redis、微服务(这个如果你简历上写了,面试官才会问,没写一般不 问)
###java中数据类型分为基本数据类型和引用数据类型。
####基本数据类型
整型:byte,short,int,long
浮点型:float,double
字符型:char
布尔型:boolean
####引用数据类型
数组
类
接口
####Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按 值传递的
###在 Java 中定义一个不做事且没有参数的构造方法的作用
Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特 定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中 只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来 调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类 中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做 事且没有参数的构造方法。
###创建一个对象用什么运算符?对象实体与对象引用有何不同?
new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象 实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象 (一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指 向它(可以用n条绳子系住一个气球)。
###关于 final 关键字的一些总结
final关键字主要用在三个地方:变量、方法、类。
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化 之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让 其指向另一个对象。
当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方 法都会被隐式地指定为final方法。
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修 改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方 法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何 性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。 类中所有的private方法都隐式地指定为final。
###为什么TCP客户端最后还要发送一次确认呢?
**一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。**
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一 个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于 TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服 务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数 据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服 务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务 器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受 到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由 于服务器收不到确认,就知道客户端并没有请求连接。
![三次握手](https://img-blog.csdn.net/20170605110405666?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXpjc3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
###为什么在四次挥手客户端最后还要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同 的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报 文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开 了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于 是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重 传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现 在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可 以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连 接中不会出现旧连接的请求报文。
###为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文 后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,**也可以发送一些数据给对方后**,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
###四 TCP 协议如何保证可靠传输
* 应用数据被分割成 TCP 认为最适合发送的数据块。
* TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传 送给应用层。
* 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
* TCP的接收端会丢弃重复的数据。
* 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只 允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的 数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协 议是可变大小的滑动窗口协议。**(TCP 利用滑动窗口实现流量控制)**
* 拥塞控制: 当网络拥塞时,减少数据的发送。
* ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就 停止发送,等待对方确认。在收到确认后再发下一个分组。
* 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收 到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
![三次握手](https://img-blog.csdn.net/20170607205756255?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXpjc3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
###五.接口与抽象的异同
####1.抽象类
抽象类是关键字abstract修饰的类,既为抽象类,抽象抽象即不能被实例化。而不能被实例化就无用处,所以抽象类只能作为基类(父类),即被继承的类。抽象类中可以包含抽象方法也可以不包含,但具有抽象方法的类一定是抽象类。
抽象类的使用原则如下:
(1)被继承性:抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public;
(2)**抽象性**:抽象类不能直接实例化,需要依靠子类采用向上转型的方式处理;
(3)抽象类必须有子类,使用extends继承,一个子类只能继承一个抽象类;
(4)子类(如果不是抽象类)则**必须覆写抽象类之中的全部抽象方法**(如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。
####1、接口与类相似点
一个接口可以有多个方法。
接口文件保存在 .java 结尾的文件中,文件名使用接口名。
接口的字节码文件保存在 .class 结尾的文件中。
接口相应的字节码文件必须在与包名称相匹配的目录结构中。
####2、接口与类的区别
接口不能用于实例化对象。
接口没有构造方法。
接口中所有的方法必须是抽象方法。
接口不能包含成员变量,除了 static 和 final 变量。
接口不是被类继承了,而是要被类实现。
接口支持多继承。
####3、接口特性
接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
![接口与抽象类异同](https://i.imgur.com/28uQkav.png)
![](https://i.imgur.com/mxRbvpg.png)
###六.static关键字
* static关键字并不会改变变量和方法的访问权限。 与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。
* 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。 很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行
* static成员变量的初始化顺序按照定义的顺序进行初始化。
* 即使没有显示地声明为static,类的构造器实际上也是静态方法。
* static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。
* * static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
* 普通类是不允许声明为静态的,只有内部类才可以。
被static修饰的内部类可以直接作为一个普通类来使用,而不需实例一个外部类
* static能作用于局部变量么?
在C/C++中static是可以作用域局部变量的,但是在Java中切记:static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定。
* 常见的笔试面试题
1. `public class Test extends Base{`
` static{
System.out.println("test static");
}`
`public Test(){
System.out.println("test constructor");
}
`
` public static void main(String[] args) {
new Test();
}
}`
` class Base{`
static{
System.out.println("base static");
}
public Base(){
System.out.println("base constructor");
}
`}`
输出:
`base static
test static
base constructor
test constructor `
先来想一下这段代码具体的执行过程,在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。
* static块可以出现类中的任何地方(只要不是方法内部,记住,任何方法内部都不行),并且执行是按照static块的顺序执行的。
* 这段代码的输出结果是什么?
` public class Test {
Person person = new Person("Test");
`
` static{
System.out.println("test static");
}
`
` public Test() { `
` System.out.println("test constructor"); `
} `
` public static void main(String[] args) {
new MyClass();
}`
}
` class Person{ `
` static{
System.out.println("person static");
}`
` public Person(String str) { `
` System.out.println("person "+str);
}`
`} `
` class MyClass extends Test { `
` Person person = new Person("MyClass"); `
` static{
System.out.println("myclass static");
}`
public MyClass() {
System.out.println("myclass constructor");
}
` } `
输出:
` test static `
` myclass static `
` person static `
` person Test`
` test constructor`
` person MyClass`
` myclass constructor`
#####思路:
首先加载Test类,因此会执行Test类中的static块。接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。在加载完之后,就通过构造器来生成对象。而在生成对象的时候,***必须先初始化父类的成员变量***,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。
###七.final
* 根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。
final类不能被继承,没有子类,final类中的方法默认是final的。
final方法不能被子类的方法覆盖,但可以被继承。
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
final不能用于修饰构造方法。
注意:父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的。
* final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。
* final方法
如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
使用final方法的原因有二:
第一、把方法锁定,防止任何继承类修改它的意义和实现。
第二、高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
* final变量(常量)
用final修饰的成员变量表示常量,值一旦给定就无法改变!
final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
从下面的例子中可以看出,一旦给final变量初值后,值就不能再改变了。
另外,final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。
* static和final一块用表示什么
static final用来修饰成员变量和成员方法,可简单理解为“全局常量”!
对于变量,表示一旦给值就不可修改,并且通过类名可以访问。
对于方法,表示不可覆盖,并且可以通过类名直接访问。
** 特别要注意一个问题: **
对于被static和final修饰过的实例常量,实例本身不能再改变了,但**对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象,**这一点在编程中用到很多。
###八.Java关键字this、super使用总结
####一、this
Java关键字this只能用于方法方法体内。当一个对象创建后,
Java虚拟机(JVM)就会给这个对象分配一个引用自身的指针,这个指针的名字就是this。因此,this只能在类中的非静态方法中使用,静态方法和静态的代码块中绝对不能出现this,这在“Java关键字static、final使用总结”一文中给出了明确解释。并且this只和特定的对象关联,而不和类关联,同一个类的不同对象有不同的this。
* 在什么情况下需要用到this:
第一、通过this调用另一个构造方法,用发是this(参数列表),这个仅仅在类的构造方法中,别的地方不能这么用。
第二、函数参数或者函数中的局部变量和成员变量同名的情况下,成员变量被屏蔽,此时要访问成员变量则需要用“this.成员变量名”的方式来引用成员变量。当然,在没有同名的情况下,可以直接用成员变量的名字,而不用this,用了也不为错,呵呵。
第三、在函数中,需要引用该函所属类的当前对象时候,直接用this。
####二、super
super关键和this作用类似,是被屏蔽的成员变量或者成员方法 或变为可见,或者说用来引用被屏蔽的成员变量和成员成员方法。
不过super是用在子类中,目的是访问直接父类中被屏蔽的成员,注意是直接父类(就是类之上最近的超类)。
* 总结一下super的用法:
第一、在子类构造方法中要调用父类的构造方法,用“super(参数列表)”的方式调用,参数不是必须的。同时还要注意的一点是:**“super(参数列表)”这条语句只能用在子类构造方法体中的第一行。**
第二、当子类方法中的局部变量或者子类的成员变量与父类成员变量同名时,也就是子类局部变量覆盖父类成员变量时,用“super.成员变量名”来引用父类成员变量。当然,如果父类的成员变量没有被覆盖,也可以用“super.成员变量名”来引用父类成员变量,不过这是不必要的。
第三、当子类的成员方法覆盖了父类的成员方法时,也就是子类和父类有完全相同的方法定义(但方法体可以不同),此时,用“super.方法名(参数列表)”的方式访问父类的方法。
###九.java中的匿名内部类总结
匿名内部类也就是没有名字的内部类
正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写
但使用匿名内部类还有个**前提条件:必须继承一个父类或实现一个接口**
####匿名内部类的基本实现
abstract class Person {
public abstract void eat();}public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}}
可以看到,我们直接将抽象类Person中的方法在大括号中实现了
这样便可以省略一个类的书写
并且,匿名内部类还能用于接口上
####在接口上使用匿名内部类
interface Person {
public void eat();}public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}}
由上面的例子可以看出,只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现
最常用的情况就是在多线程的实现上,因为要实现多线程必须继承Thread类或是继承Runnable接口
####Thread类的匿名内部类实现
public class Demo {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
}
};
t.start();
}}
####Runnable接口的匿名内部类实现
public class Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
}
};
Thread t = new Thread(r);
t.start();
}}
### 垃圾回收算法原理
转载自:https://blog.csdn.net/FateRuler/article/details/81158510
#### 第一种:标记清除
它是最基础的收集算法。
原理:分为标记和清除两个阶段:首先标记出所有的需要回收的对象,在标记完成以后统一回收所有被标记的对象。
特点:(1)效率问题,标记和清除的效率都不高;(2)空间的问题,标记清除以后会产生大量不连续的空间碎片,空间碎片太多可能会导致程序运行过程需要分配较大的对象时候,无法找到足够连续内存而不得不提前触发一次垃圾收集。
地方 :适合在老年代进行垃圾回收,比如CMS收集器就是采用该算法进行回收的。
#### 第二种:标记整理
原理:分为标记和整理两个阶段:首先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
特点:不会产生空间碎片,但是整理会花一定的时间。
地方:适合老年代进行垃圾收集,parallel Old(针对parallel scanvange gc的) gc和Serial old收集器就是采用该算法进行回收的。
#### 第三种:复制算法
原理:它先将可用的内存按容量划分为大小相同的两块,每次只是用其中的一块。当这块内存用完了,就将还存活着的对象复制到另一块上面,然后把已经使用过的内存空间一次清理掉。
特点:没有内存碎片,只要移动堆顶指针,按顺序分配内存即可。代价是将内存缩小位原来的一半。
地方:适合新生代区进行垃圾回收。serial new,parallel new和parallel scanvage
收集器,就是采用该算法进行回收的。
#### 复制算法改进思路:
由于新生代都是朝生夕死的,所以不需要1:1划分内存空间,可以将内存划分为一块较大的Eden和两块较小的Suvivor空间。每次使用Eden和其中一块Survivor。当回收的时候,将Eden和Survivor中还活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Suevivor空间。其中Eden和Suevivor的大小比例是8:1。缺点是需要老年代进行分配担保,如果第二块的Survovor空间不够的时候,需要对老年代进行垃圾回收,然后存储新生代的对象,这些新生代当然会直接进入来老年代。
优化收集方法的思路
分代收集算法
原理:根据对象存活的周期的不同将内存划分为几块,然后再选择合适的收集算法。
一般是把java堆分成新生代和老年代,这样就可以根据各个年待的特点采用最适合的收集算法。在新生代中,每次垃圾收集都会有大量的对象死去,只有少量存活,所以选用复制算法。老年代因为对象存活率高,没有额外空间对他进行分配担保,所以一般采用标记整理或者标记清除算法进行回收。
对于以上两种标记算法存在争议,在深入了解JVM最佳实践第二版中,是写的标记需要回收的对象,我也没太深入思考,直到有人提出来,我也去查了一下和想了一下。我个人现在偏向,标记存活的对象。
**标记算法的大概流程:通过引用链给所有存活的对象做个标记,然后回收所有没有标记的对象 和 清除存活对象的标记,等待下一次GC**
### GC用的引用可达性分析算法中,哪些对象可作为GC Roots对象
先说一下可达性分析算法的思想:从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
#### 在java中可以作为GC Roots的对象有以下几种:
转载自:https://blog.csdn.net/ma345787383/article/details/77099522
虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象 **不一定会被回收。**当一个对象不可达GC Roots时,这个对象并不会马上被回收,而是处于一个死缓的阶段,**若要被真正的回收需要经历两次标记。**如果对象在可达性分析中没有与GC Roots的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已经被虚拟机调用过,那么就认为是没必要的。
如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的队列中,虚拟机会触发一个finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这还是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除“即将回收”集合,等待回收。
### hashmap中key是否可以为null
1、 HashMap计算key的hash值时调用单独的方法,在该方法中会判断key是否为null,如果是则返回0;而Hashtable中则直接调用key的hashCode()方法,因此如果key为null,则抛出空指针异常。
2、 HashMap将键值对添加进数组时,不会主动判断value是否为null;而Hashtable则首先判断value是否为null。
3、以上原因主要是由于Hashtable继承自Dictionary,而HashMap继承自AbstractMap。
4、虽然ConcurrentHashMap也继承自AbstractMap,但是其也过滤掉了key或value为null的键值对。
* JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过**(n - 1) & hash** 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
### 十.基本数据类型和对象的区别
(1) 基本数据类型的存储原理:所有的简单数据类型不存在“引用”的概念,基本数据类型都是直接存储在内存中的栈上的,数据本身的值就是存储在栈空间里面,Java语言里面八种数据类型是这种存储模型;
(2) 引用类型的存储原理:引用类型继承于Object类(也是引用类型)都是按照Java里面存储对象的内存模型来进行数据存储的,使用Java堆和栈来进行这种类型的数据存储,简单地讲,“引用”(存储对象在内存堆上的地址)是存储在有序的栈上的,而对象本身的值存储在堆上的;
不论是基本数据类型还是引用类型,他们都会先在栈中分配一块内存,对于基本类型来说,这块区域包含的是基本类型的内容;而对于引用类型来说,这块区域包含的是指向真正内容的指针,真正的内容被手动的分配在堆上。
### Synchronized的使用
#### synchronized的三种应用方式
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
1. 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
2. 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3. 同步方法块,锁是**括号里面的对象**,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
#### 一个类中存在一个synchronized修饰的方法和一个普通的方法,不同线程同时访问这两个方法,会出现什么情况?
* 一个线程持有对象锁,另一个线程可以以异步的方式调用对象里面的非synchronized方法,输出结果是不按照顺序的
* 一个线程持有对象锁,另一个线程可以以同步的方式调用对象里面的synchronized方法,需要等待上一个线程释放资源,也就是同步。
* 两个线程访问不同对象中不同的synchronized方法不会受到synchronized的限制
### Java常见的几种内存溢出及解决方案
* 1.JVM Heap(堆)溢出:java.lang.OutOfMemoryError: Java heap space
JVM在启动的时候会自动设置JVM Heap的值, 可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap的大小是Young Generation 和Tenured Generaion 之和。在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息。
解决方法:手动设置JVM Heap(堆)的大小。
Java堆用于储存对象实例。当需要为对象实例分配内存,而堆的内存占用又已经达到-Xmx设置的最大值。将会抛出OutOfMemoryError异常。
* 2.PermGen space溢出: java.lang.OutOfMemoryError: PermGen space
PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被JVM存放Class和Meta信息的,Class在被Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同,sun的 GC不会在主程序运行期对PermGen space进行清理,所以如果你的APP会载入很多CLASS的话,就很可能出现PermGen space溢出。一般发生在程序的启动阶段。
解决方法: 通过-XX:PermSize和-XX:MaxPermSize设置永久代大小即可。
方法区用于存放java类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。在类装载器加载class文件到内存的过程中,虚拟机会提取其中的类型信息,并将这些信息存储到方法区。当需要存储类信息而方法区的内存占用又已经达到-XX:MaxPermSize设置的最大值,将会抛出OutOfMemoryError异常。
* 3.栈溢出: java.lang.StackOverflowError : Thread Stack space
栈溢出了,JVM依然是采用栈式的虚拟机,这个和C和Pascal都是一样的。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的 “层”太多了,以致于把栈区溢出了。 通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K的空间(这个大约相当于在一个C函数内声明了256个int类型的变量),那么栈区也不过是需要1MB的空间。通常栈的大小是1-2MB的。通俗一点讲就是单线程的程序需要的内存太大了。 通常递归也不要递归的层次过多,很容易溢出。
解决方法:1:修改程序。2:通过 -Xss: 来设置每个线程的Stack大小即可。
在Java虚拟机规范中,对这个区域规定了两种异常状况:StackOverflowError和OutOfMemoryError异常。
- (1)StackOverflowError异常
每当java程序代码启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。当线程调用java方法时,虚拟机压入一个新的栈帧到该线程的java栈中。只要这个方法还没有返回,它就一直存在。如果线程的方法嵌套调用层次太多(如递归调用),随着java栈中帧的逐渐增多,最终会由于该线程java栈中所有栈帧大小总和大于-Xss设置的值,而产生StackOverflowError内存溢出异常。
(2)OutOfMemoryError异常
java程序代码启动一个新线程时,没有足够的内存空间为该线程分配java 栈(一个线程java栈的大小由-Xss参数确定),jvm则抛出OutOfMemoryError异常。
* Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
? StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。
? OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。
### 十一.内存泄漏
* 内存泄漏与内存溢出的关系:
内存泄漏的堆积最终会导致内存溢出
内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系 统相当于没法满足你的需求,就会报内存溢出的错误。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以 后却不归还(delete),结果你申请到的那块内存你自己也不能再访问 (也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的 程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之 后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法 供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信 息。
内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉 倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空 间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分 配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了 那么多,那我就报错,
#### 内存泄漏的几种情况:
* 1、**静态集合类**,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
* 2、**各种连接**,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
* 3、**变量不合理的作用域**。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
public class UsingRandom {
private String msg;
public void receiveMsg(){
readFromNet();// 从网络中接受数据保存到msg中
saveDB();// 把msg保存到数据库中
}
}
如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg 中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没 用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因 此造成了内存泄漏。
实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么 msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg 后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。
* 4、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
* 5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
### Java中常见的几种RuntimeException
#### JAVA中常见的几种RuntimeException,大约有如下几种:
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ArithmeticException - 算术运算异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常
#### 常见的RuntimeException
RuntimeException是开发中最容易遇到的,下面列举一下常见的RuntimeException:
- 1、NullPointerException:见的最多了,其实很简单,一般都是在null对象上调用方法了。
String s=null;
boolean eq=s.equals(""); // NullPointerException
这里你看的非常明白了,为什么一到程序中就晕呢?
public int getNumber(String str){
if(str.equals("A")) return 1;
else if(str.equals("B")) return 2;
}
这个方法就有可能抛出NullPointerException,我建议你主动抛出异常,因为代码一多,你可能又晕了。
public int getNumber(String str){
if(str==null) throw new NullPointerException("参数不能为空");
//你是否觉得明白多了
if(str.equals("A")) return 1;
else if(str.equals("B")) return 2;
}
- 2、NumberFormatException:继承IllegalArgumentException,字符串转换为数字时出现。比如int i= Integer.parseInt("ab3");
- 3、ArrayIndexOutOfBoundsException:数组越界。比如 int[] a=new int[3]; int b=a[3];
- 4、StringIndexOutOfBoundsException:字符串越界。比如 String s="hello"; char c=s.chatAt(6);
- 5、ClassCastException:类型转换错误。比如 Object obj=new Object(); String s=(String)obj;
- 6、UnsupportedOperationException:该操作不被支持。如果我们希望不支持这个方法,可以抛出这个异常。既然不支持还要这个干吗?有可能子类中不想支持父类中有的方法,可以直接抛出这个异常。
- 7、ArithmeticException:算术错误,典型的就是0作为除数的时候。
- 8、IllegalArgumentException:非法参数,在把字符串转换成数字的时候经常出现的一个异常,我们可以在自己的程序中好好利用这个异常。
### java中hashmap的原理
#### 较详细链接(评论里也有详细解答)
https://www.nowcoder.com/questionTerminal/6bd3857199564b3fb2d3fee4f4de06ea?toCommentId=1185246
链接:https://www.nowcoder.com/questionTerminal/6bd3857199564b3fb2d3fee4f4de06ea?toCommentId=1185246
来源:牛客网
hashmap是一个key-value键值对的数据结构,从结构上来讲在jdk1.8之 前是用数组加链表的方式实现,jdk1.8加了红黑树,hashmap数组的默认初 始长度是16,hashmap数组只允许一个key为null,允许多个value为null
hashmap的内部实现,hashmap是使用数组+链表+红黑树的形式实现的, 其中数组是一个一个Node[]数组,我们叫他hash桶数组,它上面存放的是 key-value键值对的节点。HashMap是用hash表来存储的,在hashmap里 为解决hash冲突,使用链地址法,简单来说就是数组加链表的形式来解决 ,当数据被hash后,得到数组下标,把数据放在对应下表的链表中。
然后再说一下hashmap的方法实现
put方法,put方法的第一步,就是计算出要put元素在hash桶数组中的索引 位置,得到索引位置需要三步,去put元素key的hashcode值,高位运算, 取模运算,高位运算就是用第一步得到的值h,用h的高16位和低16位进行异 或操作,第三步为了使hash桶数组元素分布更均匀,采用取模运算,取模运 算就是用第二步得到的值和hash桶数组长度-1的值取与。这样得到的结果和 传统取模运算结果一致,而且效率比取模运算高
jdk1.8中put方法的具体步骤,先判断hashmap是否为空,为空的话扩容, 不为空计算出key的hash值i,然后看table[i]是否为空,为空就直接插 入,不为空判断当前位置的key和table[i]是否相同,相同就覆盖,不相同 就查看table[i]是否是红黑树节点,如果是的话就用红黑树直接插入键值 对,如果不是开始遍历链表插入,如果遇到重复值就覆盖,否则直接插入, 如果链表长度大于8,转为红黑树结构,执行完成后看size是否大于阈值 threshold,大于就扩容,否则直接结束
get方法就是计算出要获取元素的hash值,去对应位置取即可。
扩容机制,hashmap的扩容中主要进行两部,第一步把数组长度变为原来的 两倍,第二部把旧数组的元素重新计算hash插入到新数组中,在jdk1.8时,不用重新计算hash,只用看看原来的hash值新增的一位是零还是1,如果是1这个元素在新数组中的位置,是原数组的位置加原数组长度,如果是零就插入到原数组中。扩容过程第二部一个非常重要的方法是transfer方法,采用头插法,把旧数组的元素插入到新数组中。
3.hashmap大小为什么是2的幂次方
在计算插入元素在hash桶数组的索引时第三步,为了使元素分布的更加均匀,用取模操作,但是传统取模操作效率低,然后优化成h&(length-1),设置成2幂次方,是因为2的幂次方-1后的值每一位上都是1,然后与第二步计算出的h值与的时候,最终的结果只和key的hashcode值本身有关,这样不会造成空间浪费并且分布均匀,如果不是2的幂次方
如果length不为2的幂,比如15。那么length-1的2进制就会变成1110。在h为随机数的情况下,和1110做&操作。尾数永远为0。那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费,不随机等问题。
### spring
#### 1.单例及线程安全
spring依赖注入时,使用了双重判断加锁的单例模式,首先从缓存中获取bean实例,如果为null,对缓存map加锁,然后再从缓存中获取bean,如果继续为null,就创建一个bean。这样双重判断,能够避免在加锁的瞬间,有其他依赖注入引发bean实例的创建,从而造成重复创建的结果
### java静态方法不能调用非静态方法的原因
静态方法是属于类的,即静态方法是随着类的加载而加载的,在加载类时,程序就会为静态方法分配内存,而非静态方法是属于对象的,对象是在类加载之后创建的,也就是说静态方法先于对象存在,当你创建一个对象时,程序为其在堆中分配内存,一般是通过this指针来指向该对象。静态方法不依赖于对象的调用,它是通过‘类名.静态方法名’这样的方式来调用的。而对于非静态方法,在对象创建的时候程序才会为其分配内存,然后通过类的对象去访问非静态方法。因此在对象未存在时非静态方法也不存在,静态方法自然不能调用一个不存在的方法
### JDK1.8新特性
- Lambda表达式
- 函数式接口
- 方法引用和构造器调用
- Stream API
- 接口中的默认方法和静态方法
- 新时间日期API
- default关键字
在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化
原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode 方法,计算出哈希码值,经过哈希算法算成数组的索引值,如果对应的索引处没有元素,直接存放,如果有对象在,那么比较它们的equals方法比较内容
如果内容一样,后一个value会将前一个value的值覆盖,如果不一样,在1.7的时候,后加的放在前面,形成一个链表,形成了碰撞,在某些情况下如果链表 无限下去,那么效率极低,碰撞是避免不了的
加载因子:0.75,数组扩容,达到总容量的75%,就进行扩容,但是无法避免碰撞的情况发生
在1.8之后,在数组+链表+红黑树来实现hashmap,当碰撞的元素个数大于8时 & 总容量大于64,会有**红黑树**的引入
除了添加之后,效率都比链表高,1.8之后链表新进元素加到末尾
ConcurrentHashMap (锁分段机制),concurrentLevel,**jdk1.8采用CAS算法(无锁算法,不再使用锁分段)**,**数组+链表中也引入了红黑树的使用**
-- default关键字
通常都是认为接口里面是只能有抽象方法,不能有任何方法的实现的,那么在jdk1.8里面打破了这个规定,引入了新的关键字default,通过使用default修饰方法,可以让我们在接口里面定义具体的方法实现,如下:
```
public interface NewCharacter {
public void test1();
public default void test2(){
System.out.println("我是新特性1");
}
}
```
定义一个方法的作用是什么呢?为什么不在接口的实现类里面再去实现方法呢?
其实这么定义一个方法的主要意义是定义一个默认方法,也就是说这个接口的实现类实现了这个接口之后,不用管这个default修饰的方法,也可以直接调用,如下:
```
public class NewCharacterImpl implements NewCharacter{
@Override
public void test1() {
}
public static void main(String[] args) {
NewCharacter nca = new NewCharacterImpl();
nca.test2();
}
}
```
default方法是所有的实现类都不需要去实现的就可以直接调用,那么比如说jdk的集合List里面增加了一个sort方法,那么如果定义为一个抽象方法,其所有的实现类如arrayList,LinkedList等都需要对其添加实现,那么现在用default定义一个默认的方法之后,其实现类可以直接使用这个方法了,这样不管是开发还是维护项目,都会大大简化代码量
### 继承和组合
借鉴这篇文章 :
https://blog.csdn.net/wrs120/article/details/88584705
#### 继承优点:
- 代码复用
- 子类可重写父类方法
- 子类在父类的继承上可根据自己的业务需求扩展
- 创建子类对象时,无需创建父类对象,子类自动继承父类的的成员变量和方法,如果权限允许,子类可直接访问
#### 缺点:
- 不支持动态继承,在编译阶段就确定了子类的父类
- 破坏封装性
- 封装性指出每个类都应该封装它内容信息和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的成员变量和方法,如下例子中父类Fruit中有成员变量weight。Apple继承了Fruit之后,Apple可直接操作Fruit类的成员变量,因此破坏了封装性!
- 紧耦合
当父类的实现做了修改时,父类也不得不修改(比如修改了父类某个接口名,子类也必须作相应修改);子类必须依赖父类存在
#### 组合
##### 优点:
- 支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象(扩展性比继承好)
- 不破坏封装性
- 松耦合
##### 缺点:
- 整体类不能自动获取局部类的接口(整体类可 以看成是上面的bird,如果想在bird里使用Animal的方法,必须写代码来调用,但是继承,bird自动就拥有了animal的方法)
- 没有实现多态
#### 组合举例
```
public class Animal {
private void beat(){
System.out.println("心脏跳动...");
}
public void breath(){
beat();
System.out.println("呼吸中...");
}
}
public class Bird {
//将Animal作为Bird的成员变量
private Animal a;
public Bird(Animal a){
this.a = a;
}
public void breath(){
a.breath();
}
public void fly(){
System.out.println("我在飞..");
}
public static void main(String[] args){
Animal animal = new Animal();
Bird b = new Bird(animal);
b.breath();
b.fly();
}
}
```
#### 如何选择?
- 想实现复用,并且复用部分可能改变,用继承;复用不会改变,用组合
- 当两个类之间明显存在整体与部分的关系时,用组合关系
### 进程间的通讯方式
#### liunx六大进程间通信方式
##### 管道,消息队列,共享内存,信号量,socket,信号,文件锁
- 1,管道
1,匿名管道:
概念:在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利,一般使用fork函数实现父子进程的通信。
2,命名管道:
概念:在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利,没有血缘关系的进程也可以进程间通信。
3,特点:
1,面向字节流,
2,生命周期随内核
3,自带同步互斥机制。
4,半双工,单向通信,两个管道实现双向通信。
- 2,消息队列
1,概念:在内核中创建一队列,队列中每个元素是一个数据报,不同的进程可以通过句柄去访问这个队列。
消息队列提供了?个从?个进程向另外?个进程发送?块数据的?法。
每个数据块都被认为是有?个类型,接收者进程接收的数据块可以有不同的类型值
消息队列也有管道?样的不?,就是每个消息的最??度是有上限的(MSGMAX),
每个消息队 列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有?个上限(MSGMNI)
2,特点:
1, 消息队列可以认为是一个全局的一个链表,链表节点钟存放着数据报的类型和内容,有消息队列的标识符进行标记。
2,消息队列允许一个或多个进程写入或者读取消息。
3,消息队列的生命周期随内核。
4,消息队列可实现双向通信。
- 3,信号量
1,概念
在内核中创建一个信号量集合(本质是个数组),数组的元素(信号量)都是1,使用P操作进行-1,使用V操作+1,
(1) P(sv):如果sv的值?大于零,就给它减1;如果它的值为零,就挂起该进程的执? 。
(2) V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运?,如果没有进程因等待sv?挂起,就给它加1。
PV操作用于同一进程,实现互斥。
PV操作用于不同进程,实现同步。
2,功能:
对临界资源进行保护。
- 4,共享内存
1,概念:
将同一块物理内存一块映射到不同的进程的虚拟地址空间中,实现不同进程间对同一资源的共享。
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。
2,特点:
1,不用从用户态到内核态的频繁切换和拷贝数据,直接从内存中读取就可以。
2,共享内存是临界资源,所以需要操作时必须要保证原子性。使用信号量或者互斥锁都可以。
3,生命周期随内核。
- 5,总结
所有的以上的方式都是生命周期随内核,不手动释就不会消失。