一、面向对象
1、成员变量、局部变量、类变量存储在内存的什么地方 ?
(1)成员变量定义在类中,存储在类实例所在的堆内存中。 成员变量会随着类实例的产生而产生,销毁而销毁,是类实例的一部分
(2)局部变量定义在类的方法中,存储在虚拟机栈中。当执行到该方法时,该方法会自动到虚拟机的栈顶执行,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
(3)类变量是用static修饰的变量,定义在方法外的变量,随着java进程产生和销毁在java7之前把静态变量存放于方法区,到java7开始存放在堆中
2、重载和重写的区别
重载发生在一个类内部,方法名必须相同,形参列表必须不同;重写发生在继承关系中,子类重新实现了父类中已有的方法,要求方法名、参数列表和返回类型都相同。重载是在编译时进行静态绑定,重写是在运行时进行动态绑定。
3、下面变量引用存放在哪里(jdk1.8)
public class StaticObjTest {
static class Test{ //静态内部类
// 静态变量
// 一个java.lang.Class类型的对象实例引用了此变量
static ObjectHolder staticObj = new ObjectHolder();
// 实例变量
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
// 局部变量
ObjectHolder localObj = new ObjectHolder()();
System.out.println("done");
}
}
private static class ObjectHolder{ //静态内部类
}
public static void main(String[] args) {
Test test = new StaticObjTest.Test();
test.foo();
}
}
4、HotSpot 方法区变迁
HotSpot是指java虚拟机,从jdk1.3.1版本开始代替了JIT,由于从传统的将源代码翻译为字节码文件到虚拟机执行,改为将常用的部分代码编译为本地代码,所以提高了运行性能。
jdk8之前,HotSpot方法区的实现为永久代,永久代使用的是jvm默认内存,容易出现性能问题和内存溢出(OOM)。
jdk8及之后元空间替代了永久代,元空间使用的是本地内存,也就是主机的内存,好处是不会再出现OOM,本地内存剩余多少理论上metaspace就可以有多大。不过总不可能将所有的本地内存都给jvm,所以JVM默认在运行时会根据需要动态的设置其大小。
5、为什么调整字符串常量池和静态变量的位置
jdk7以前StringTable和静态变量放在了永久代,到了jdk7时放在了堆空间,因为永久代的回收效率很低,在Full GC时才会触发,而Full GC在老年代的空间不足、永久代不足时才会触发,这就导致字符串常量池回收效率不高;而放到堆里,能及时回收内存。
6、为什么用元空间替换永久代
(1)因为永久代设置最大空间大小是难以确定的。
(2)在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个工程中,在运行时要不断加载很多很多类,这时就容易出现OOM,而元空间的好处是内存为本地内存,不容易出现OOM。
(3)永久代的调优操作很困难
7、JDK1.8元空间什么情况下会产生内存溢出?
Java8 及以后的版本使用Metaspace来代替永久代,如果加载到内存中的 class 数量太多或者体积太大,仍然会发生内存溢出。
8、java8的内存结构
(1)程序计数器
java中程序计数器是用寄存器实现的,它的作用是寻找下一个要执行的指令。程序计数器是线程私有的,每个线程都已自己的程序计数器。
(2)虚拟机栈
Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。每个方法执行的过程就对应了一个入栈和出栈的过程。
虚拟机栈可能会抛出两种异常
● 如果线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出StackOverFlowError即栈溢出
● 如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出
产生StackOverFlowError的原因
● 无限递归循环调用
● 执行了大量方法,导致线程栈空间耗尽
● 方法内声明了海量的局部变量
产生OutOfMemoryError的原因
● 请求创建一个超大对象,通常是一个大数组。
● 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
● 过度使用终结器(Finalizer),该对象没有立即被 GC。
● 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收。
(3)java堆
堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,JDK 1.7后,字符串常量池从永久代中剥离出来,存放在堆中。
堆中主要存放
对象实例、字符串常量池、静态变量、线程分配缓冲区
堆内存划分
Eden、S0、S1归为Young区(Young Gen),即新生代,执行new时大部分对象在此分配内存,经过一定GC次数(默认15次)后进入old区。
大部分对象在Eden区中生成,当Eden区满时,还存活的对象将被复制到第一个Survivor(s0)区,当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor(s1)区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。
Eden和survivor(S0、S1)默认比例是8:1:1
(4)方法区
主要存放:类型信息、 域(Field)信息、方法(Method)信息、运行时常量池、直接内存
9、你知道的几种主要的JVM参数
-Xmx设置堆的最大空间大小。 -Xmx3550m: 最大堆大小为3550m。
-Xms设置堆的初始空间大小。 -Xms3550m: 设置初始堆大小为3550m。
-XX:NewSize设置新生代最小空间大小。 -Xmn2g: 设置新生代大小为2g。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:MaxPermSize设置永久代最大空间大小。 -XX:MaxPermSize: 设置永久代大小为16m
-XX:PermSize设置永久代最小空间大小。
-Xss设置每个线程的堆栈大小。 -Xss128k: 每个线程的堆栈大小为128k。
二、多线程
1、进程和线程区别
进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
线程是由进程创建的,是进程的一个实体。一个进程可以有多个线程,但一个线程只能属于一个进程。
2、java线程的生命周期
java线程的生命周期分为6个状态:
(1)创建
通过new创建线程
(2)RUNNABLE
可运行状态,创建线程后调用start()方法进入runnable状态,runnable状态也可以细分为就绪和等待两个状态,调用start()方法会先进入就绪状态,获取cpu时间片才会进入运行状态
(3)BLOCKED
同步阻塞,运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则该线程进入锁池中
(4)WAITING
等待阻塞,运行的线程执行wait()方法,该线程进入等待池中
(5)TIMED_WAITING
运行的线程执行sleep()或join()方法,或者发出了I/O请求时,该线程置为阻塞状态。
(6)死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
3、创建线程的方式
(1)实现Runnable
Runnable规定的方法是run(),无返回值,无法抛出异常
(2)继承Thread类创建多线程
继承Thread类,重写Thread类的run()方法,在run()方法中实现运行在线程上的代码,调用start()方法开启线程。Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。
4、wait()和sleep()的区别与联系?
wait是Object类的方法,sleep()是Thread类的方法
sleep是让一个线程进入休眠状态,不会释放持有的锁,也不会影响其他进程的运行,sleep结束后线程获取cpu时间片才会进入运行状态。但在sleep过程中可能会被interrupt(),产生InterruptedException异常,如果没有捕获异常,线程会被终止,只有捕获异常才会执行后面的代码。
wait会释放锁,使得其他线程可以使用同步控制块或方法。wait只能在同步控制方法或者同步控制块里面使用,sleep可以在任何地方使用。使用wait不需要捕获异常,sleep需要捕获异常
5、分时调度模型和抢占式调度模型
分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片。
java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
6、如何避免死锁
1.发生死锁的条件
互斥条件 同一时间只能有一个线程获取资源。
不可剥夺条件 一个线程已经占有的资源,在释放之前不会被其它线程抢占
请求和保持条件 线程等待过程中不会释放已占有的资源
循环等待条件 多个线程互相等待对方释放资源
2.死锁预防,那么就是需要破坏这四个必要条件
互斥条件(一般不能被破坏,但可以避免)
指两个线程都因互相占用对方需要的资源导致程序不能向下进行
比如,synchronized嵌套容易发生死锁,所以需要避免synchronized嵌套使用
破坏不可剥夺条件
一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式地释放重新加入到系统的资源列表中,可以被其他的进程使用
破坏请求与保持条件
静态分配即每个进程在开始执行时就申请他所需要的全部资源,或者动态分配即每个进程在申请所需要的资源时他本身不占用系统资源
破坏环路等待条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
7、synchronized和lock区别 ?
synchronized 和 lock 都是用于实现线程同步的机制。synchronized是java内置的关键字,lock是java5开始提供的接口。synchronized需要语句块执行完毕才会释放锁,lock可以手动控制锁的获取和释放。
使用 synchronized 关键字获取锁时,如果没有成功获取,线程只有被阻塞;而使用Lock.tryLock( )方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回 false。
使用 Lock.tryLock(long time, TimeUnit unit)方法, 显式锁可以设置限定抢占锁的超时时间。而在使用 synchronized 关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。
8、Executor提供了几种线程池
Java 通过 Executors 工厂类提供四种快捷创建线程池的方法
newSingleThreadExecutor()创建只有一个线程的线程池
newFixedThreadPool(int nThreads) 创建指定大小的线程池
newCachedThreadPool() 创建一个不限数量的线程池,如果线程池中的线程数量过大,它可以有效地回收多余的线程,如果线程数不足,那么它可 以创建新的线程。
newScheduledThreadPool() 创建一个可定期或者延时执行任务的线程池
9、线程池的拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,就会执行拒绝策略。
AbortPolicy:拒绝策略。如果线程池队列满了则新任务被拒绝,并且会抛出 RejectedExecutionException异常。该策略是线程池的默认的拒绝策略。
DiscardPolicy:抛弃策略。如果线程池队列满了,新任务会直接被丢掉,并且不会有任何异常抛出。
DiscardOldestPolicy:抛弃最老任务策略。该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
CallerRunsPolicy:调用者执行策略。该策略下,在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任务。
自定义拒绝策略。实现 RejectedExecutionHandler接口的 rejectedExecution 方法可以自定义一个拒绝策略
10、线程池任务调度流程
提交者提交任务后,首先判断核心线程池是否已满,如果未满,则创建线程并执行当前任务。
如果核心线程池已满,就判断阻塞队列是否已满,如果阻塞队列未满,将任务存储在阻塞队列,等待空闲线程执行。如果阻塞队列已满,就创建一个新的线程立刻执行该任务。
在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出 maximumPoolSize。如果线程池的线程总数超时 maximumPoolSize,则线程池会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
11、描述一下锁的四种状态及升级过程?
三、String
1、String str1=“abc”;和String str2=new String(“abc”);的区别
第一种是创建常量方式给字符串赋值,常量"abc"在常量池中创建的对象。第二种是new String方式创建对象,而这个对象是一个value数组存储在堆中,再由堆中的value指向常量池中的"abc"对象。
2、第一次String str2=new String(“abc”);创建了几个对象
创建了两个对象。"abc"本身就是在字符串常量池中创建的对象,而new String又是在堆中创建的对象实例,所以创建了两个对象。
四、集合
1、ArrayList底层扩容机制
ArrayList底层维护了一个Object类型的elementDate数组
使用无参构造器创建ArrayList,开始会先创建一个长度为0的elementDate,当添加第一个元素时再创建一个始容量为10的elementDate,如果添加的元素大于ArrayList长度时,开始扩容。默认情况下,新的容量会是原容量的1.5倍。 (新容量=旧容量右移一位(相当于除于2)再加上旧容量。)
使用有参构造器指定elementDate长度,扩容时会根据指定的大小的1.5倍来扩容
2、 ArrayList与LinkedList区别
ArrayList是基于数组的,LinkedList是基于链表的。LinkedList的插入和删除速度更快,但是随机访问速度慢,ArrayList 元素随机访问速度更快,但是插入与删除速度慢。
3、HashMap原理,java8做了什么改变
hashmap底层维护了Node类型的数组table,默认为null。Node是一组键值对key-value。HashMap里边最重要的两个方法put、get分别添加和取出元素。
hashmap的存储位置是通过hashCode值与数组大小取余计算得出的。
java1.7及之前,hashmap存储结构是(数组+链表)。java1.8时,hashmap扩容到一定程度链表可能会变为红黑树(数组+ (链表 |红黑树))。
4、HashMap底层扩容机制
创建hashmap时会创建一个为null的table表,添加一个元素时会将table表扩容到16,临界值为12,超过临界值hashmap会进行扩容,大小为原容量的2倍。新的临界值=新容量*临界因子(默认0.75)
hashmap的存储位置是通过hashCode值与数组大小取余计算得出的,如果两个元素存储在相同的位置,会发生hash碰撞,形成一个链表,到了jdk1.8当这个链表长度大于8并且数组长度到达64,链表会树化,形成一颗红黑树。
5、List 和 Set,Map 的区别
List和Set都是Collection的子接口,二者本身并没有直接关系。
List实现的子类都是有序可重复集合,可以通过下标访问元素,支持增删改查操作。
Set实现的子类都是无序不可重复集合,不支持下标访问元素,支持增删查操作。
List和Set实现的是单列集合,而Map实现的是双列集合,Map是无序的键值对数据结构,通过键来访问值,键不重复,值可以重复,支持增删改查操作。
6、那些集合是线程安全的
Vector是长度可变的数组,Vector的每个方法都加了 synchronized 修饰符,是线程安全的。
Hashtable是单线程集合,它给几乎所有public方法都加上了synchronized修饰符,是一个线程安全的集合。
stack继承Vector集合,也是线程安全的。
7、TreeMap底层?
与HashMap不同,TreeMap是对元素排序的。与HashMap相同,TreeMap也不允许key元素重复
TreeSet底层使用红黑树结构存储数据,通过自然排序和定制排序对元素进行排序。默认情况下, TreeSet 采用自然排序。
TreeMap特点:有序,查询速度比List快
8、如何决定使用 HashMap 还是TreeMap?
在选择使用 HashMap 还是 TreeMap 时,需要考虑访问数据的速度和数据的顺序性。
访问速度: HashMap 的访问速度比 TreeMap 更快,因为 HashMap 内部使用哈希表,能够快速定位到元素;而 TreeMap 则是基于红黑树实现的,需要进行树的查找操作,效率较低。
数据顺序性: 如果需要按照键的顺序来访问映射表中的元素,则应该使用 TreeMap;因为 TreeMap 会自动按照键值进行排序,比如需要将元素遍历输出或排序,或者需要查找最小值和最大值等操作,使用 TreeMap 会更加合适。
9、HashSet是如何保证不重复的
是通过存储的对象的两个方法进行唯一性判断的hashCode( )和equals(),在调用集合的add(E e)方法时,会进行判断,通过e.hashCode( )获取要添加对象的hash值,和集合里面的对象进行判断,如果hash值不一样,则会存储。如果一样,则会调用equals()方法,和集合中hash值一样的对象进行判断,如果有一个equals返回true,则判定一样,不会存储。直到找到一个通过equals()方法后返回false的元素才会存储。
10、ArrayList 和 Vector 的区别是什么?
二者都实现了List接口,都是有序集合。
Vector是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList是线程序不安全的,它的方法之间是线程不同步的。
Vector默认扩容大小为原来的2倍,ArrayList默认扩容大小为原来的1.5倍。Vector和ArrayList都可以设置容量的初始大小,但Vector还可以设置增长空间大小。
五、网络编程
1、TCP为什么需要三次握手四次挥手
三次握手的本质是确认通信双方收发数据的能力
TCP是通过程序实现的,可靠的,面向连接的协议。而程序是严谨的,每一次建立连接都会进行“三次握手”这样的步骤。建立连接的目的是为了可靠的数据传输。所以需要保证客户端和服务端都能正常的发送或接收数据。
四次挥手的目的是关闭一个连接
三次握手是为了建立可靠的数据传输通道,四次挥手是为了保证等数据传输完再关闭连接,保证双方都达到关闭连接的条件才能断开。
2、为什么TCP连接的时候是三次握手?两次不可以吗?
三次握手的目的是确定双⽅都做好发送数据的准备⼯作,⽽且双⽅都知道对⽅已准备好。
如果改为两次握手,客户端发送请求连接后,服务端接收请求,然后发送给客户端连接请求,如果只有两次握手,服务端发送完请求后不管客户端是否接收到请求就直接开始传输数据,可能会导致数据丢失。
3、为什么TCP连接的时候是三次握手,关闭的时候却是四次挥手?
因为只有在客户端和服务端都没有数据要发送的时候才能关闭TCP。
关闭连接时,被动断开方收到对方的FIN请求结束报文时,如果正在给对方传输数据,不能立刻结束,这时被动断开方就只能给对方发送一个ACK响应报文,目的是告诉对方收到了它的请求结束报文。等待数据传输完毕后,被动断开方会再给对方一个FIN请求结束报文,对方收到后会发出一个ACK响应报文,被动断开方收到响应报文后,二者安全地关闭连接。
而建立连接时,二者之间并不会出现数据未传输完这种问题。所以第一次发送SYN请求连接报文后,第二次的ACK响应报⽂和SYN连接请求报⽂可以同时给对方发送,所以握手只需要三次。
4、为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?
这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。如果不等待2MSL,客户端会直接CLOSED,而服务端一直收不到响应报文就无法正常进入CLOSED。