本文题目来自于牛客上面的面经分享,会贴上来源帖子
本次题目来源:感谢分享
个人想法,结合其他资料整理的,文章有问题希望大佬可以指出,目前正在准备春招,希望上岸🙏🏻
文章目录
- 1.自我介绍
- 2.创建线程的方式
- 3.可以直接调用run方法来启动线程吗?
- 4.线程池如何使用?参数的含义?
- 5. HashMap和Hashtable的区别?
- 6. 如何保证HashMap线程安全?
- 7. HashMap扩容机制?
- 8. ThreadLocal有了解过吗?
- 9. volatile关键字作用?
- 10. ArrayList、LinkedList、Vector的区别
- 11. JVM运行时内存区域介绍
- 12. 垃圾判定和回收算法
- 13. 遇到过OOM吗?如何处理?
- 14.JVM调优参数有了解过吗?
- 15. 给定一张表test,有三个字段id,name,value,求根据name对value进行计算,求max,min,avg
- 16.事务隔离级别
- 17.SQL查询效率较低,如何排查?
- 18.Linux常用命令?
1.自我介绍
自信就是力量。
2.创建线程的方式
创建java线程的方法主要是四种,目前查阅到的资料查到有五种。
- 继承Thread类
创建一个类并继承Thread类,然后覆写其run()方法来定义线程的执行逻辑。通过实例化该类对象并调用其start()方法启动线程。
class MyThread extends Thread {
public void run() {
// 线程执行的逻辑
}
}
// 启动线程
MyThread thread = new MyThread();
thread.start();
- 实现Runnable接口:
创建一个实现了Runnable接口的类,然后通过该类的对象创建一个Thread对象,并将该Runnable对象作为参数传递给Thread的构造函数。
class MyRunnable implements Runnable {
public void run() {
// 线程执行的逻辑
}
}
// 启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();
- 实现Callable接口
和Runnable接口类似,但是Callable接口的call()方法可以返回执行结果,并且可以抛出异常。
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
// 线程执行的逻辑,并返回结果
return 42;
}
}
// 启动线程(由于Thread的构造参数不支持Callable,所以需要通过task来包装Callable)
FutureTask futureTask= new FutureTask(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
- 使用匿名内部类
可以直接在创建Thread对象时使用匿名内部类来实现Runnable接口或覆写Thread类的run()方法,还可以使用lambda表达式。
// 使用匿名内部类实现Runnable接口
Thread thread1 = new Thread(new Runnable() {
public void run() {
// 线程执行的逻辑
}
});
// 使用匿名内部类覆写Thread类的run()方法
Thread thread2 = new Thread() {
public void run() {
// 线程执行的逻辑
}
};
// 使用lambda表达式
Thread thread3=new Thread(
()->{
// 线程执行的逻辑
}
);
- 通过线程池创建
使用线程池来创建和管理线程,这样可以更有效地利用系统资源,并且可以控制线程的数量和生命周期。
public static void main(String[] args) {
// 创建一个固定大小的线程池,线程数为3,但是日常开发建议使用线程池的构造方法构建,这里仅是例子。
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
Runnable task = new MyTask(i);
executor.execute(task);
}
// 关闭线程池
executor.shutdown();
}
static class MyTask implements Runnable {
private final int taskId;
public MyTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running.");
// 线程执行的逻辑
}
}
3.可以直接调用run方法来启动线程吗?
直接调用run方法所起的效果类似于调用普通类的方法,并不能起到启动线程的效果,启动线程只能是使用start方法来启动。
4.线程池如何使用?参数的含义?
首先需要知道什么是线程池:
线程池是一种用于管理和复用线程的机制,它可以在程序中预先创建一组线程,并且可以控制线程的数量、生命周期以及执行方式。线程池通常用于提高多线程程序的性能和资源利用率,特别是在需要频繁创建和销毁线程的情况下。
一句话就是:线程池通过管理和复用线程来提高避免线程的频繁创建和销毁,以达到提高性能和资源利用率的效果。
线程池的创建方法有两种:通过ThreadPoolExecutor构造函数来创建(推荐)和通过 Executor 框架的工具类 Executors 来创建,这里就通过推荐的方法来使用线程池。
使用线程池第一步那就是先要有一个线程池,我们可以看到ThreadPoolExecutor一共有四种构造方法:
我们可以看到最长的构造方法里面有七个参数,我们可以来看他的参数名是什么
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
通过参数名我们可以知道各个参数的作用分别是什么(标红的是必带参数,所有构造函数都必须要传的参数,其他两个参数不传会使用默认参数)
参数名称 | 含义描述 |
---|---|
corePoolSize | 核心线程数,指线程池中保持活动状态的最小线程数量。即使线程处于空闲状态,核心线程也不会被销毁,除非设置了允许核心线程超时(allowCoreThreadTimeOut)。 |
maximumPoolSize | 最大线程数,指线程池中允许存在的最大线程数量。当工作队列已满且当前线程数小于最大线程数时,线程池会创建新的线程来执行任务,直到达到最大线程数为止。 |
keepAliveTime | 线程空闲时间,指非核心线程的空闲时间超过该值时,会被销毁。只有在allowCoreThreadTimeOut为true时,核心线程也会根据该参数进行回收。 |
unit | 空闲时间的时间单位,通常为秒、毫秒等。 |
workQueue | 工作队列,用于存放待执行的任务。当线程池中的线程都处于忙碌状态时,新的任务会被放入工作队列中等待执行。 |
threadFactory | 线程工厂,用于创建新线程。可以通过线程工厂来自定义线程的属性,比如线程名称、优先级等。 |
handler | 拒绝策略,指当工作队列已满且线程池中的线程数量达到最大值时,新提交的任务会被拒绝执行的处理策略。常见的拒绝策略包括抛出异常、丢弃任务等。 |
我们可以将问题1的最后一种创建方法进行改写,把我们创建的线程池使用起来。
public static void main(String[] args) {
// 创建一个固定大小的线程池,核心线程数为5,最大线程数为10,存活时间为10s的线程池
ExecutorService executor = new ThreadPoolExecutor(5,10,10,TimeUnit.SECONDS,new LinkedBlockingDeque<>());
for (int i = 0; i < 10; i++) {
Runnable task = new MyTask(i);
// 提交任务给线程池
executor.execute(task);
}
// 关闭线程池
executor.shutdown();
}
static class MyTask implements Runnable {
private final int taskId;
public MyTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running.");
// 线程执行的逻辑
}
}
执行效果如下(由于是多线程,所以会产生乱序的现象):
5. HashMap和Hashtable的区别?
标红的地方是主要的区别
特性 | HashMap | Hashtable |
---|---|---|
线程安全性 | 非线程安全 | 线程安全 |
继承关系 | 继承自AbstractMap,实现Map接口 | 继承自Dictionary类,实现Map接口 |
允许null键和值 | 允许 | 不允许(会抛出NullPointerException) |
性能 | 比Hashtable性能更好(非线程安全) | 在多线程环境下由于线程安全的开销,性能相对较低 |
初始容量和加载因子 | 默认初始容量 16 ,加载因子0.75 | 默认初始容量11,加载因子0.75 |
扩容机制 | 每次扩容为2n | 每次扩容为2n+1 |
底层数据结构 | 数组+链表+红黑树 | 数组+链表 |
6. 如何保证HashMap线程安全?
主要有以下三种方法
- 使用ConcurrentHashMap
这是Java提供的一个线程安全的HashMap实现。它通过分段锁的方式实现了并发性能的提升。每个线程可以独立地对不同段的HashMap进行操作,从而避免了锁竞争。
ConcurrentHashMap在JDK 1.7中是由Segment 数组结构和 HashEntry 数组结构组成,而在JDK 1.8之后ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。
- 使用Collections.synchronizedMap()
这个方法可以将HashMap转换为一个SynchronizedMap。它会为所有对HashMap的操作使用同一个锁进行同步,这样可以防止多个线程同时访问相同的键/值对,转换后的Map所有操作都会上锁。
3. 使用锁机制
线程安全最简单粗暴的方法就是加锁,操作到map的地方都给他加上一把锁,这样同一时刻就只会有一个线程操作map,也就不会有线程安全问题了。
7. HashMap扩容机制?
借用一下javaguide网站的图:
总结下来也比较简单,首先是插入操作的步骤如下:
1.插入数据时,先判定插入数据的位置是否以及存在数据,如果存在就使用拉链法解决哈希冲突问题,并且插入的时候使用尾插法,遍历过程中如果存在相同的key就覆盖掉里面的数据;不存在就直接插入。
2.插入了以后需要判定是否达到扩容的条件(大于阈值),达到条件就会发起扩容机制。
我们可以看看扩容机制的源码:
首先我们需要知道map中存在的成员变量以及他们的含义:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于等于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于等于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容
int threshold;
// 负载因子
final float loadFactor;
}
具体resize函数代码如下:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 判定当前容量是否超过最大容量,超过了就没办法扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果不超过,就将之前的长度(oldCap)扩大一倍赋值给newCap
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 这里扩大的是阈值
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
// 这里的作用是如果table中没数据,设置容量为默认的threshold
newCap = oldThr;
else {
// 这里是无参构造函数创建的对象在这计算容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//如果此时新阈值为0,则说明之前的阈值也为0,即通过上面的else if判断为真的赋值
// 需要重新给新的阈值为新容量*负载因子(超过)
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将全局变量里的阈值设置为新的阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建新容量的数组,准备搬迁原数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将全局变量的table设置为新的数组
table = newTab;
if (oldTab != null) {
// 这里就是将原table中的元素重新做hash操作并插入到新的数组中,这样就完成了整个resize()过程
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 当前节点如果next为空,代表该位置没有后续元素,直接将该元素放入到新位置就行
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 当前节点如果是红黑树,所以需要额外调用split函数,作用是拆分这棵树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 走到这里代表着当前节点有后续节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 能放回原长度的元素
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 放到拓展长度以后的元素
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
这就是resize函数的源码(jdk1.8)总结一下就是:
- 设置新阈值和新容量。
- 生成新数组。
- 遍历老数组中的每个位置上的链表或红黑树。
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去。
- 如果是红黑树,则需要拆分红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置。
其中split方法代码解读可以看这篇博客:HashMap-split()方法源码简读(JDK1.8)。
8. ThreadLocal有了解过吗?
ThreadLocal 是 Java 中的一个类,它提供了一种线程局部变量的解决方案。每个 ThreadLocal 对象可以存储线程私有的数据,这些数据对于其他线程是不可见的,每个线程都可以独立访问自己的 ThreadLocal 变量,也就是说,如果我们创建了一个ThreadLocal变量,则访问这个变量的每个线程都会有这个变量的一个本地副本。如果多个线程同时对这个变量进行读写操作时,实际上操作的是线程自己本地内存中的变量,从而避免了线程安全的问题。简单来说,ThreadLocal 提供了一种线程内部的数据共享方式。
可以通过这个例子了解一下:
public static void main(String[] args) {
ThreadLocal threadLocal=new ThreadLocal();
Thread a=new Thread(()->{
threadLocal.set("线程a");
System.out.println("线程a第一次获取:"+threadLocal.get());
threadLocal.remove();
System.out.println("线程a删除后再次获取"+threadLocal.get());
});
Thread b=new Thread(()->{
threadLocal.set("线程b");
System.out.println("线程b第一次获取:"+threadLocal.get());
System.out.println("线程b未删除再次获取:"+threadLocal.get());
});
b.start();
a.start();
}
结果为:
9. volatile关键字作用?
首先volatile不是java专有的,在c语言中也有volatile,被他修饰的变量表示这个变量是易变的。当一个变量被声明为 volatile 时,对该变量的读取操作会直接从主内存中读取,而写操作也会直接写入主内存,而不是线程的本地缓存中。
volatile主要有两个作用:
-
保证可见性:当一个线程修改了 volatile 变量的值后,其他线程可以立即看到这个修改的值。这是因为 volatile 变量的修改会立即刷新到主内存,其他线程读取该变量时会直接从主内存中读取最新值,而不会使用线程本地缓存中的旧值。
-
禁止重排序:在多线程环境下,编译器和处理器为了提高性能可能会对指令进行重排序,但这种重排序可能导致多线程程序出现问题。使用 volatile 关键字可以禁止指令重排序,保证程序的执行顺序符合预期。
这个关键字主要用在多线程编程中,当一个变量被多个线程共享并且这些线程中有写操作时,通常会考虑使用 volatile 关键字来确保线程间对该变量的操作的可见性。
10. ArrayList、LinkedList、Vector的区别
经典面试题,秒了
特性 | ArrayList | LinkedList | Vector |
---|---|---|---|
底层数据结构 | 数组 | 双向链表 | 数组 |
线程安全性 | 非线程安全 | 非线程安全 | 线程安全(使用了synchronized) |
性能 | 读取和修改元素速度较快 | 在插入和删除元素操作频繁的情况下性能较好 | 在多线程环境下性能较差,单线程环境下与 ArrayList 相当 |
扩容机制 | 当元素个数超过当前容量时,增加原容量的一半 | 不需要扩容,每次插入或删除元素都需要重新分配节点 | 当元素个数超过当前容量时,容量翻倍 |
随机访问 | O(1) 时间复杂度 | O(n) 时间复杂度 | O(1) 时间复杂度 |
插入和删除操作 | O(n) 时间复杂度 | O(1) 时间复杂度(在已知位置) | O(n) 时间复杂度 |
内存空间利用率 | 数组大小动态增长,可能会有一定的空间浪费 | 每个元素存储在独立的节点中,内存利用率较高 | 数组大小动态增长,可能会有一定的空间浪费 |
11. JVM运行时内存区域介绍
这里用一下javaguide的图:
接下来从上到下分别介绍一下各个区域的作用:
线程共享
1. 堆
Java 中的堆区(Heap)是用于存储对象实例的内存区域。堆区是 Java 虚拟机(JVM)管理的最大的一块内存区域,它的大小在 JVM 启动时就已经确定,并且可以动态扩展。堆区的主要作用包括:
- 存储对象实例:堆区是用于存储 Java 程序创建的对象实例的内存区域。当我们使用 new 关键字创建一个对象时,该对象就会被分配到堆区中。
- 动态分配和释放内存:堆区的大小是动态分配的,它可以根据程序的需要动态扩展。当堆区的内存空间不足时,JVM 会自动扩展堆区的大小。另外,Java 的垃圾回收机制也会在堆区中回收不再被引用的对象,释放其占用的内存空间。
- 支持垃圾回收:Java 的垃圾回收器会定期扫描堆区中的对象,标记哪些对象是不再被引用的,并且回收这些对象占用的内存空间。这个过程称为垃圾回收,它可以帮助程序员减少手动管理内存的工作量,提高代码的健壮性和可维护性。因此Java堆也被成为GC堆。
总结一句话就是:Java堆是JVM管理最大的一块内存区域,主要目的就是存放对象的实例,堆也是Java垃圾回收的主要区域,因此也叫做GC堆。
2.字符串常量池
在Java中,字符串常量池是一种特殊的内存区域,用于存储字符串字面量。当你创建一个字符串常量时,如果字符串常量池中已经存在相同内容的字符串,那么该字符串的引用会被重用,而不是重新创建一个新的字符串对象。这个机制在很多情况下可以减少内存的消耗,提高程序的执行效率。
线程独占
1.虚拟机栈
每个Java线程都有自己的虚拟机栈,它与线程的生命周期一一对应。每当一个线程创建时,Java虚拟机都会为其分配一个虚拟机栈,这个栈的大小可以在启动JVM时通过参数进行调整。
Java虚拟机栈用于存储方法的局部变量、方法的参数、部分计算结果以及方法的调用和返回信息,每一次调用方法就相当于创建了一个栈帧压入到虚拟机栈中,栈帧则是用来储存本次调用方法的相关信息。
栈帧里面包含了:局部变量表、操作数栈、动态链接、方法返回地址。接下来讲述一下他们分别有什么用:
- 局部变量:用于存储方法中的局部变量,包括方法参数和方法内部定义的局部变量。局部变量表是一个固定长度的数组,每个元素可以存储一个基本数据类型或一个对对象的引用。
- 操作数栈:用于执行方法中的操作。它是一个后进先出(LIFO)的栈结构,可以存储方法执行过程中的临时数据、中间结果和方法返回值。
- 动态链接:用于指向当前方法所属的类在运行时常量池中的方法引用。通过动态链接,Java虚拟机可以在运行时解析方法的调用。简单来说就是将调用其他方法的符号引用转化为该方法的直接引用。
- 方法返回地址:在方法执行完毕后,Java虚拟机会根据方法返回地址返回到调用该方法的地方。
总结来说:虚拟机栈就是Java线程调用方法的实现,被用于方法的执行和参数传递。
2.本地方法栈
和虚拟机栈的作用一样,也是被用于方法的执行和参数的传递,但是从名字可以看出这个栈是被用于本地方法的,也就是被native关键字修饰的方法,而虚拟机栈则是被用于被我们编写的java方法。
3.程序计数器
在Java中,程序计数器(Program Counter,简称PC)是一种线程私有的、具有记录当前线程执行位置的特殊寄存器。每个线程都有自己的程序计数器,它是线程执行字节码指令的指示器。
程序计数器的作用有以下几点:
- 指示下一条指令的执行位置: 程序计数器存储了当前线程正在执行的字节码指令的地址。在Java虚拟机执行字节码时,它通过程序计数器来确定下一条要执行的指令。
- 线程切换时的恢复点: 程序计数器还记录了线程当前的执行状态,包括正在执行的方法、当前执行的字节码指令位置等。当线程被操作系统挂起并重新调度时,Java虚拟机可以通过程序计数器快速地恢复线程的执行状态,而无需重新分析字节码。
- 支持线程独立性: 每个线程都有自己的程序计数器,因此线程之间的执行状态是相互独立的。这使得Java线程能够并发执行而不会相互干扰。
- 方法调用和返回的准确跟踪: 程序计数器可以准确地跟踪方法的调用和返回。在方法调用时,程序计数器会记录调用的下一条指令地址;在方法返回时,程序计数器会恢复到方法调用前的位置。
总结一句话,程序计数器就是线程执行字节码指令的指示器。
注意点:程序计数器是Java中唯一一个不会出现 OutOfMemoryError 的内存区域
本地内存
1.运行时常量池
首先简单讲讲什么是元空间,Java中的元空间是Java虚拟机中用于存储类元信息的内存区域,是Java虚拟机规范中对永久代的替代方案。
1.8以后才替换的,之前的永久代是在Java堆中,会占用虚拟机内存,而元空间是本地内存,他不受Java虚拟机内存大小的影响,只受本机物理内存大小的影)。
那么运行时常量池则是用于存储类文件中的常量池信息以及在运行时产生的一些常量,他会在类加载后被创建,属于每个类的一部分。
2.直接内存
直接内存就是一种在堆内存之外直接分配的内存空间,由操作系统直接管理,所以叫直接内存,他的作用主要有以下几点:
-
高效的I/O操作: 直接内存通常与Java NIO(New I/O)库一起使用,可以通过零拷贝技术实现高效的I/O操作。例如,可以将直接内存中的数据直接传输到网络或磁盘上,而无需经过Java堆内存的中间复制,从而提高了I/O操作的效率。
-
避免堆内存限制: 直接内存的分配不受Java堆内存的限制,可以分配更大的内存空间。这使得直接内存特别适用于需要处理大量数据或需要高性能的I/O操作的场景,例如网络编程、文件处理等。
-
降低内存管理开销: 直接内存的分配和释放不受Java虚拟机的垃圾回收机制控制,而是由操作系统进行管理。这降低了内存管理的开销,并且可以减少Java堆内存的碎片化,提高了内存使用效率。
-
提高程序性能: 由于直接内存的分配和释放不需要经过Java虚拟机的堆内存管理机制,因此可以减少内存管理的开销,提高程序的运行性能。
总结就是:直接内存在Java中的作用是提供一种高效、灵活、不受堆内存限制的内存管理方式,特别适用于需要处理大量数据或高性能I/O操作的场景,能够帮助提高程序的性能和效率。
12. 垃圾判定和回收算法
垃圾判定
所谓垃圾,就是这个对象没有在任何地方使用到,一点用处都没有,把他扔了也无所谓。判定对象是否为垃圾的方法有两种:引用计数法和可达性分析法。
引用计数法
每个对象都维护着一个引用计数器,当对象被引用时计数器加一,当对象的引用被释放时计数器减一。当计数器为0时,表示对象不再被引用,可以被回收。但Java虚拟机一般不采用这种算法,因为它无法解决循环引用的问题,也就是a引用了b,b引用了a,这样子形成了一个环,谁都没办法被回收。
可达性分析法
在可达性分析中,从一组称为“GC Roots”的对象出发,通过对象之间的引用关系,逐个标记所有能被引用到的对象,未被标记的对象即被判定为垃圾对象。用图片方式比较直观(来源javaguide)
上图没有被GC指向或间接指向的对象(6,7,8,9,10)
就会被判定为垃圾然后回收。
GC Roots包括虚拟机栈中引用的对象、静态变量引用的对象、常量引用的对象以及本地方法栈中引用的对象。
回收算法
四种:标记-清除算法、复制算法、标记-压缩算法、分代收集。
标记清除算法
很简单,分两个阶段:标记阶段,垃圾回收器会从根对象出发,遍历所有可达对象,并标记其为活动对象;清除阶段,垃圾回收器会清除未被标记的对象。
简单往往就会有问题,标记清除产生的问题就是内存过于碎片化了,也就是说内存不连续了,后面如果对象所需要的内存都大于这些碎片,那就没办法创建这个对象了。(图片来源:javaguide)
复制算法
因为上面的标记清除算法会产生碎片,那么复制算法干脆直接将内存划分为两块,一次只用一块,另外一块用来垃圾回收了以后,将存活的对象连续存放,这样就可以保证内存使用是连续的了。(图片来源:javaguide)
但是,一半一半就会有一半内存在正常情况下没被使用,内存利用率降低了;并且复制过程中如果对象存活很多,也会占用资源用于复制的这个过程。
标记整理算法
标记-压缩算法首先与标记-清除算法类似,但在垃圾回收阶段会将存活的对象压缩到一端,从而减少内存碎片化。(图片来源:javaguide)
但是,压缩的这个过程也是会占用资源的,只适合那种内存不经常被回收的场景。
分代算法
分代算法根据对象的存活周期将堆内存划分为多个代,一般分为年轻代和老年代。年轻代使用复制算法进行垃圾回收,老年代使用标记-清除或标记-压缩算法进行垃圾回收。利用各个算法的优缺点给不同的代使用不同的算法,就是分代算法。
13. 遇到过OOM吗?如何处理?
OOM(Out of Memory)是指内存溢出,即在程序运行过程中,由于申请的内存空间超出了可用内存空间的限制,导致无法再分配更多的内存而发生错误。
主要产生原因有:
- 内存泄漏(Memory Leak): 程序中存在内存泄漏时,申请的内存空间没有被正确释放,导致内存占用不断增加,最终耗尽了可用内存空间。
- 对象生命周期过长: 如果程序中创建了大量长期存活的对象,而这些对象又无法被垃圾回收器回收,就会导致内存不足。
- 大对象分配: 程序中一次性申请了过大的内存空间,超出了可用内存空间的限制,导致内存溢出。
处理方法有:
- 分析内存使用情况: 使用工具如JVisualVM、jmap、jstack等分析内存使用情况,定位内存溢出的原因和具体的内存泄漏点。
- 增加堆内存大小: 可以通过设置JVM参数 -Xmx 和 -Xms 来增加Java堆内存的大小,提高可用内存空间。
- 优化程序代码: 优化程序代码,避免创建过多的对象、减少对象生命周期、避免大对象分配等,从而降低内存消耗。
- 使用内存管理工具: 使用内存管理工具如Eclipse Memory Analyzer(MAT)等工具,帮助分析和诊断内存溢出问题。
- 检查第三方库和框架: 如果使用了第三方库和框架,需要检查其内存使用情况,确保其正确释放资源。
- 使用内存溢出自动恢复机制: 在一些特定场景下,可以使用内存溢出自动恢复机制来处理OOM,例如使用Hystrix等断路器模式库来进行服务降级或熔断。
14.JVM调优参数有了解过吗?
这无非就是JVM参数的设置有哪些。
参数 | 描述 |
---|---|
-Xms<size> | 指定Java堆的初始大小。 |
-Xmx<size> | 指定Java堆的最大大小。 |
-Xmn<size> | 指定新生代的大小。 |
-XX:NewRatio=<N> | 指定新生代和老年代的比例,默认值为2。 |
-XX:SurvivorRatio=<N> | 指定Eden区和Survivor区的比例,默认值为8。 |
-XX:+UseSerialGC | 指定使用串行垃圾回收器。 |
-XX:+UseParallelGC | 指定使用并行垃圾回收器。 |
-XX:+UseG1GC | 指定使用G1垃圾回收器。 |
-XX:PermSize=<size> | 指定永久代的初始大小。 |
-XX:MaxPermSize=<size> | 指定永久代的最大大小。 |
-XX:MetaspaceSize=<size> | 指定元空间的初始大小。 |
-XX:MaxMetaspaceSize=<size> | 指定元空间的最大大小。 |
-XX:+PrintGC | 打印GC日志。 |
-XX:+PrintGCDetails | 打印详细的GC日志信息。 |
-XX:+PrintGCTimeStamps | 打印GC发生的时间戳。 |
-XX:+HeapDumpOnOutOfMemoryError | 在OOM时生成堆转储文件。 |
-XX:HeapDumpPath=<path> | 指定堆转储文件的路径。 |
-XX:MaxTenuringThreshold=<N> | 指定对象进入老年代的年龄阈值。 |
-XX:+UseCompressedOops | 启用压缩指针以减少堆内存占用。 |
-XX:ParallelGCThreads=<N> | 指定并行垃圾回收器的线程数。 |
15. 给定一张表test,有三个字段id,name,value,求根据name对value进行计算,求max,min,avg
不太清楚具体题目意思,pass
16.事务隔离级别
四种隔离级别:串行化,可重复度,读已提交,读未提交。
17.SQL查询效率较低,如何排查?
SQL查询效率低,主要有三个原因:索引问题,SQL语句问题,硬件问题。
排查和优化方式可以有以下几种:
- 分析执行计划(Explain Plan): 使用数据库提供的分析工具(如EXPLAIN关键字)来获取SQL查询的执行计划。执行计划可以告诉你数据库是如何执行查询的,包括使用了哪些索引、执行了哪些操作等信息,从而帮助你确定性能瓶颈所在。
- 检查索引: 确保查询中涉及的字段有适当的索引。缺少索引或者使用了不恰当的索引都可能导致查询效率低下。可以通过执行计划来查看是否使用了索引,以及索引的效果如何。
- 优化SQL语句: 优化SQL语句的编写方式,尽量减少数据扫描和计算量。可以通过减少不必要的连接、过滤和排序等方式来提高查询效率。
- 分析性能日志: 查看数据库的性能日志,了解数据库的负载情况、查询响应时间等信息。性能日志可以帮助你找到繁忙的查询、高负载时段等问题。
- 检查锁和阻塞: 如果查询涉及到大量更新操作,可能会导致锁和阻塞问题,进而影响其他查询的性能。可以通过查看数据库的锁信息来排查这类问题。
- 硬件资源: 确保数据库服务器有足够的硬件资源(CPU、内存、磁盘等)来支撑查询的执行。资源不足可能会导致查询效率低下。
18.Linux常用命令?
这个就平时用到啥就是啥,多敲熟了。可以参考网站:指令