👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO TOP红人
Java知识图谱点击链接:体系化学习Java(Java面试专题)
💕💕 感兴趣的同学可以收藏关注下 ,不然下次找不到哟💕💕
✊✊ 感觉对你有帮助的朋友,可以给博主一个三连,非常感谢 🙏🙏🙏
1.Java 基础
1.1. 面向对象的特征
继承、封装、多态(向上转型、向下转型)
1.2. 重载、重写
重载 是指在同一个类中,可以定义多个同名的方法,但它们的参数类型、参数个数或返回类型不同。通过重载,可以根据不同的参数来执行不同的操作。编译器会根据方法的参数列表来确定要调用的方法。
重写 是指子类重新实现(覆盖)了父类中已有的方法。子类可以根据自己的需求重新定义方法的实现逻辑,但方法的名称、参数类型和返回类型必须与父类中的方法相同。重写方法可以实现多态性,即在运行时根据对象的实际类型来调用相应的方法。
1.3. 向上转型、向下转型
向上转型(Upcasting)和向下转型(Downcasting)是面向对象编程中用于处理对象的类型转换的概念。
向上转型是指将一个子类的实例赋值给父类类型的引用。这样做可以使得父类引用指向子类对象,但只能访问父类中定义的属性和方法,无法访问子类中特有的属性和方法。这是因为在向上转型后,编译器只能识别出父类中的成员。
示例:
class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
Animal animal = dog; // 向上转型
animal.eat(); // 可以访问父类的方法
// animal.bark(); // 错误,无法访问子类的方法
}
}
向下转型是指将一个父类类型的引用转换为子类类型的引用。这样做可以使得父类引用指向的子类对象恢复为子类的类型,从而可以访问子类中特有的属性和方法。但是,需要注意的是,向下转型只能在实际对象类型是子类类型的情况下才能成功进行,否则会抛出ClassCastException异常。
示例:
class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog();
Dog dog = (Dog) animal; // 向下转型
dog.eat(); // 可以访问父类的方法
dog.bark(); // 可以访问子类的方法
}
}
需要注意的是,在进行向下转型时,需要确保实际对象的类型是子类类型,否则会导致运行时错误。因此,在进行向下转型之前,最好使用instanceof运算符进行类型检查,以确保转型的安全性。
1.4. String 类可以继承吗?
final修饰的类不能被继承
1.5. String和StringBuilder、StringBuffer的区别?
String 创建后只是不能修改的,修改值就会引发新的对象生成。
StringBuffer 线程安全的,里面方法大多数是synchronized修饰的,所有性能有差距。
StringBuilder 线程不安全的,性能好。
1.6. String s = new String(“xyz”) 创建了几个字符串对象?
分情况判断:
1、如果在代码之前已经有String = “xyz”, 那么这里就只是创建了一个对象。因为xyz已经在(String = “xyz”)的时候放到常量池中了,现在只不过在堆中创建一个xyz。
2、如果代码之前没有String = “xyz”,那么就创建了两个对象,堆中一个,常量池中一个,常量池中放的是引用。
1.7. == 和 equals 的区别是什么?
1、基本数据类型时,== 是比较值
2、引用类型 == 判断的是引用地址
3、equals,如果是基本数据类型的封装类,比较的是值,如果是引用类型,如果没有重写equals,调用的都是Object方法中的。
1.8. 什么是反射
反射(Reflection)是一种在运行时检查、获取和操作类、对象、方法和属性的能力。它使得程序能够动态地获取类的信息,调用对象的方法,访问和修改对象的属性,以及在运行时创建新的对象实例。
通过反射,我们可以在运行时获取类的名称、父类、接口、字段、方法等信息。还可以动态地创建对象、调用方法、访问和修改属性,即使在编译时我们并不知道这些信息。这使得我们能够编写更加灵活和通用的代码,实现一些动态的、可扩展的功能。
在Java中,反射是通过java.lang.reflect包中的类和接口来实现的。主要的类包括Class类(表示类的信息)、Constructor类(表示构造方法的信息)、Method类(表示方法的信息)和Field类(表示字段的信息)等。
需要注意的是,由于反射涉及到动态的操作和访问,它可能会牺牲一些性能。因此,在使用反射时,需要权衡灵活性和性能之间的平衡,并谨慎使用
1.9. 深拷贝和浅拷贝区别是什么?
浅拷贝基本数据类型可以被复制,但是引用类型只是复制它的引用地址,如果之后发生了改变,复制后的也会跟着改变。
深拷贝是基本数据类型和引用类型都真正意义上的拷贝,就是新的对象,在内存中也是指向不同的空间,所有源对象发生改变,并不会引起拷贝后的对象改变。
1.10.并发和并行有什么区别?
并行是真正意义上的同时进行,并发只是同一时间密集的交替工作,就像单核CPU交替分配时间片。
1.11.构造器是否可被重写?
只能被重载不能被重写。
1.12.Java 静态变量和成员变量的区别
静态变量也叫作类变量,是随着类的加载而加载,在类加载过程中的准备阶段就开始初始化。 而成员变量又叫做实例变量,是随着对象的创建而创建(一类对象的创建可以分为两个过程,先实例化、然后初始化,实例化是开辟空间,属性填充默认值,然后再填充相应的值,这个属性就是成员变量,它是随着对象的创建而创建)。
并且成员变量只能对象才能调用,而静态变量是类也能调用,对象也能调用(虽然对象调用编译器会报红)。
1.13.是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
静态方法的调用可以通过类,也可以通过对象,而非静态方法的调用只能是对象实例化之后,需要显式的创建对象才能调用。所以如果一个静态的方法想要调用非静态方法,就必须先创建对象,然后调用。
1.14.子类和父类中变量、方法的执行顺序
父类中静态代码块/变量 -》 子类中的静态代码块/变量 -》 父类中的非静态变量/方法 -》父类中的构造器 -》 子类中的非静态变量/方法 -》 子类中的构造器 。
子类在实例化的时候,即使子类构造器中没有写super调用, 也没有this调用,还是会隐式的调用父类构造器。
1.15.为什么不能根据返回类型来区分重载?
方法的返回值是运行后的状态,而且并不是所有的方法都关注返回值,有些方法不关心返回值。所以返回值不能作为方法重载的条件。
1.16.抽象类(abstract class)和接口(interface)有什么区别?
抽象类单继承,抽象类无法被实例化,想要实例化就必须要靠子类继承。子类继承抽象类就必须要重写。在JDK 1.7之前,抽象类中所有的方法都必须是抽象的。
接口可以多现实,并且接口可以多继承接口。接口不能实例化,接口因为是无状态的,所以里面的成员变量都是常量。JDK 1.8支持了非抽象方法(default方法,静态方法)(目的是可以自行扩展),JDK 1.9又引入了私有方法,私有的静态方法(目的是可以封装方法,代码复用)。
1.17. Error 和 Exception 有什么区别?
Error 和 Exception 都是 Throwable的子类。
Error 是系统层面的错误,一般很难或者说基本上不可能自行恢复,如内存溢出,需要人工干预。
Exception 异常,代码层面的问题,我们可以捕捉和处理,主要是程序运行过程中可能性的错误。
1.18.Java 中的 final 关键字有哪些用法?
修饰类: 不可被继承,如String
修饰方法:不可被重写
修饰变量: 如果是基本数据类型就是常量,不能改变。如果是引用类型,改对象的的引用地址不能改变,其内的属性还是能变的。
1.19.阐述 final、finally、finalize 的区别。
final: 上面阐述过了
finally: 只能跟在try-catch的后面,finally块是一定会被执行到的,通常用来关闭流、连接之类的。即使try 里面有return了,finally块也会在return之前执行。
finalize: 来源于Object,在JDK 1.9中已经标记被放弃了。这个方法是干嘛的呢?
在jvm里,一个引用不被其他强引用关联时,这个对象即将被回收,在对象回收之前就会调用一次这个finalize方法,但是这个方法的失败之处就是gc的回收时机我们没办法控制,所以这个方法我没办法控制他的执行时间。
1.20.JDK1.8之后有哪些新特性?
1、接口里面可以添加非抽象方法(静态方法、default方法)(接口可以自行的扩展);
2、Lambda 表达式 (函数式接口,@FunctionInterface, 函数式接口类里面有且只有一个抽象方法接口),Lambda 表达式本质上是一段匿名内部类
3、Stream API:Stream API 提供了一种高效且易于使用的处理数据的方式。Stream API 提供了一种高效且易于使用的处理数据的方式。
源代码中给出的例子:
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
4、方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。MethodRef1 m = (x, y) -> System.out.println(x);
5、日期时间API:Java 8 引入了新的日期时间API改进了日期时间的管理。
6、Optional 类
7、新工具:新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器 jdeps。
详细看 【大厂面试必问】Java8 新特性 https://jiliu.blog.csdn.net/article/details/131508614
1.21.List、Set、Map三者的区别?
List(对付顺序的好帮手): List 接口存储一组不唯一(可以有多个元素引用相同的对象)、有序的对象。
Set(注重独一无二的性质):不允许重复的集合,不会有多个元素引用相同的对象。
Map(用Key来搜索的专业户): 使用键值对存储。Map 会维护与 Key 有关联的值。两个 Key可以引用相同的对象,但 Key 不能重复,典型的 Key 是String类型,但也可以是任何对象
1.22.ArrayList 和 LinkedList 的区别
ArrayList 底层基于动态数组实现,LinkedList 底层基于链表实现。
对于按 index 索引数据(get/set方法):ArrayList 通过 index 直接定位到数组对应位置的节点,而 LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上 ArrayList 优于 LinkedList。
对于随机插入和删除:ArrayList 需要移动目标节点后面的节点(使用System.arraycopy 方法移动节点),而 LinkedList 只需修改目标节点前后节点的 next 或 prev 属性即可,因此在效率上 LinkedList 优于 ArrayList。
对于顺序插入和删除:由于 ArrayList 不需要移动节点,因此在效率上比 LinkedList 更好。这也是为什么在实际使用中 ArrayList 更多,因为大部分情况下我们的使用都是顺序插入。
1.23.ArrayList 和 Vector 的区别
Vector 和 ArrayList 几乎一致,唯一的区别是 Vector 在方法上使用了 synchronized 来保证线程安全,因此在性能上 ArrayList 具有更好的表现。
有类似关系的还有:StringBuilder 和 StringBuffer、HashMap 和 Hashtable。
1.24.介绍下 HashMap 的底层数据结构
我们现在用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,如下图,而在 JDK 1.8 之前是由“数组+链表”组成。
详细的就看 HashMap 的底层原理和源码分析 https://jiliu.blog.csdn.net/article/details/131094698
1.25.为什么要改成“数组+链表+红黑树”?
主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。
1.26.那在什么时候用链表?什么时候用红黑树?
对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。
1.27.HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?
默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。
HashMap 的插入流程是怎么样的?
1.28.HashMap 的扩容(resize)流程是怎么样的?
1.29.除了 HashMap,还用过哪些 Map,在使用时怎么选择?
1.30.HashMap 和Hashtable 的区别?
-
从锁方面看: Hashtable 是同步的,这意味着它是线程安全的,可以在多线程环境中使用而无需外部同步。而 HashMap 默认情况下是非同步的,因此如果需要在多线程环境中使用,需要实现外部同步。
-
空值和空键:Hashtable 不允许空键或空值。如果尝试插入空键或空值,它会抛出 NullPointerException。而 HashMap 允许空键和空值。事实上,它允许最多一个空键和多个空值。
-
性能:通常认为 HashMap 的性能比 Hashtable 好。这是因为 Hashtable 是同步的,这在性能方面引入了一些开销。HashMap 是非同步的,在单线程环境中可以有更好的性能。
-
迭代器:Hashtable 返回的迭代器是快速失败的迭代器,这意味着如果在迭代过程中对 Hashtable 进行结构性修改,它会抛出 ConcurrentModificationException。HashMap 返回的迭代器也是快速失败的。
-
继承关系:Hashtable 是一个早期版本的遗留类,自 Java 早期版本以来就存在。HashMap 是在 Java 1.2 中作为集合框架的一部分引入的新类。
1.31.Java有哪几种数据类型
基本数据类型:byte(1字节) short(2字节) int(4字节) long(8字节) float(4字节) double(8字节) char(2字节) boolean(1字节)
引用数据类型:String 类 接口 抽象类 枚举 数组
2.JUC(Java 并发编程)
2.1.wait() 和 sleep() 方法的区别
首先两个都是暂停线程的方法
wait 是继承Object的方法,wait主动让出CPU时间片,需要手动唤醒,通过notify 或者 notifyAll 去唤醒,wait只能在同步代码中使用,否则会抛IllegalMonitorStateException异常。
sleep是Thread类中的方法,是暂停指定的时间,时间一到,自动恢复。sleep随便其他方法中都可以使用。
2.2.线程的 sleep() 方法和 yield() 方法有什么区别?
sleep上面已经说过了
yield是主动的让CPU出时间片,从running 转为 runnable,具体让出CPU时间片是否就暂停当前线程,这个要看cpu,如果cpu没有分配时间片,那么就是等待运行状态,但是如果分配了,仍然是运行状态。例如只有一个线程,你让出时间片也没有用,cpu还是会给改线程可执行的时间片。
2.3.线程的 join() 方法是干啥用的?
join的本意是让其他线程加入到当前线程,等待其他线程完成之后,当前线程再开始运行。例如 a.join() 表示等待a线程执行完成之后。可以加入超时时间,加入时间,如果在指定时间内,a线程还没有执行完成,那就不等了,恢复可运行状态。
join的底层原理还是调用wait。
2.4.编写多线程程序有几种实现方式?
1、继承Thread类
2、实现Runnable接口
3、实现Callable,这个Callable是可以设置返回值的。 Callable可以配合Future获得返回值
4、线程池
线程的4种创建方式 https://jiliu.blog.csdn.net/article/details/131040552
2.5.Thread 调用 start() 方法和调用 run() 方法的区别
start意味着加入到线程组中排队,状态变成可运行的状态runnable,等待cpu分配时间片就可以运行。
run就是一个普通方法,主动调用run运行线程,不会真正创建一个线程运行,就和普通对象调用方法一样,等待我执行完成才能到你。
2.6.线程的状态流转
2.7.synchronized 和 Lock 的区别
1、Lock是个接口,而Synchronized是关键字,由内置语言实现的。
2、Lock上锁lock() 解锁是unlock方法,需要手动去解锁,配合finally块使用。而synchronized锁比较透明,自动上锁解锁。
3、Lock更加灵活,有响应中断、超时时间。而synchronized却不行
4、性能上Lock要好点,但是synchronized也在不断的优化,性能差距越来越小。但是官方认为性能不是决定用哪种锁的必要原因,而是根据业务情况去使用。推荐优先使用synchronized。
2.8.synchronized的底层实现?
实现是通过一把monitor的互斥锁实现的,这个锁是系统级别的,所以操作系统要实现线程之间的切换,就要从用户态转为核心态,这个成本很高。因此说synchronized的效率低,并且称它叫做重量级锁。
JDK1.6优化给出了轻量级锁:
t1线程cas成功,表示获得锁,标志位改成00 轻量级锁。
如果接下来是t1线程自己再次执行synchronized锁重入,那么就会添加一条Lock Record的锁重入记录。
如果接下来有其他线程t2来竞争锁,这个线程cas失败,那么就进入锁膨胀过程,升级成重量级锁,标志位变成10,那么t2要获得重量级锁,但是此时t1在占用着,所以t2只能进入重量级锁Monitor的EntryList Blocked 中等待,也就是在一个阻塞的队列中等待,在进入这个Entry前,重量级锁还有一步优化,叫自旋优化,t2不会立马进入阻塞状态,而是进入自旋状态,会进行一定次数的自旋,这个可以看做web浏览器的loading一样,如果在自旋次数内,t1释放了锁,那么t2就不用进入阻塞,就可以直接获得锁,如果自旋结束了还是没有获得锁,那么再去EntryList中等待。
如果线程t1开始解锁,就将Mark Word中的标志位调整成01,表是解锁成功。如果调整失败,说明轻量级锁已经膨胀成重量级锁,就要走重量级锁的解锁流程,重量级锁解锁流程: 首先找到自己持有的重量级锁,然后让锁中的Owner 变为null,并且唤醒在阻塞队列中的线程进入等待。
JDK 1.6继续优化—设计出了偏向锁。
如果一个线程竞争成功了锁,那么这个锁会设置一个标志位01,并且设置自己线程ID在Mark Word上(这种设置id的操作只会再第一次cas时设置),表示偏向模式,其他线程要竞争锁,就要有cas的操作,如果发现这个锁并未指向任何线程,那么竞争锁成功。(这个就相当于,我要进一个门,门上贴着一张纸,这个纸就是偏向锁标志,如果门上没有纸,说明里面没有人,我可以直接进门,进门之前贴上纸,纸上写着我的名字。标志位就在Mark Word中,其中一个字节表示偏向锁标志(0 非偏向锁 1 偏向锁),另两个直接表示锁标志位(01 无锁 00 轻量级锁 10 重量级锁 11 GC标记)。如果cas失败,表示这个锁并不是偏向自己,此时就会升级成轻量级锁。
偏向锁的目的就是减少锁重入时的Cas操作,提升性能。
2.9.如何取消偏向锁?
1、禁用偏向锁 -xx:UseBiasedLocking
2、object.hashCode() 会禁用偏向锁,因为设置了hashCode,头部没有字节可以存储偏向锁ID了
3、类持有了线程1的偏向锁,然后线程1执行完成,此时偏向锁的ID还会保留在MarkWord头上,此时线程2对类加锁,发现类已经有了偏向锁,这是就会升级成轻量级锁,偏向锁的信息就会被清除。
4、调用wait/notify也会取消偏向锁。因为这两个方法是重量级锁拥有的,使用它就会升级成重量级锁
2.10.如何检测死锁?
jstack、jconsole都可以检测
2.11.为什么要使用线程池?直接new个线程不是很舒服?
1、降低资源消耗
2、提高响应的速度
3、增加线程的可管控性。
把线程交给线程池去管理,对我们更透明,我们不必专注于资源的使用、优化、释放,目的更专注于业务实现。一些设计模式的本意不就是把实现过程封装,对外提供统一接口访问,更专注于业务使用
2.12.线程池的核心属性有哪些?
线程池核心类 ThreadPoolExecutor, 7个核心参数。具体看下面链接,面试必背。
线程池和使用 https://jiliu.blog.csdn.net/article/details/131059674
2.13.说下线程池的运作流程
2.14.synchronized和ReentrantLock的区别:
实现方面:一个是JVM层次通过,(监视器锁)monitor实现的。一个是通过AQS实现的
相应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
锁的类型不同:synchronized 是非公平的锁,而ReentrantLock即可以是公平的也可以是非公平的
获取锁的方式不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
2.15.为什么AQS使用的双向链表
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候就会用到prev和next方便删除掉中间的节点。
3. JVM (1-2年 不用掌握太深)
3.1.Java 内存结构(运行时数据区)
程序计数器:线程私有。一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。
Java虚拟机栈:线程私有。它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈:线程私有。本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
Java堆:线程共享。对大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区:与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造方法、接口定义)、常量、静态变量、即时编译器编译后的代码(字节码)等数据。方法区是JVM规范中定义的一个概念,具体放在哪里,不同的实现可以放在不同的地方。
运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
3.2.什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
3.3.Java虚拟机中有哪些类加载器?
启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
扩展类加载器(Extension ClassLoader):
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):
这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器: 用户自定义的类加载器。
3.3.类加载的过程
类加载的过程包括:加载、验证、准备、解析、初始化,其中验证、准备、解析统称为连接。
加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。
验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值“通常情况”下是数据类型的零值。
解析:将常量池内的符号引用替换为直接引用。
初始化:到了初始化阶段,才真正开始执行类中定义的 Java 初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。
3.4.介绍下垃圾收集机制(在什么时候,对什么,做了什么)?
在触发GC的时候,具体如下,这里只说常见的 Young GC 和 Full GC。
触发Young GC:当新生代中的 Eden 区没有足够空间进行分配时会触发Young GC。
触发Full GC:
当准备要触发一次Young GC时,如果发现统计数据说之前Young GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Young GC而是转为触发Full GC。(通常情况)
如果有永久代的话,在永久代需要分配空间但已经没有足够空间时,也要触发一次Full GC。
System.gc()默认也是触发Full GC。
heap dump带GC默认也是触发Full GC。
CMS GC时出现Concurrent Mode Failure会导致一次Full GC的产生。
对那些JVM认为已经“死掉”的对象。即从GC Root开始搜索,搜索不到的,并且经过一次筛选标记没有复活的对象。
对这些JVM认为已经“死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。
3.5.GC Root有哪些?
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
3.6.垃圾收集有哪些算法,各自的特点?
标记 - 清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记 - 整理算法
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。
详细算法看:
深入学习 JVM 算法 - 引用计数法 https://jiliu.blog.csdn.net/article/details/131463986
深入学习 GC 算法 - 标记清除算法 https://jiliu.blog.csdn.net/article/details/131453939
深入学习 JVM 垃圾回收算法 https://jiliu.blog.csdn.net/article/details/131446119文章来源:https://www.toymoban.com/news/detail-650929.html
💕💕 本文由激流原创,原创不易,希望大家关注、点赞、收藏,给博主一点鼓励,感谢!!!
🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃
文章来源地址https://www.toymoban.com/news/detail-650929.html
到了这里,关于【八股文篇】Java 面试题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!