小厂实习面试记录-004

1.什么是类加载?类加载的过程?

类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。

加载

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的Class对象,作为方法区类信息的访问入口

验证

确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证

准备

为类变量分配内存并设置类变量初始值的阶段。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。

初始化

开始执行类中定义的Java代码,初始化阶段是调用类构造器的过程。

2.什么是双亲委派模型?

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派模型的具体实现代码在 java.lang.ClassLoader中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}

3.为什么需要双亲委派模型?

双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。

4.什么是类加载器,类加载器有哪些?

  • 实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

    主要有一下四种类加载器:

    • 启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。
    • 扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
    • 系统类加载器:它根据应用的类路径来加载 Java 类。可通过ClassLoader.getSystemClassLoader()获取它。
    • 自定义类加载器:通过继承java.lang.ClassLoader类的方式实现。

5.Java中变量,代码块,构造器之间执行顺序是怎么样的?

Java程序中类中个元素的初始化顺序 初始化的原则是:

  • 先初始化静态部分,再初始化动态部分,
  • 先初始化父类部分,后初始化子类部分,
  • 先初始化变量,再初始化代码块构造器

6.怎么自定义一个类加载器?

加载一个类时,一般是调用类加载器的loadClass()方法来加载一个类,loadClass()方法的工作流程如下:

1.先调用findLoadedClass(className)来获取这个类,判断类是否已加载。

2.如果未加载,如果父类加载器不为空,调用父类加载器的loadClass()来加载这个类,父类加载器为空,就调用父类加载器加载这个类。

3.父类加载器加载失败,那么调用该类加载器findClass(className)方法来加载这个类。

  • 所以我们一般自定义类加载器都是继承ClassLoader,重写findClass()方法,来实现类加载,这样不会违背双亲委派类加载机制。

  • 也可以通过重写loadClass()方法进行类加载,但是这样会违背双亲委派类加载机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class DelegationClassLoader extends ClassLoader {
private String classpath;

public DelegationClassLoader(String classpath, ClassLoader parent) {
super(parent);
this.classpath = classpath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
try {
String classFilePath = this.classpath + name.replace(".", "/") + ".class";
is = new FileInputStream(classFilePath);
byte[] buf = new byte[is.available()];
is.read(buf);
return defineClass(name, buf, 0, buf.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
throw new IOError(e);
}
}
}
}

public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
MalformedURLException {
sun.applet.Main main1 = new sun.applet.Main();

DelegationClassLoader cl = new DelegationClassLoader("java-study/target/classes/",
getSystemClassLoader());
String name = "sun.applet.Main";
Class<?> clz = cl.loadClass(name);
Object main2 = clz.newInstance();

System.out.println("main1 class: " + main1.getClass());
System.out.println("main2 class: " + main2.getClass());
System.out.println("main1 classloader: " + main1.getClass().getClassLoader());
System.out.println("main2 classloader: " + main2.getClass().getClassLoader());
ClassLoader itrCl = cl;
while (itrCl != null) {
System.out.println(itrCl);
itrCl = itrCl.getParent();
}
}
}

7.Java线程模型详解

定义

线程是操作系统的最小调度单位,包含于进程。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程可以并行执行不同的任务。

与进程的不同

  1. 进程是操作系统资源分配的最小单位,线程是操作系统执行的最小单位;
  2. 进程包含线程,一个进程可以包含多个线程,同一个进程中的不同线程共享同一资源(此处会引发另一个问题,线程不可见问题);
  3. 进程是指一段程序的执行过程,线程指的是进程中一个单一顺序的控制流(任务);

线程分类

线程主要分为两种

  • 内核线程,简称 KLT(Kernel Level Thread)
  • 用户线程,简称 ULT(User Level Thread)

内核线程

系统内核管理线程,内核保存线程的状态和上下文,线程阻塞不会引起进程阻塞。在多处理器上,多线程在多处理器上并行运行。线程的创建、调度和管理等生命周期是由内核直接管理完成,效率比 ULT 要低,比进程要高。

内核空间会维护 [进程表] 和[线程表]。进程表中维护并管理进程 [运行的程序代码] 集合。线程维护并管理各个进程中的线程集合。线程阻塞,其所在的进程不会阻塞。内核线程和轻量级进程 (Light Weight Process,LWP, 是使用内核线程的一种高级接口) 是一对一模型,这个文章后面会有介绍。

优缺点

  • 优点:可以将复杂的线程生命周期的管理任务交给操作系统,编程和实现简单;线程维护在操作系统内核,线程阻塞不会阻塞进程;如果机器是多核处理器,内核线程可以充分利用多核处理器进行并行运行线程。
  • 缺点:当线程进行调度、创建等,会涉及到用户态和内核态之间的转化以及线程上下文的切换,资源消耗大、效率较低。

用户线程

用户程序实现,不依赖操作系统内核,应用提供创建、调度和管理线程的函数来控制用户线程。不需要用户态 / 核心态切换,速度高、效率高。内核对 ULT 无感知,线程阻塞则进程(包含它的所有线程)阻塞。

内核空间会维护 [进程表]进程表中记录进程 [运行的程序代码] 集合。线程阻塞,其所在的进程也会阻塞。操作系统内核无法感知用户线程,对于线程的创建、调度、撤销等无感知。

优缺点

  • 优点:由于操作系统内核无法感知用户线程,对于线程的创建、调度、撤销等无感知。所以当线程进行调度、创建等,不会涉及到用户态和内核态之间的转化以及线程上下文的切换,资源消耗小、效率较高。
  • 缺点:需要维护复杂的线程生命周期,编程和实现复杂;线程维护在进程中,内核无感知,线程阻塞会阻塞整个进程(包含整个进程的其他线程);用户线程不能充分利用多核处理器进行并行运行线程,只能在单个核中运行。

线程模型

线程模型主要分为三种

  • 一对一模型
  • 多对一模型
  • 多对多模型

一对一模型

一对一模型 简易

用户空间的一个线程 (应用程序的线程概念) 对应内核的一个线程,1:1。用户空间的线程通过 LWP(属于用户空间) 对内核线程进行创建、销毁等操作。由于每个线程的创建、调度、销毁都需要内核的支持,每次线程的创建、切换都会设计用户状态 / 内核状态的切换,性能开销比较大,并且单个进程能够创建的 LWP 的数量是有限。能够充分里用多核的优势。

一对一模型

用户进程可以创建多个 (有限个)LWP 对内核线程进行管理 (包含创建、销毁等生命周期的方法),本质上还是操作系统进行的管理。KLT 是实际的计算运行的线程,在内核空间,由操作系统内核进行管理维护 (创建、调度、销毁等)。严格意义上,LWP 是属于操作系统层面的

一对一模型 详细

JVM 进程中通过 new Thread(Runnable/Callable) 创建 Java 层面的线程。Java 线程通过库调度器调用 LWP 的接口创建、销毁内核线程等。内核线程是由操作系统内核进行管理维护。

注:About LWP

  1. 其实 LWP(轻量级进程) 是操作系统提供的操作内核线程的入口 (接口),属于中间层。
  2. 在 Linux 操作系统中,往往都是通过 fork 函数创建一个子进程来代表内核中的线程,在 fork 完一个子进程后,还需要将父进程中大部分的上下文信息复制到子进程中,消耗大量 cpu 时间用来初始化内存空间,产生大量冗余数据。为了避免上述情况,轻量级进程 (Light Weight Process, LWP) 便出现了,其使用 clone 系统调用创建子进程,过程中只将部分父进程数据进行复制,没有被复制的资源可以通过指针进行数据共享,这样一来 LWP 的运行单元更小、运行速度更快。
  3. LWP 与内核线程一一映射,每个 LWP 都由一个内核线程支持
  4. LWP 可以被普通进程创建,有父子进程的关系。

多对一模型

多对一模型 简易

一个内核线程可以对应多个用户线程,即跟用户线程相匹配。用户线程的创建、调度、销毁不需要内核的支持,所以也就不涉及上下文切换的资源损耗,效率通常较高。但是内核无法感知到用户线程(只能感知到用户空间的进程),所以当一个进程中的一个线程阻塞,将会导致整个进程都阻塞。由于内核感知的是应用进程,所以进程中的多线程只能是运行在单个运算核上,无法充分利用计算性能并行计算,当然如果机器是单核就另当别论了;

多对一模型

此时内核直接管理进程,所有的资源类操作都是通过进程进行代为转发与内核进行通信,所谓的一直对外?

多对多模型

多对多模型 简易

这个模型其实就是一个混合的线程模型。LWP 和内核是 1:1 对应关系 (LWP 需要内核的支持才能工作,fork 等函数)。用户线程和 LWP 是 n:1(n>0) 对应关系。由于用户进程和 LWP(可以理解为内核线程) 是 n:m 的关系,所以即解决了 1:1 模型中性能开销及线程数量的问题,也解决了 N:1 模型中阻塞问题,同时也能充分利用 CPU 的多核优势。

多对多模型

此模型是大部分协程实现的基础。(Go语言)


小厂实习面试记录-004
https://zty-f.github.io/2022/05/28/小厂面试记录-004/
作者
ZTY
发布于
2022年5月28日
更新于
2025年1月2日
许可协议