自己根据经验整理的一些面试题,希望能给帮助大家,祝各位都能找到满意的工作,加油!!!
内容比较多,整理了一部分,更多内容请关注公众号小程序或者回复面试题获取
目录
6.String,StringBuffer 和 StringBuilder 的区别?
9.synchronized 和 ReentrantLock 的区别?
2.BeanFactory 和 ApplicationContext 的区别?
2.分库分表中间件有哪些(mycat、sharding-jdbc)
2. Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么
3. Kafka中的HW、LEO、LSO、LW等分别代表什么?
JAVA 基础
基础/集合
1.ArrayList/LinkedList有什么区别?
1、数据结构: 在数据结构上,ArrayList 和 LinkedList 都是 “线性表”,都继承于 Java 的 List 接口。另外 LinkedList 还实现了 Java 的 Deque 接口,是基于链表的栈或队列,与之对应的是 ArrayDeque 基于数组的栈或队列;
2、线程安全: ArrayList 和 LinkedList 都不考虑线程同步,不保证线程安全;
3、底层实现: 在底层实现上,ArrayList 是基于动态数组的,而 LinkedList 是基于双向链表的。
遍历速度: 数组是一块连续内存空间,基于局部性原理能够更好地命中 CPU 缓存行,而链表是离散的内存空间对缓存行不友好;
访问速度: 数组是一块连续内存空间,支持 O(1) 时间复杂度随机访问,而链表需要 O(n) 时间复杂度查找元素;
添加和删除: 如果是在数组的末尾操作只需要 O(1) 时间复杂度,但在数组中间操作需要搬运元素,所以需要 O(n)时间复杂度,而链表的删除操作本身只是修改引用指向,只需要 O(1) 时间复杂度(如果考虑查询被删除节点的时间,复杂度分析上依然是 O(n),在工程分析上还是比数组快);
内存消耗: ArrayList 在数组的尾部增加了闲置位置,而 LinkedList 在节点上增加了前驱和后继指针
2.HashMap的底层实现-详解(jdk7&jdk8)
数据结构
jdk1.7 :数组 + 链表(单向)
jdk1.8 :数组+链表(单向)+红黑树
概念:
数组:一段连续的节点组成的内存区域,在内存中连续存储
链表:一段非连续的节点组成的存储结构,分为单向链表(最后一个节点的next 指向null)和双向链表(两个指针,pre 指向上一个节点,next 指向下一个节点,第一个节点的pre 指向null,最后一个节点的next 指向null)。
单向循环链表:最后一个节点next 指向head
双向循环链表:最后一个节点next 指向head,第一个节点pre 指向最后一个节点,linkedList 采用的就是这种
而hashMap 采用单向链表就是为了避免循环引用
jdk 1.7 中的数据存储结构:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
}
put 操作
1.若数组不存在,会初始化一个默认容量(1<<4=16) 的一个数组
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
此处没有直接写16(10000),用的是位移,是为了增加效率
1.1. 由于数据长度为16,所以向数组中放入数据时,数组的下标index 应该为0-15。
- 取模:用key.hashCode%16 取模得到,但是取模运算比较耗时
- 位运算(& 与):所以采用位运算与 (n - 1) & hash(直接对内存数据进行运算,不需要转成十进制,1&1 =1)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
为了让key更均匀的分布,对hash 做异或 ^,在高低16位
hash = key.hashCode^key.hashCode >>>16
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
初始容量和扩容为什么一定是2的整数次幂(面试点)
和与的计算有关,因为与操作是对应位置都为1 时,计算结果才为1,当hashmap 的容量为2的n 次幂时,n-1 的二进制才是1111***111,这样和添加的元素hash 与操作时才能更充分的散列,减少hash 碰撞,使添加的元素更均匀的分配的hashmap上。
2.如果存在,查找key 如果有值,替换原来的,并返回老的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
如果没有,将新插入的节点放到尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
头插法(1.7)和尾插法(1.8)的区别(面试点)
- 头插法:从链表头部插入,多线程会产生闭环,插入慢
- 尾插法:从链表尾部插入,不会产生闭环,插入快。
2.1 instanceof 方法会判断节点的类型是否为tree
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
- 链表转红黑树:因为树节点所占空间是普通节点的两倍,所以只有当节点足够多(长度大于8,泊松分布)的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins.
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
3.2 转红黑树条件2 数组的长度大于64
static final int MIN_TREEIFY_CAPACITY = 64;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
3.3 当长度小于6时,在转为链表
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
扩容 resize
当数组的长度大于 默认值16* 加载因子0.75(泊松分布)时会扩容 ,最大值为2的30次幂
if (++size > threshold)
resize();
扩容完成后,新数组的长度为老数组的2倍,新数组的下标为老数组的下标+计算下标
hashmap 容量的设置(面试点)
当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。
3.== 和 equals 比较有什么区别?
而对于 "==" 操作符来说
(1). 基本数据类型:比较的是两个基本数据类型的值是否相等,比如两个整型值是否相等。
(2). 引用数据类型:比较的是两个引用对象的引用地址是否一样,比如说新建了两个对象,比较的是两个对象的引用地址是否一样。
对于 "equals" 来说
(1).没有重写equals方法
Object中的equals()方法用于判断this和obj本身的值是否相等,即用来判断调用equals方法的对象和形参obj所引用的对象是否是同一对象。所谓同一对象就是指两个对象是否指向了内存中的同一块存储单元,如果this和obj指向的是同一块内存单元,则返回true;反之,则返回false。如果没有指向同一内存单元,即便是内容完全相等,也会返回false。所以 Object类中equals()方法与"=="操作符其实是等效的,都是用于比较两个对象的引用地址是否相等。
(2)重写了equals方法
String类中equals()方法用于比较两个字符串是否相同。如果两个字符串的地址相同,则直接返回true;如果两个字符串的地址不同,则进一步判断字符串的内容是否相同,如果字符串内容完全相同,也会返回true。
4.Java 到底是值传递还是引用传递?
引用传递(pass by reference)是指在调用方法时将实际参数的地址直接传递到方法中,那么在方法中对参数所进行的修改,将影响到实际参数。
值传递(pass by value)是指在调用方法时将实际参数拷贝一份传递到方法中,这样在方法中如果对参数进行修改,将不会影响到实际参数。
Java都是“值传递”,关键看这个值是什么,简单变量就是复制了具体值,引用变量就是复制了地址。字符串是不可变的,不会被修改
① 当传的是基本类型时,传的是值的拷贝,对拷贝变量的修改不影响原变量;
② 当传的是的引用类型时,传的是引用地址的拷贝,但是拷贝的地址和真实地址指向的都是同一个真实数据,因此可以修改原变量中的值。
一个方法中的局部变量是存在栈中的,
① 如果是基本类型的变量则直接存的是这个变量的值;
② 如果是引用类型的变量则存的是值的引用地址,指向堆中具体的对象。
5.String 的底层实现是怎样的?
1.String类由final修饰,不可以被继承
2.底层是由char数组实现的
3.value用final修饰,不能修改value的引用地址(value不可变)
4.private修饰和成员变量没有提供setter接口,保证了不可以通过外部接口来修改String的值
5.在JDK9中,将底层的char[]数组改为了byte[]数组存储。
原因:char类型是2字节的,使用 char[] 来表示 String 就导致了即使 String 中的字符只用一个字节就能表示,也必须占用两个字节。但是在实际使用中,只用单字节字符的频率远高于双字节字符,节省字符串的占用空间。
仅仅将char[]数组存储修改为byte[]数组存储是远远不够的,jdk9中还配合了 Latin-1 的编码方式的编码方式( Latin-1:用单字节表示字符)。对于双字节字符使用UTF16的编码方式表示。因此在jdk9的String源码中引入了coder字段区分编码方式。
6.String,StringBuffer 和 StringBuilder 的区别?
1. String类是不可变的,每次对String对象进行修改都会创建一个新的String对象,因此在需要对字符串进行大量修改的场景下,使用String类会产生很多的开销。
2. StringBuffer和StringBuilder类是可变的,可以对其进行修改,而不会创建新的对象。
3. StringBuffer类是线程安全的,而StringBuilder类不是线程安全的。
4. 在单线程环境下,StringBuilder类的性能比StringBuffer类更高。
在实际开发中,应该根据具体的场景选择适合的字符串处理类。如果需要对字符串进行大量修改,并且在多线程环境下使用,应该使用StringBuffer类。如果在单线程环境下需要对字符串进行大量修改,应该使用StringBuilder类。如果字符串不需要修改,应该使用String类。
7.接口和抽象类有什么区别?
相同之处:
1:都包含抽象方法,都不能被实例化
那么接口为什么不能被实例化呢?原因是:接口的定义和类很相似,分为接口声明和接口体两个部分,接口中能够定义的成员只有常量和抽象方法两种(或者两选一),不能包含构造方法,由于接口中只包含抽象方法,这就导致了它不能被实例化的原因和抽象类是相似的
2:他们的对象进行实例化操作的方法相同
无论是接口还是抽象类,虽然自身不能进行实例化操作,但由于抽象类和接口都是引用数据类型,可以声明抽象类和接口类型的引用变量,并将子类的对象实例赋给抽象类和接口变量,因此它们的对象都可利用其自身的多态性向上转型实现对接口或抽象类的实例化操作
不同之处:
1:关键字不同:
对于接口,使用interface修饰接口,它的子类使用implements关键字实现该接口,而对于抽象类,使用abstract用来修饰抽象类,它的子类使用extends关键字继承该抽象类
2:组成不同
在抽象类中可以定义常量,变量,抽象方法,普通方法,构造方法,而对于接口,在java8之前,接口中只能定义抽象方法和全局常量,java8之后,接口中可以定义静态方法,但静态方法必须有方法体。
3:权限不同
抽象类可以使用各种权限,而接口的权限只能是public
4:关系不同
一个抽象类可以实现多个接口,但接口不能继承抽象类,可以继承多个接口
5:局限性
由于java单继承的限制,一个子类只能继承一个抽象类,但一个子类可以实现多个接口
8.对象克隆浅拷贝和深拷贝的区别?
深拷贝(Deep Copy):
适用场景:
当源对象包含引用类型的属性时,如果需要复制对象及其子对象的所有属性,而不仅仅只是复制引用,就需要使用深拷贝。
当希望修改副本对象的属性不影响原始对象时,需要使用深拷贝。
工作原理:
深拷贝将源对象及其关联的全部对象进行递归复制,每个对象都拥有独立的内存空间,修改副本对象不会影响原始对象。
实现方式:
使用递归或者拷贝构造函数来复制对象及其子对象的属性。
示例场景:
复制复杂对象的副本,使其成为独立的个体,例如:拷贝一个包含集合、嵌套对象等的数据结构。
对象图的克隆,当原对象包含子对象,并且对子对象的修改不应该影响原对象时。
浅拷贝(Shallow Copy):
适用场景:
当源对象的属性全为基本数据类型或者不可变对象,并且不需要复制引用类型的属性时,可以使用浅拷贝。
当希望修改副本对象的属性同时影响原始对象时,可以使用浅拷贝。
工作原理:
浅拷贝只复制对象及其引用,而不复制引用指向的实际对象,新旧对象将共享同一个引用对象。修改副本对象会影响原始对象。
实现方式:
通常使用对象的 clone() 方法来进行浅拷贝。
示例场景:
快速创建对象副本,以便在某些操作中对其进行修改,同时保留原始对象。
在某些情况下,共享一部分数据以节省内存和提高性能
9.Java 反射机制的优缺点?
什么是反射
AVA反射机制是在运行状态中,对于任意一个类,都能够获取这个类的所有属性(属性的权限修饰符,数据类型,变量名等)和方法(方法名,返回值,参数等),构造器,运行时类实现的接口,所在的包,注解等,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
反射的优缺点:
1、优点:在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
2、缺点:
(1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
(2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
10.Java创建对象有几种方式?
1.使用new关键字
这是最常见的创建对象的方式。使用 new 关键字,调用对象的构造方法来创建一个新的实例。
2.使用Class类的newInstance()方法
Java中的每个类都有一个名为 Class 的类对象,可以使用它的 newInstance() 方法创建类的实例。这种方式要求类必须有一个无参的构造方法。
3.使用Constructor类的newInstance()方法
Java的反射机制允许在运行时动态地获取类的信息并操作类。通过 Class 类的 getConstructor() 和 newInstance() 方法可以创建对象。这种方式可以使用带参的构造方法。
4.使用clone()方法
需要实现Cloneable,对象拷贝是创建对象的一种方式,它通过复制一个已有对象的值来创建一个新的对象。被拷贝的类需要实现 Cloneable 接口,并重写 clone() 方法,浅clone()不会调用构造方
法。
5 反序列化
需要实现Serializable,反序列化是将对象从字节流(如文件、网络传输等)转换回内存中的对象的过程。在进行反序列化时,Java会使用特定的机制创建对象,并将字节流中的数据填充到对象中。
反序列化创建对象的原理如下:
类加载:在进行反序列化之前,Java需要加载类的定义。如果已加载的类与序列化数据中的类完全匹配,则直接使用已加载的类。否则,Java会尝试根据序列化数据中的类名去加载类定义。
对象实例化:一旦类定义加载完成,Java使用类的构造函数创建一个新的对象实例。通常情况下,会调用类的无参构造函数来创建对象。如果类中没有无参构造函数,或者无法访问无参构造函数(如私有构造函数),则可能会导致反序列化失败。
数据填充:在对象实例化后,Java将会将序列化数据中的字段值逐个填充到对象的对应字段中。这个过程会使用反射来访问对象的字段,并根据字段的类型和值进行填充。
如果是文件可以使用FileInputStream和ByteArrayOutputStream的原理一样。
jvm 内存
1.JVM 内存区域分类哪些?
(1)虚拟机栈
(2)本机方法栈
(3)程序计数器
一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
线程共享:
(1)堆
堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间
创建对象时,该对象首先会在Eden区域分配空间,在一次新生代的垃圾回收之后,如果对象还存活,则会进入Survivor区的S0或者S1,并且这个对象的年龄会加1(在对象头中记录对象的年龄),当在Survivor区中的对象年龄增加到一定程度后(默认是15岁),就会被转移到老年代中。对象转移到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold来设置。
(2)方法区(1.8之前叫永久代,1.8之后叫元空间)
方法区的作用是保存一个类的信息,当我们需要使用一个类的时候,需要先加载并解析这个类的Class文件,获取类的信息,其中类的信息包括字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
(3)直接内存(在Java运行时内存区域之外)
2.jvm调优实战
以下是整理自网络的一些JVM调优实例:
网站流量浏览量暴增后,网站反应页面响很慢
1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。
2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进行GC 次数频率非常高,GC所占用的时间非常长,所以基本推断就是因为GC频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。
3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁GC,所以这里问题在于新生代内存太小,所以这里可以增加JVM内存就行了,所以初步从原来的2G内存增加到16G内存。
4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。
5、问题推测:练习到是之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次GC的时间变长从而导致间接性的卡顿。
6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很高,但是花费在FGC上的时间是非常高的,根据GC日志 查看到单次FGC的时间有达到几十秒的。
7、解决方案: 因为JVM默认使用的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次GC时间过长,所以需要更换并发类的收集器,因为当前的JDK版本为1.7,所以最后选择CMS垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。
后台导出数据引发的OOM
**问题描述:**公司的后台系统,偶发性的引发OOM异常,堆内存溢出。
1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从4G调整到8G。
2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。
3、VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。
4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。
5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。
6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。
7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。
单个缓存数据过大导致的系统CPU飚高
1、系统发布后发现CPU一直飚高到600%,发现这个问题后首先要做的是定位到是哪个应用占用CPU高,通过top 找到了对应的一个java应用占用CPU资源600%。
2、如果是应用的CPU飚高,那么基本上可以定位可能是锁资源竞争,或者是频繁GC造成的。
3、所以准备首先从GC的情况排查,如果GC正常的话再从线程的角度排查,首先使用jstat -gc PID 指令打印出GC的信息,结果得到的GC 统计信息有明显的异常,应用在运行了才几分钟的情况下GC的时间就占用了482秒,那么问这很明显就是频繁GC导致的CPU飚高。
4、定位到了是GC的问题,那么下一步就是找到频繁GC的原因了,所以可以从两方面定位了,可能是哪个地方频繁创建对象,或者就是有内存泄露导致内存回收不掉。
5、根据这个思路决定把堆内存信息dump下来看一下,使用jmap -dump 指令把堆内存信息dump下来(堆内存空间大的慎用这个指令否则容易导致会影响应用,因为我们的堆内存空间才2G所以也就没考虑这个问题了)。
6、把堆内存信息dump下来后,就使用visualVM进行离线分析了,首先从占用内存最多的对象中查找,结果排名第三看到一个业务VO占用堆内存约10%的空间,很明显这个对象是有问题的。
7、通过业务对象找到了对应的业务代码,通过代码的分析找到了一个可疑之处,这个业务对象是查看新闻资讯信息生成的对象,由于想提升查询的效率,所以把新闻资讯保存到了redis缓存里面,每次调用资讯接口都是从缓存里面获取。
8、把新闻保存到redis缓存里面这个方式是没有问题的,有问题的是新闻的50000多条数据都是保存在一个key里面,这样就导致每次调用查询新闻接口都会从redis里面把50000多条数据都拿出来,再做筛选分页拿出10条返回给前端。50000多条数据也就意味着会产生50000多个对象,每个对象280个字节左右,50000个对象就有13.3M,这就意味着只要查看一次新闻信息就会产生至少13.3M的对象,那么并发请求量只要到10,那么每秒钟都会产生133M的对象,而这种大对象会被直接分配到老年代,这样的话一个2G大小的老年代内存,只需要几秒就会塞满,从而触发GC。
9、知道了问题所在后那么就容易解决了,问题是因为单个缓存过大造成的,那么只需要把缓存减小就行了,这里只需要把缓存以页的粒度进行缓存就行了,每个key缓存10条作为返回给前端1页的数据,这样的话每次查询新闻信息只会从缓存拿出10条数据,就避免了此问题的 产生。
CPU经常100% 问题定位
问题分析:CPU高一定是某个程序长期占用了CPU资源。
1、所以先需要找出那个进行占用CPU高。
top 列出系统各个进程的资源占用情况。
2、然后根据找到对应进行里哪个线程占用CPU高。
top -Hp 进程ID 列出对应进程里面的线程占用资源情况
3、找到对应线程ID后,再打印出对应线程的堆栈信息
printf "%x\n" PID 把线程ID转换为16进制。
jstack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。
4、最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。
查看是否有线程长时间的watting 或blocked
如果线程长期处于watting状态下, 关注watting on xxxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。
内存飚高问题定位
分析: 内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。
1、先观察垃圾回收的情况
jstat -gc PID 1000 查看GC次数,时间等信息,每隔一秒打印一次。
jmap -histo PID | head -20 查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。
如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。
2、导出堆内存文件快照
jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆内存信息到文件。
3、使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。
3.你怎么理解强、软、弱、虚引用?
(1)强引用
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。类似
“Object obj = new Object()”
这类的引用。
当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收器回收的,即使该对象永远不会被用到也不会被回收。
当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现了 OOM 也不会对该对象进行回收,打死都不收。因此强引用有时也是造成 Java 内存泄露的原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集器回收。(具体回收时机还要要看垃圾收集策略)。
(2)软引用
软引用是一种相对强引用弱化了一些的引用,需要用
java.lang.ref.SoftReference
类来实现,可以让对象豁免一些垃圾收集。
软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
对于只有软引用的对象来说:当系统内存充足时它不会被回收,当系统内存不足时它才会被回收。
(3)弱引用
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
弱引用需要用
java.lang.ref.WeakReference
类来实现,它比软引用的生存期更短。
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存。
(4)虚引用
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。
虚引用,顾名思义,就是形同虚设,与其他几种引用都不太一样,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
虚引用需要
java.lang.ref.PhantomReference
来实现。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(RefenenceQueue)联合使用。
虚引用的主要作用是跟踪对象垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。
PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 GC 回收,用来实现比 finalization 机制更灵活的回收操作。
换句话说,设置虚引用的唯一目的,就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理。
4.什么是双亲委派机制
双亲委派机制(Parent-Delegate Model)是Java类加载器中采用的一种类加载策略。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。
启动类加载器(Bootstrap ClassLoader): 负责加载 %JAVA_HOME%/jre/lib 目录下的核心Java类库如 rt.jar、charsets.jar 等。
扩展类加载器(Extension ClassLoader): 负责加载 %JAVA_HOME%/jre/lib/ext 目录下的扩展类库。
应用类加载器(Application ClassLoader): 负责加载用户类路径(ClassPath)下的应用程序类。
1.避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
2.可以保证安全性。因为BootstrapClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.String,那么这个类是不会被随意替换的。
5.什么是 JVM 内存模型?
并发的三大特性,其中原子性问题可以通过加锁的方式解决,可见性是因为缓存导致,有序性是因为编译优化指令重排序导致,那么是不是可以按需禁用缓存以及编译优化, 顺着这个思路,就提出了JAVA内存模型(JMM)规范。
Java 内存模型是 Java Memory Model(JMM),本身是一种抽象的概念,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的 每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝 线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量 线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成
6.一次完整的 GC 流程是怎样的?
对象的正常流程:Eden 区 -> Survivor 区 -> 老年代。
新生代GC:Minor GC;老年代GC:Full GC,比 Minor GC 慢10倍。
【总结】:内存区域不够用了,就会引发GC,JVM 会“stop the world”,严重影响性能。Minor GC 避免不了,Full GC 尽量避免。
【处理方式】:保存堆栈快照日志、分析内存泄漏、调整内存设置控制垃圾回收频率,选择合适的垃圾回收器等
7.JVM 如何判断一个对象可被回收?
有一种引用计数法,可以用来判断对象被引用的次数,如果引用次数为0,则代表可以被回收。
这种实现方式比较简单,但对于循环引用的情况束手无策,所以 Java 采用了可达性分析算法。
即判断某个对象是否与 GC Roots 的这类对象之间的路径可达,若不可达,则有可能成为回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
在 Java 中,可作为 GC Roots 的对象包括以下几种:
虚拟机栈(本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
8.常用的垃圾收集器有哪些?
垃圾收集器根据发展实践可以分为:
串行
吞吐量优先
响应时间优先
(1)Serial收集器是最基础、历史最悠久的收集器。它是用于新生代的垃圾收集器。
这个收集器是一个单线程工作的收集器,“单线程”不仅仅代表了这个收集器只会使用一个处理器或一个收集线程去进行垃,Serial 收集器的优点很简单,那就是简单高效,在内存资源受限的环境中,它是所有收集器里额外内存消耗最小的,对于单核处理器或核心数较小的处理器来说,Serial 收集器没有线程交互的开销,专心做垃圾收集因此保证了收集效率。
(2)ParNew 收集器实际上就是 Serail 收集器的多线程版本,除了同时使用多线程进行垃圾回收之外,其他的任何行为包括所有控制参数、收集算法等都与 Serail 收集器一样,并无太多创新之处
(3)Paraller Scavenge 收集器是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
该垃圾收集器常被称为“吞吐量优先收集器”
Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数
(4)Serial Old收集器是Serial收集器的老年代版本,同样也是一个单线程收集器,使用标记-整理算法
(5)Parller Old收集器是Parller Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
(6)CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网网站或者基于浏览器的 B/S 系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。
(7)G1收集器面向堆内存任何部分来组成回收集 (Collection Set,一般简称 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式。
G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间或者老年代空间。
9.为什么会发生内存泄漏?
当应用程序持有不再需要的对象引用时,就会发生 Java 内存泄漏。这些意外的对象引用阻止内置的 Java 垃圾收集机制释放这些对象消耗的内存,最终导致致命的OutOfMemoryError。
简而言之,内存泄漏是- 不再需要的对象引用,仍然存在于 HEAP 内存中,垃圾收集器无法删除它们。
发生内存泄漏的最常见场景:
(1).没有正确使用静态成员。
(2).未关闭的资源。
(3).将没有 hashCode() 和 equals() 的对象添加到 HashSet 中。
(4).过多的会话对象。
(5).自定义数据结构编写不当。
多线程
多线程/线程池
1.线程创建的方式有哪些?
1.继承Thread类
2.实现Runnable接口
3.使用Callable接口
4.通过线程池ExecutorService
2.线程的五种状态?
新建状态(New)
当用new操作符创建一个线程时,例如new Thread(),线程还没有开始运行,此时线程处于新建状态。
就绪状态(Runnable)
线程对象被创建后,当线程对象调用start()方法,从而来启动该线程。处于就绪状态的线程,随时可能被CPU调度执行。
运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。需要注意的是,线程只能从就绪状态进入到运行状态
阻塞状态(Blocked)
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
死亡状态(Dead)
run()方法执行结束,线程正常退出。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
3.线程池的参数有哪些?
4.线程池的优势?
5.ThreadLocal 是什么?有什么用?
ThreadLocal 是一个本地线程副本变量工具类。
主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
7.Java 线程数过多会造成什么异常?
1、内存资源耗尽
Java 中每个线程都会占用一部分内存空间,当线程数过多时,会导致系统内存资源的消耗增加。如果系统内存无法满足所有线程所需的内存,则会引发 OutOfMemoryError 异常,在这种情况下,系统很可能会崩溃或死锁。
2、CPU 资源利用率降低
过多的线程数会使 CPU 在调度线程时的负担增加。因为在任何时刻,CPU 只有一个核心可以执行线程代码,当线程数过多时,CPU 在不停地切换线程上下文,导致 CPU 利用率低下,从而降低系统性能。
3、死锁和竞争
线程数过多还会影响资源竞争和死锁的出现。线程之间的竞争会在处理临界区时产生冲突。如果临界区没有受到充分保护,那么多个线程可能会试图同步访问共享资源,这样就会造成数据覆盖、混乱和丢失等问题,从而导致应用程序崩溃或死锁。
4、上下文切换成本增加
线程之间的上下文切换是非常耗费资源的操作。当线程数过多时,系统会不停地进行线程之间的上下文切换,这样会使系统效率下降,并且导致 CPU 资源被浪费,影响整个应用程序的执行性能。
为保证系统运行的高效和稳定,我们最好要控制好线程数,避免过度使用线程。一般情况下,线程数超过 CPU 核心数的两倍就需要警惕了。
8.线程池大小设置?
般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。
CPU 密集型任务:这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务:这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
那么碰上一些常规的业务操作,比如,通过一个线程池实现向用户定时推送消息的业务,我们又该如何设置线程池的数量呢?
此时我们可以参考以下公式来计算线程数:
线程数 =N(CPU 核数)*(1+WT(线程等待时间)/ST(线程时间运行时间))
9.execute和submit的区别?
1、execute和submit的区别
提交任务的类型:
execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务
submit既能提交Runnable类型任务也能提交Callable类型任务。
异常:
execute会直接抛出任务执行时的异常,可以用try、catch来捕获,和普通线程的处理方式完全一致
submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
返回值:
execute()没有返回值
submit有返回值,所以需要返回值的时候必须使用submit
10.线程run和start的区别?
在Java中,线程的 run() 和 start() 方法有以下几个区别:
1. 定义位置不同
run()方法是Thread类中的一个普通方法,它是线程中实际运行的代码,线程的代码逻辑主要就是在run()方法中实现的。
start()方法是Thread类中的一个启动方法,它会启动一个新的线程,并在新的线程中调用run()方法。
2. 执行方式不同
直接调用run()方法,会像普通方法一样在当前线程中顺序执行run()方法的内容,这并不会启动一个新的线程。
调用start()方法会创建一个新的线程,并在新的线程中并行执行run()方法的内容。
3. 线程状态不同
当我们调用start()方法启动一个新线程时,该线程会进入就绪状态,等待JVM调度它和其他线程的执行顺序。而当我们直接调用run()方法时,则会在当前线程中执行,不会产生新的线程。
因此,在Java多线程编程中,我们应该始终使用start()方法来启动新线程,而不是直接调用run()方法。
11.线程死锁怎么产生?怎么避免?
1. 死锁的定义
在 Java 中,死锁(Deadlock)情况是指:两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。线程竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西
2.如何避免产生死锁
其实产生死锁也不是那么容易的,需要满足四个条件,才会产生死锁。
互斥,也就是多个线程在同一时间使用的不是同一个资源。
持有并等待,持有当前的锁,并等待获取另一把锁
不可剥夺,当前持有的锁不会被释放
环路等待,就是两个线程互相尝试获取对方持有的锁,并且当前自己持有的锁不会释放。
我们只需要让其中一个条件不成立,那么就可以避免死锁问题的产生。一般最常见的解决方式就是使用资源有序分配法,来使环路等待条件不成立。
环路等待就是我们刚开始代码演示的那种情况,两个线程互相尝试获取对方的锁,但是他们两个都不会释放自己的锁,这样就会陷入一个无限循环等待,这种情况就是环路等待。
资源有序分配法其实很简单,就是把线程获取资源的顺序调整为一致的即可,资源可以理解为代码示例中的锁。
12.ThreadLocal使用场景有哪些?
场景一:用户登录态上下文
每条线程都需要存取一个同名变量,但每条线程中该变量的值均不相同。
ThreadLocal内存泄露:
ThreadLocal.ThreadLocalMap.Entry中的key是弱引用的,也即是当某个ThreadLocal对象不存在强引用时,就会被GC回收,但是value是基于强引用的,所以当key被回收,但是value还存在其他强引用时,就会出现内存的泄露情况,
在最新的ThreadLocal中已经做出了修改,即在调用set、get、remove方法时,会清除key为null的Entry,但是如果不调用这些方法,仍然还是会出现内存泄漏 ,所以要养成用完ThreadLocal对象之后及时remove的习惯。
数据库链接
各个层参数传递
13.CountDownLatch 有什么用?
CountDownLatch是一个同步工具类,它通过一个计数器来实现的,初始值为线程的数量。每当一个线程完成了自己的任务,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程就可以恢复执行任务。
CountDownLatch(int count):count为计数器的初始值(一般需要多少个线程执行,count就设为几)。
countDown(): 每调用一次计数器值-1,直到count被减为0,代表所有线程全部执行完毕。
getCount():获取当前计数器的值。
await(): 等待计数器变为0,即等待所有异步线程执行完毕。
boolean await(long timeout, TimeUnit unit): 此方法与await()区别:
①此方法至多会等待指定的时间,超时后会自动唤醒,若 timeout 小于等于零,则不会等待
②boolean 类型返回值:若计数器变为零了,则返回 true;若指定的等待时间过去了,则返回 false
1. 某个线程需要在其他n个线程执行完毕后再向下执行
2. 多个线程并行执行同一个任务,提高响应速度