Java面试题及答案(2022版),适用于应届生、有工作经验的程序员,每道都是认真筛选出的高频面试题,助力大家能找到满意的工作!
• 类加载器(ClassLoader)
• 运行时数据区(Runtime Data Area)
• 执行引擎(Execution Engine)
• 本地库接口(Native Interface)
组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
• 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
• Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
• 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
• Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
• 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
• 寄存器:我们无法控制。
• 静态域:static 定义的静态成员。
• 常量池:编译时被确定并保存在 .class 文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)。
• 非 RAM 存储:硬盘等永久存储空间。
• 堆内存:new 创建的对象和数组,由 Java 虚拟机自动垃圾回收器管理,存取速度慢。
• 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性。
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
• jconsole:用于对 JVM 中的内存、线程和类等进行监控;
• jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
• -Xms2g:初始化推大小为 2g;
• -Xmx2g:堆最大内存为 2g;
• -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
• -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
• –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
• -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
• -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
• -XX:+PrintGC:开启打印 gc 信息;
• -XX:+PrintGCDetails:打印 gc 详细信息。
JVM 的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些 对象回收掉之前,他们会一直占据堆内存空间。
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
• 启动类加载器(Bootstrap ClassLoader)用来加载 Java 核心类库,无法被 Java 程序直接引用。
• 扩展类加载器(extensions class loader):它用来加载 Java的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
• 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader() 来获取它。
• 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
Java 类加载需要经历一下 7 个过程:
加载是类加载的第一个过程,在这个阶段,将完成一下三件事情:
• 通过一个类的全限定名获取该类的二进制流。
• 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
• 在内存中生成该类的 Class 对象,作为该类的数据访问入口。
验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:
• 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
• 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
• 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
• 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
public static int value=123;//在准备阶段 value 初始值为 0 。在初始化阶段才会变为 123 。
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由
虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的
Java 程序代码。
使用
卸载
GC 是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象, 并将它们添加到要回收的集合中,进行回收。
对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是” 可达的”,哪些对象是”不可达的”。当 GC 确定一些对象为“不可达”时,GC 就有责任回收这些内存空间。可以。程序员可以手
动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。
所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器
从内存中清除掉。
由于 Java 使用有向图的方式进行垃圾回收管理, 可以消除引用循环的问题,例如有两个对象,相互引用,只要它们 和根进程不可达的,那么 GC 也是可以回收它们的,例如下面的代码可以看到这种情况的内存回收:
import java.io.IOException;
public class GarbageTest {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException
{
// TODO Auto-generated method stub
try {
gcTest();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("has exited gcTest!"); System.in.read();
System.in.read(); System.out.println("out begin gc!");
for(int i=0;i<100;i++)
{
System.gc(); System.in.read(); System.in.read();
}
}
private static void gcTest() throws IOException { System.in.read();
System.in.read();
Person p1 = new Person(); System.in.read(); System.in.read();
Person p2 = new Person(); p1.setMate(p2); p2.setMate(p1);
System.out.println("before exit gctest!"); System.in.read();
System.in.read(); System.gc();
System.out.println("exit gctest!");
}
private static class Person
{
byte[] data = new byte[20000000]; Person mate = null;
public void setMate(Person other)
{
mate = other;
}
}
简单来讲就是复制、克隆。
Person p=new Person(“张三”);
浅拷贝就是对对象中的数据成员进行简单赋值,如果存在动态成员或者指针就会报错。
深拷贝就是对对象中存在的动态成员或指针重新开辟内存空间。