JAVA 高频基础知识

基础知识

##Java语言的三大特性是什么?

  1. 封装 指将对象的属性私有化,提供一些访问属性的方法,通过访问方法,去访问对象的属性
  2. 继承 (java单继承,多实现)
    子类继承父类的所有属性方法(包括私有属性和方法),不过不能访问私有属性和方法,只是拥有。
    子类可以拥有自己的方法,可以扩展
    子类可以重写父类的方法
  3. 多态
    运行时多态,子类重写父类的方法,只有在运行时才知道具体调用哪个方法。

重载与重写的区别

  1. 重载:发生在一个类中,方法名相同,返回值和访问修饰符可以不同,参数列表和参数顺序,参数个数一定不同的两种方法
  2. 重写:子类重写父类的方法,方法名,参数列表都相同
    在这里插入图片描述

接口和抽象类的区别是什么?

  • 相同点:都不能被实例化 ; 接口的实现类或者抽象类的子类实现了接口或抽象类的方法才能被实例化。
  • 不同点:
    1. 接口只有定义,不能有实现的方法,抽象类可以有定义和实现
    2. 实现接口implements,继承抽象类extends(单继承多实现)
    3. 接口强调特定功能的实现,抽象类强调所属关系
    4. 接口方法默认public,抽象方法有public,protected,default
    5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
      接口是行为的抽象,是一种行为的规范,利用接口可以很好地降低各模块的耦合,从而提高系统的可扩展性和可维护性。

java 中内部类?

java有4个内部类

  1. 静态内部类 常见的main函数就是静态内部类, 调用静态内部类通过 外部类.静态内部类
  2. 局部内部类 定义在方法中的类叫做局部内部类
  3. 匿名内部类 是指继承一个父类或者实现一个接口的方式直接定义并使用的类,匿名内部类没有class关键字,因为匿名内部类直接使用new生成一个对象
  4. 成员内部类 成员内部类是最普通的内部类,它可以无条件访问外部类的所有成员属性和成员方法

final关键字

final 修饰的类,方法, 属性,一旦初始化就不能被修改。

  1. 修饰类: 表示此类不能被继承,final中所有方法都被默认指定为final
  2. 修饰方法:这个方法不能被重写
  3. 修饰属性:如果是基本类型,一旦初始化,不能修改,如果是引用类型,初始化后不能在指向其他对象

“hello” 与new String(“hello”)的区别

string s1=hello,会在字符串常量池中找有没有“hello”,如果有栈中的引用就直接指向他,没有就在字符串常量池中加入hello,在指向他。
new 会在,若常量池中不存在hello,加入到常量池中,然后在堆中开辟一块内存存放“hello”,栈中的引用分别指向各自创建对象的内存地址。

string StringBuilder和StringBuffer的区别

string类中使用final修饰字符数组来保存字符串,private final char value[],所以string是不可变的
stringbuilder 和stringbuffer都继承了AbstractStringBuilder ,但是没有用final修饰char value[] ,stringbuffer对方法加了同步锁,stringbuilder没有加

  1. 操作少量数据,使用string
  2. 单线程操作,使用strignbuilder速度快
  3. 多线程,使用strignbuffer

说一下Java中的==与eaquels的区别

= = : 基本类型比较的是值,引用数据类型比较对象地址
equals(): 没有重写equals ,等价于= =
重写了equals,比较两个对象的内容是否相等

为什么重写 equals() 时必须重写 hashCode() 方法?

两个对象相等其哈希值一定相等,不重写hashcode,默认hashcode是内存地址计算的,产生矛盾。
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

大大减少了 equals 的次数,相应就大大提高了执行速度

Java访问修饰符有哪些?都有什么区别?

public 无限制
private 除了本类任何类都不能直接使用。
protect 子类可以自由使用,对于其他磊来说,protect相当于private

static

类成员不能访问实例成员,static修饰的类可以被继承

怎么获取private修饰的变量

private可以通过反射获取,可以设置setaccessable为true实现

Java反射机制的理解?

反射机制指的是在运行状态中,对于任何一个类,可以获取类的属性和方法,对于任何一个对象,可以调用对象的方法,这种动态获取信息,动态调用对象的方法称为反射机制。简单的说反射将类的各个组成部分封装成其他对象。可以在运行过程中操作这些对象。

java中的异常体系

java中的异常主要分为Error和Exception

在这里插入图片描述
Error java 程序运行错误,出现Error,系统将退出程序(不能被动态处理,只能记录错误的原因和终止),是系统内部错误,或者资源耗尽。
Exception java程序运行异常,运行时发生了程序员不期望的事情,可以被java异常处理机制处理,分为运行时异常,检查异常
在这里插入图片描述

  • RuntimeException(运行时异常):指在Java虚拟机正常运行期间抛出的异常,RuntimeException可以被捕获并处理,如果出现此情况,我们需要抛出异常或者捕获并处理异常。常见的有NullPointerException、ClassCastException、ArrayIndexOutOfBoundsException等
  • CheckedException(检查异常):指在编译阶段Java编译器检查CheckedException异常,并强制程序捕获和处理此类异常,要求程序在可能出现异常的地方通过try catch语句块捕获异常并处理异常。常见的有由于I/O错误导致的IOException、SQLException、ClassNotFoundException等。该类异常通常由于打开错误的文件、SQL语法错误、类不存等引起。

追问:异常的处理方式?

异常处理方式 抛出异常 和使用try catch

  • 抛出异常 throws、throw和系统自动抛出异常。其中throws作用在方法上,用于定义方法可能抛出的异常;throw作用在方法内,表示明确抛出一个异常。
  • try catch 使用费try catch 捕获异常能够有针对性的处理每种可能出现的异常,并在捕获到异常后根据不同的情况做不同的处理。其使用过程比较简单:try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。

在finally中return会发生什么?

在通常情况下,不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return、throw语句,将会导致try块、catch块中的return、throw语句失效。

深拷贝和浅拷贝?

深拷贝和浅拷贝都是对象拷贝
浅拷贝 对基本类型进行值传递,对引用类型进行引用传递的拷贝,是浅拷贝
深拷贝 对基本类型进行值传递,对引用数据类型,创建一个新的对象,复制其内容,为深拷贝

浅拷贝与深拷贝的特点是什么?

浅拷贝:

  1. 基本类型是之值传递,修改一个对象不会影响另一个
  2. 引用数据类型的,将内存地址赋值给另一个,指向同一内存空间,改变其中一个,也会影响另一个
    深拷贝:
  3. 基本类型是之值传递,修改一个对象不会影响另一个
  4. 对引用数据类型,创建一个新的对象,复制其内容,为深拷贝,改变一个不会影响另外一个
  5. 深拷贝相比于浅拷贝速度较慢并且花销较大

JDK动态代理和CGlib动态代理

JDK 动态代理只能代理实现了接口的类,CGLIB 可以代理未实现任何接口的类(如果类被final修饰,就不能代理成功)。

Jdk动态代理:利用拦截器(必须实现 InvocationHandler )加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
Cglib动态代理:利用 ASM (字节码生成库)框架,对代理对象类生成的 class 文件加载进来,通过修改其字节码生成子类来处理。

java集合框架

在这里插入图片描述
在这里插入图片描述
java集合框架主要包括collection(元素集合)和map(键值对映射)

ArrayList和vector的底层实现和区别

  • ArrayList 初始长度10,扩容为1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1);
  • vector 线程安全 ,初始长度10,扩容为2倍,

ArrayList和LinkedList的底层实现和区别

ArrayList底层使用object数组,初始长度为10,LinkedList底层使用双向链表结构,两者都是线程不安全的
ArrayList 增删慢,查询快,元素是连续存储的
LinkedList 增删快 ,查询慢
##java 数组

		int[] a={1,2,3,4,5,6,7,8,9};
        int[] b=Arrays.copyOf(a,5);
        int[] c=Arrays.copyOfRange(a,2,5);
        //b 1 2 3 4 5
        //c 3 4 5

ArrayList扩容机制

List<Integer> list=new ArrayList<>();

通过无参构造方法创建ArrayList时,实际初始化赋值的是一个空数组,当对数组进行添加元素时,才真正的分配容量
,添加第一个元素时,容量扩为10,之后每次扩充容量为之前的1.5倍。

int newCapacity =oldCapacity+(oldCapacity>>1);

hashmap的底层实现?扩容?是否线程安全?

hash是键值对的集合,通过key可以快速的取出value的值,可以快速的检索数据(接近o(1))
hash :高效 不可逆 散列
hashmap 懒加载机制
jdk7 之前hashmap是基于数组 和链表实现的,采用头插法
jdk8之后 数组+链表+红黑树 Node(key+value+next+hash)
散列表数组长度初始为16,
默认负载因子为0.75 ,扩容用,如果数组长度达到16*0.75=12时,将数组容量16扩容为之前的2倍
转红黑树条件:如果链表长度大于阈值8,(如果数组长度小于64,会先选择resize()方法对数组扩容,)将链表转化为红黑树,以减少搜索时间。
红黑树任何不平衡都会在3次旋转之内解决,降低了对旋转的要求,对于查询,插入,删除操作较多时,红黑树效率最好
HashMap 通过 key 的 hashCode 经过扰动函数处理过后(高16位与低16位异或)得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

	static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

可以在预制存储数据量的情况下,提前设置初始容量 初始容量=预知存储数据量/负载因子,可以减少resize操作,提高hashmap的效率

hashmap扩容为什么是2的n次幂

数组下标计算是(n-1)& hash, 如果n(length)是2的幂次,(n-1)&hash 等价于hash%length, 二进制&,相对于%能提高运算效率。

hashmap负载因子为什么是0.75

是一种时间和空间的权衡
1 空间利用率高,冲突多,查询效率低
0.5,时间效率提升,空间利用率降低

hashmap的put方法

  1. 根据key的hashcode的扰动函数得到hash值,通过(n-1)&hash得到存放的位置
  2. 如果数组下标元素为空,直接放到该位置
  3. 如果数组下标元素不空
    • 先判断节点类型是红黑树节点还是链表节点,若是红黑树节点,将key-value封装为node节点添加到红黑树中,判断红黑树中是否存在当前key,存在就更新value
    • 若是链表节点,遍历链表,若存在当前key,存在就更新value,否则就尾插法插入
    • 插入后判断是否扩容

HashMap源码中在计算hash值的时候为什么要右移16位?

高16位与低16位异或,为了混合原始哈希吗的高位与低位,加大了低位的随机性,而且混合后的低位参杂了高位的部分特征,高位的信息也被变相的保留下来。

java线程安全的有哪些?

  • vector: 比ArrayList多了个同步化机制(线程安全)
  • hashtable:比hashmap多了线程安全
  • concurrentHashMap:一种高效且安全的集合
  • stack 继承自vector

说一下ConcurrentHashMap的底层实现,它为什么是线程安全的?

  1. 底层采用分段的数组+链表实现,线程安全
  2. jdk1.7 分段锁,jdk1.8只锁定当前链表或者红黑树的首节点
  3. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
  4. 1.8 Node数组+链表/红黑树(treenode)采用 CAS 和 synchronized 来保证并发安全

HashMap和Hashtable的区别

  • hashmap线程不安全,可以存储null的key和value,初始容量16,数组+链表+红黑树
  • hashtable线程安全,不可以存储null的key和value,初始容量11 数组+链表

HashMap和TreeMap的区别?

hashmap中的元素没有顺序,treemap比hashmap多了按元素key值排序

Java多线程与并发编程高频问题

说说什么是线程安全?如何实现线程安全?

多个线程同时访问同一个对象,不考虑线程在运行环境下的调度与交替执行,不需要额外的同步,调用这个对象都会获得正确的结果,那该线程是线程安全的。
线程安全的三种方法:

  • 互斥同步 : 同步指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用,互斥同步有synchronized关键字或ReentrantLock等
  • 非阻塞同步 一种乐观并发策略 CAS
  • 无同步方案 ,ThreadLocal

线程不安全原因

  1. 线程是抢占式执行的
  2. 有些操作不是原子的
  3. 多个线程修改同一个变量(一对一修改,多对一读取,多对不同变量的修改,是安全的)
  4. 内存可变性
  5. 指令重排序 java编译器在编译代码时,会对指令进行优化,调整指令的先后顺序,保证原有的逻辑不变的情况下,提高程序的运行效率

JMM(java内存模型)

在并发过程中如何处理可见性,原子性,有序性三个特性建立的模型

  • 原子性:
    要么全部执行完毕,要么都不执行
  • 可见性:

一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)

  • 有序性
    有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令
    在这里插入图片描述
    happens-before 先行发生,是 Java 内存模型中定义的两项操作之间的偏序关系,如果操作A 先行发生于操作B,那么A的结果对B可见。
    内存屏障是被插入两个 CPU 指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障有序性的。

线程Run方法和start方法

调用start方法会启动线程并时线程进入就绪状态,然后自动执行run方法的内容; 直接执行run方法不会以多线程的方式执行,会将run方法当做main线程的普通方法执行。

说一下 synchronized 底层实现原理?(无锁-轻量级锁-偏向锁-重量级锁)

synchronized关键字可以解决多线程访问资源的的同步性,synchronized保证它所修饰的方法或者代码块在任意时刻只有一个线程执行。
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象

静态同步方法,锁是当前类的class对象

同步方法块,锁是当前类的class对象

单例模式

//饿汉模式
//优点:在类加载时完成实例化,避免线程同步
//缺点:不能达到懒加载效果,如果从始至终未使用过这个实例,会造成内存浪费
public class Singleton {
//私有化构造方法使得该类无法在外部通过new 进行实例化
    private Singleton(){}
    //准备一个类属性,指向一个实例化对象。 因为是类属性,所以只有一个
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
}

//懒汉式(双重校验)
public class Singleton{
	private Singleton(){}
	// volatile保证了内存可见性和禁止指令重排序
	private static volatile Singleton singleton;
	public static Singleton getInstance(){
		//先判断对象是否实例化,没有实例化进入加锁代码
		if(singleton == null){
			//类对象加锁
			synchronized(Singleton.class){
				if(singleton==null){
					singleton=new Singleton();
				}
			}
		}
		return singleton;
	}
}
/*uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
1.为 uniqueInstance 分配内存空间
2.初始化 uniqueInstance
3.将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行**/

synchronized 锁升级

无锁,偏向锁,轻量级锁,重量级锁
JVM中对象被分为对象头,实例数据,对齐填充组成,对象头由markword,元数据指针(指向类的指针),数组长度(数组对象有)

  • 无锁: 刚开始一个对象object,刚刚new出来,没有任何线程给它加锁。
  • 偏向锁:来了一个线程A,A给object上了个锁,上锁指的就是把object的对象头里的MarkWord的线程ID改为自己线程 ID 的过程。因为只有A一个线程,没有发生竞争,所以object加的是偏向锁。
  • 轻量级锁:线程B来了,尝试获取锁,发现锁已被占用,产生锁竞争,将偏向锁升级为轻量级锁。通过CAS去抢锁(比较当前锁标志位是否为释放,如果是将其修改锁定,就算抢到锁, 然后将MarkWord的线程ID改为自己线程 ID ),没抢到锁就会进行自旋,不停判断循环判断所能否被成功获取。(锁自旋是一种锁竞争机制,假设在一段时间内比如一个时钟周期内能获得锁,因此虚拟机会进行一次赌博,在这段时间类一直尝试加锁)。用短时间的忙等,换取线程在用户态和内核态之间切换的开销。
  • 重量级锁 :如果竞争加剧,很多线程抢一把锁,有线程自旋超过10次(可修改),或者自旋线程数超过CPU核数的一半,锁会膨胀微重量级锁。竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程。

synchronized 和 volatile 的区别是什么?

  • volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,禁止指令重排序,但不能保证数据的原子性。synchronized 关键字都能保证(可见性原子性,禁止指令重排序)。
    -volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

synchronized和ReentLock的区别是什么?

  • 相同点:
    1. 都是可重入锁
    2. 都保证了可见性与互斥性
    3. 都可用于控制多线程对共享对象的访问
  • 不同:
    1. ReentrantLock等待可中断
    2. synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,但是可以通过修改参数来实现公平锁。(公平与非公平皆可)
    3. sychronized是jvm级别的锁,reentrantlock是lock接口下实现类,是API层面的锁
    4. sychronized隐式获取锁和释放锁,reentrantlock是显示的

atomic原理

Atomic:
AtomicBoolean、AtomicInteger、tomicIntegerArray、AtomicReference、AtomicStampedReference
常用方法:
addAndGet(int)、getAndIncrement()、compareAndSet(int, int)
采用cas,一种硬件对并发的支持,无锁的非阻塞算法
V,A, B 当且仅当(V==A)时,将V更新为B,否则不会执行任何操作。
缺点: 循环时间长,只能保证一个变量的原子操作,ABA问题

java中的线程状态有哪些?线程间的通信方式有哪些?

java中线程周期分为

  • 新建new
  • 运行runnable
  • 阻塞blocked
  • 有限期等待 time waiting
  • 无限期等待waiting
  • 结束
    线程间的通信方式:
  • 互斥量 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问,,比如java中的synchronized关键词和各种lock都是这种机制。
  • 信号量,允许同一时刻内多个线程访问同一资源,控制同一时刻访问此资源的最大数量。
  • 事件,通过通知操作的方式保持多线程同步,还可以方便的的实现多线程的优先级比较操作。

sleep后进入什么状态,wait后进入什么状态?

sleep后进入time waiting状态,wait后进入waiting状态。

sleep和wait的区别?

sleep方法属于thread类,wait方法属于object类
sleep方法暂停执行的指定时间,让出cpu给其他线程,指定时间后恢复运行
sleep方法不会释放对象锁,wait会释放对象锁

wait为什么是数Object类下面的方法?

因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object。
释放资源实际是通知对象内置的monitor对象进行释放,而只有所有对象都有内置的monitor对象才能实现任何对象的锁资源都可以释放,又因为所有对象都继承自object对象,故wait是object下面的方法,就是通过wait()来通知对象内置的monitor对象释放,而且事实上因为这涉及对硬件底层的操作,所以wait()方法是native方法,底层是用C写的。

AQS(AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转换为悲观锁)

AQS(abstract queue synchronizaed) 抽象队列同步器,通过维护一个状态标志位state和一个先进先出(FIFO)的线程等待队列来实现一个线程访问共享资源的同步框架。
在这里插入图片描述
AQS原理:先给每个共享资源设置一个共享锁,线程在访问共享资源时,先获取共享锁,如果成功获取到共享锁,就访问共享资源,失败将其加入等待队列,等待下一次资源调度。
AQS定义了两种资源共享方式:独占式和共享式。

  • 独占式 只有一个线程能访问,具体的java实现由reentrantLock.
  • 共享式 多个线程同时执行,具体的由semaphore countdownlatch
    AQS只是一个框架,只定义了接口,具体资源的获取,释放交给自定义的同步器去实现。

不同的自定义同步器争取用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器在做处理。

java中的并发关键字

countdownlatch semaphore cylicbarrier

你使用过哪个AQS组件,有将其用于多线程编程吗?(给一个例题说一下思路或者直接写)

CAS

CAS compare and swap,比较和替换,是设计并发算法用到的一种技术,cas指令有三个操作数,内存位置,旧的预期值和准备设置的新值。CAS指令在执行时,当且仅当内存位置处的值与旧的预期值相等时,替换为新值,否则不替换。

CAS 带来的问题

  • ABA问题,循环时间开销大,只能保证一个共享变量的原子操作。
    解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。也可以使用携带类似时间戳的版本AtomicStampedReference。
  • 循环时间长开销大导致的性能问题,我们使用时大部分时间使用的是 while(true)方式对数据的修改,直到成功为止。优势就是相应极快,但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。

什么是乐观锁,什么是悲观锁?

悲观锁,认为对于同一数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为会修改,对于同一个数据的并发操作,悲观锁采取加锁的方式,悲观认为不加锁会出问题
乐观锁,与悲观锁相反,当数据提交更新时,才会真正对数据是否产生冲突进行监测,如果发生冲突,就返回给用户错误信息,由用户来决定如何去做,主要有两个步骤:冲突检测和数据更新。

Java中创建线程的方式有哪些?

java中创建线程的方式有4种

  1. 继承thread,重写run方法
  2. 重写runable接口,重写run方法
  3. 重写callable接口,重写call方法
  4. 使用线程池

线程池的好处?说几个Java中常见的线程池?说一下其中的参数和运行流程?

使用线程池可以降低资源消耗(反复创建线程是一件很消耗资源的事,利用已创建的线程降低线程创建和销毁开销),提高处理速度(当任务到达时,可以直接使用已有线程,不必等到线程创建完成再去执行)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
在这里插入图片描述

线程池有7大核心参数

corePoolSize:核心线程数

maximumPoolSize:线程池中最大线程数

keepAliveTime:多余空闲线程数的存活时间,当前线程数大于corePoolSize,并且等待时间大于keepAliveTime,多于线程或被销毁直到剩下corePoolSize为止。

TimeUnit unit: keepAliveTime的单位。

workQueue:阻塞队列,被提交但未必执行的任务。

threadFactory:用于创建线程池中工作线程的线程工厂,一般用默认的。

handler:拒绝策略,当堵塞队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)
在这里插入图片描述
线程池中的执行流程:
当线程数小于核心线程数时,使用核心线程数
线程数大于核心线程数,将多余的线程放入任务队列中
当任务队列阻塞时(满),启动最大线程数
当最大线程也到达后,启动拒绝策略
在这里插入图片描述

拒绝策略有哪些?

有四种拒绝策略

  1. ThreadPoolExecutor.AbortPolicy 默认拒绝策略,丢弃任务并抛出rejectexecutionexception异常
  2. ThreadPoolExecutor.DiscardPolicy静默丢弃,丢弃任务不抛出异常
  3. ThreadPoolExecutor.DiscardOldestPolicy丢弃最前面的任务,重新提交被拒绝的任务
  4. ThreadPoolExecutor.CallerRunsPolicy由调用线程处理该任务

线程池的参数如何确定呢?

一般要确定的核心线程数,最大线程数,任务队列,和拒绝策略,根据实际业务场景分为cpu密集型和IO密集型

  • cpu密集型,任务可一减少配置线程数,和cpu核数,使得每个线程都在执行任务
  • IO密集型,大部分线程阻塞,需要多配置线程数,2*cpu核数

Java中常见的阻塞队列有哪些?

  • ArrayBlockingQueue 典型数组实现的有界队列,基于数组实现
  • LinkedBlockingQueue 链表实现
  • SynchronousQueue 每一个put操作必须等待take操作,否则不能添加元素。同时它也支持公平锁和非公平锁。
  • PriorityBlockingQueue支持优先级排序的无界阻塞队列
  • DelayQueue 实现PriorityBlockingQueue的延迟获取的无界队列。具有“延迟”的功能。

ThreaLocal?

java中每个线程都有自己的专属本地变量,ThreadLocal类解决的是让每个线程绑定自己的值,存储每个线程私有数据。

  1. ThreadLocal是java提供的线程本地存储机制
  2. ThreadLocal底层是通过ThreadLocalMap实现的,key是ThreadLocal对象,value为需要缓存的值

ThreadLocal 会带来什么问题

ThreadLocal将自身作为key,值作为value存储到ThreadLocalMap中。
在这里插入图片描述

在线程池使用ThreadLocal会带来内存泄漏,当ThreadLocal对象使用完成后,应该对Entry对象进行回收,但是线程池中的线程不会回收,线程对象通过强引用指向ThreadLocalmap,ThreadLocalmap通过强引用指向entry对象,线程不被回收,entry对象不会被回收,从而出现内存泄漏。 解决办法:使用ThreadLocal之后,调用remove方法,手动清除entry对象。

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。【摘自JavaGuide】

什么是强软弱虚引用?

  1. 强引用是最普遍的引用,只要某个对象有个强引用与之关联,即使在内存不足的情况下,jvm宁愿抛出outofmemory也不会回收该对象(若要回收强引用,可以显示的将引用赋值为null)
  2. 软引用用来描述一些有用但并不是必需的对象,在Java中java.lang.ref.softreference类来表示,只有内存不足时才会回收。
  3. 只具有弱引用对象具有短暂的生命周期,垃圾回收器线程扫描他说在的区域过程中,一旦发现弱引用,直接回收内存(只要进行垃圾回收,无论内训是否充足,都会回收)。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  4. 虚引用(幻影引用)PhantomReference,虚引用不会对生存时间构成影响,无法通过虚引用来获取一个真实引用,唯一的用处,在对象GC时,收到系统通知。
    虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

高级并发编程艺术

https://www.processon.com/view/link/5d5fadaae4b06e49191eb2e7

Java虚拟机高频问题

介绍一下Java运行时数据区域,并说一下每个部分都存哪些内容?

在这里插入图片描述

java运行时数据区域有(堆,方法区)线程共享,(程序计数器,本地方法栈,虚拟机栈)线程私有

1. 方法区: 类信息(构造方法,接口定义),静态变量,常量,运行时常量池,编译后的代码都放在方法区
垃圾回收在此区域是比较少出现,但并不是数据进去方法区就永久存在了。
jdk8以后,永久代被彻底移除,有元空间取代,元空间使用的是直接内存。
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存 大小的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
2. 堆 实例变量,数组,字符串常量池在堆中
3. 虚拟机栈(生命周期与线程相同)Java中每个方法执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
4. 程序计数器 是当前的线程所执行字节码的行号指示器,保存下一条要执行的字节码指令
5. 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

程序计数器可以为空吗?

可以为空,当执行本地方法时

堆中是如何细分的

堆中可以细分为新生代和老年代,其中新生代又分为Eden区,From Survivor和To Survivor区,比例是8:1:1

内存泄漏和内存溢出

  • 内存泄漏:该释放的内存没有释放,会导致OOM(内存溢出)
  • 内存溢出:在申请内存时,没有足够的空间供其使用

内存泄漏时,如何定位问题代码

  • 静态对象,静态变量引用的对象生命周期和程序的生命周期一致,垃圾回收机制无法回收,使用完及时 赋值为 null。
  • 输入流,连接使用完之后没有关闭
  • 减少长生命周期的对象持有短生命周期的引用;

哪些区域会造成OOM

回答:除了程序计数器不会产生OOM,其余的均可以产生OOM

OOM出现的原因,出现了怎么办?出现OOM后堆空间真的没有空间了吗?

  1. 堆空间溢出
    一般由于==内存泄露或者堆的大小(大对象的分配)==设置不当引起,对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
  2. 永久代溢出
    即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
  3. 栈溢出
    不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

Java中对象的创建过程是什么样的?

  1. 当遇到new关键字时,根据指令参数在常量池定位类的符号引用,检查是否被加载连接初始化在这里插入图片描述
  2. 为新对象分配内存
  3. 分配到的内存空间初始化为0
  4. 需要对对象进行相关的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据信息
  5. 执行init方法

内存分配的策略有哪些?

java内存分配

  • 指针碰撞
    假设内存分配是规整的,用过的放在一边,没用的放在另一边,中间由指针隔开,分配时,将指针向空闲方向移动即可
  • 空闲列表
    假设内存分配是不规整的,虚拟机需要维护一个空闲列表,分配时将一块足够大的空闲内存分配给他。

对象头包含哪些信息?

虚拟机中对象包含两类信息:

  1. 存储对象自身运动所需数据,如hash码,GC分代年龄,线程持有的锁等
  2. 类型指针,对象指向类型元数据的指针

对象访问定位的方法又几种?

在这里插入图片描述

使用句柄好处是对象移动时,只改变reference中的实列数据指针,不改变reference地址。
在这里插入图片描述

直接内存地址,减少一次内存定位开销,速度快

判断对象已死

  1. 引用计数器 对象中添加一个引用计数器,引用一次,加一,引用失效减一,引用计数器为0时,对象不能再被使用。
  2. 可达性分析 根据GC ROOT作为跟节点,根据引用关系向下搜索,某个对象到GCroot没有任何链相连,则对象已死

GC root 可以是那些?

Java中可以作为GC Roots的比较多,分别有

  1. 虚拟机栈中引用的对象
  2. 方法区中静态引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈找中JNI引用的对象
  5. java虚拟机内部引用
  6. 所有被同步锁持有的对象

所有被标为GC的对象一定会被GC对象回收掉吗?

不一定,还有逃脱的可能,真正标记一个对象死亡至少经历两次标记的过程
如果对象进行可达性分析后没有与GC Roots相连,那么这是第一次标记,之后会在进行一次筛选,筛选的条件是是否有必要执行finalize()方法

垃圾回收算法有哪些?详细叙述一下

垃圾回收算法,标记清除,标记整理,标记复制

  1. 标记清除: 算法包括两个步骤,先标记要回收的对象,然后清除有标记的对象。 存在问题:效率问题(标记和清除的效率比较低),空间问题,可能导致较大对象没有空间容纳,提前导致GC
  2. 标记复制 将可用的空间分为两部分,每次将活着的对象移动到另外一部分。 存在问题:可用空间只有一半。当对象存活率高时,复制次数多,效率降低。
  3. 标记整理:将活着的对象移动到一边,其他部分全部清除(清理边界外内存)

新生代和老年代一般使用什么算法?

新生代,主要使用标记复制和标记整理,老年代主要使用标记清除

为什么新生代不使用标记清除算法?

在新生代,每次垃圾清除都有大批对象死亡,只有少量存活,就选用复制算法,只需付出少量对象的复制成本就完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

垃圾回收器有哪些?

垃圾回收器可以在新生代和老年代都有,在新生代有Serial、ParNew、Parallel Scavenge;老年代有CMS、Serial Old、Parallel Old;还有不区分年的G1算法。

CMS垃圾回收器的过程是什么样的?会带来什么问题?

  1. 初始标记 标记一下GC root能直接关联的对象,速度快,需要暂停其他工作线程
  2. 并发标记 GC和用户线程一起工作,执行GCroot跟踪标记过程,不需要暂停线程
  3. 重新标记 在并发标记中部分状态发生变化,对其重新标记并暂停工作线程
  4. 并发清除 清理删除标记判断已死亡的对象
    带来的问题:对处理器资源敏感,无法处理浮动垃圾,基于标记清除会产生大量空间碎片

G1垃圾回收器的改进是什么?相比于CMS突出的地方是什么?

G1 垃圾回收器摒弃了分代的概念,将内存划分为几个独立的区域,维护一个优先级列表,根据系统的最长垃圾回收时间,优先回收垃圾最多的区域(G1是一种可控的STW算法,GC收集器和GC调优目标就是减少STW(停顿stop the world)的时间和次数)
G1优点 基于标记整理算法,不产生垃圾碎片,可以精确控制停顿时间,不牺牲吞吐量的前提下实现垃圾回收

现在jdk默认使用的是哪种垃圾回收器?

1.7 Parallel Scavenge(新生代)+Parallel Old(老年代)
1.8 Parallel Scavenge(新生代)+Parallel Old(老年代)
1.9 G1

jvm堆内存分配策略是什么样的?

对象优先在Eden分配,如果说Eden内存不足,会发生Minor GC/Young GC
大对象直接进入老年代(避免大对象分配内存时由于分配担保机制带来的复制而降低效率),大对象,需要大量连续内存空间的对象,比如长字符串和大型数组。
在这里插入图片描述
在这里插入图片描述

  1. 长期存活的对象进入老年代(默认为15) 也可通过MaxTenuringThreshold调整
  2. 如果Survivor空间中相同年龄对象总和个数大于survivior的一半,年龄大于等于该年龄的对象直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。

内存溢出与内存泄漏的区别?

内存溢出:超出可容纳的最大内存,实实在在的内存空间不足导致的
内存泄漏:该释放的内存没有释放

jvm调优了解过吗?常用的命令和工具有哪些?

Linux中有top、vmstat、pidstat,jdk中的jstat、jstack、jps、jmap等

jstack和jsp的区别是什么?

jstack (stack trace for java) 生成当前时刻的线程快照

线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照主要目的是定位线程出现的长时间停顿的原因,比如线程死锁,死循环,请求外部资源导致的长时间等待。
在代码中可以用java.lang.Thread类的getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能

jps 列出当前机器正在运行的虚拟机进程

-p :仅仅显示VM 标示,不显示jar,class, main参数等信息.
-m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整package名称或jar完整名称.
-v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数

虚拟机中的加载机制(类加载)?

在这里插入图片描述
类加载包括 加载,验证,准备,解析,初始化,使用和卸载

  • 加载:读取Class文件,根据Class文件描述创建对象的过程
  • 验证:确保class文件符合当前虚拟机的要求
  • 准备: 在方法区为类变量分配内存,设置类中变量的初始值
  • 解析:jvm将会将常连接池中符合引用替换为直接引用
  • 初始化: 执行类构造器方法对类进行初始化

类加载器有哪些?

启动类加载器: 最顶层加载器,由c++实现,%JAVA_HOME%/lib目录下的 jar 包和类
扩展类加载器 主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类
应用类加载器 面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类

双亲委派机制

在这里插入图片描述
一个类在收到加载请求后不会尝试自己去加载,委派其父类去完成,一直委派到启动类加载器,若能加载就加在,不能加载就向下委派给子类去加载,知道加载成功

如何打破双亲委派机制?

重写一个类继承classloader,并重写loadclass方法。(tomcat不支持双亲委派机制)

高频问答

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值