1、类加载机制?
类加载指的是 JVM 通过类加载器,把.class文件加载到方法区,并在JVM堆区建一个 java.lang.Class的 实例,用来封装 Java 类相关数据和方法。一般说的类加载机制,指的是双亲委派机制,即如果一个类加载器加载了一个类,它首先不是自己去加载这个类,而是把请求委派给父加载器,以此类推...。如果父加载器加载不了,它自己才会加载,自己也加载不了就抛出ClassNotFoundException。
JVM 判定一个类是否相同有 2 个条件:1 、包+类名一致;2 、同一个类加载器加载的
类加载器从顶到下分别为 Bootstrap ClassLoader(启动类加载器)、 ExtClassLoader(扩展类加载器)、AppClassLoader(应用类加载器)
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) {
long t0 = System.nanoTime();
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.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2、如何打破双亲委派机制?
A:继承ClassLoad抽象类,覆写loadClass方法。Tomcat 通过自定义WebAppClassLoader,优先加载Web 应用目录下的类,然后再加载其他目录下的类,进而打破双亲委派机制。具体参加:https://time.geekbang.org/column/article/105110
3、双亲委派机制的好处?
A:一是避免同类(指的同包+同名+同类加载器实例)被加载多次,二是因为优先加载父类加载器,保护 Java 核心 api的类不会被篡改。
4、类加载过程或类的生命周期?
A:类的生命周期分为 Loading(加载)、 Linking(连接)、Initialization(初始化)、Using(使用)、 UnLoading(卸载)阶段。
类加载过程分 3 步:加载->连接->初始化,其中连接过程又分为 3 步:验证、准备、解析。
- 加载:类加载器通过类全名获取类二进制字节流;将字节流所代表的静态存储结构,转化为方法区的运行时数据结构;在内存生成代表该类的 Class对象,作为方法区这些数据的访问入口。
- 验证:是连接阶段的第一步,目的是确认加载的Class 文件字节流符合《Java 虚拟机规范》,主要验证点包含Class文件格式检查、元数据验证(字节码语义)、字节码验证(程序语义)、符号引用验证(类的正确性检查)
- 准备:为类变量分配内存,并为其设置初始值。需要注意的是,从 JDK1.7 及以后版本,HotSpot已经将字符串常量池和类静态变量等移动到堆中,这时候类变量会随着 Class 对象一起放到堆中
- 解析:JVM 将常量池内的符号引用替换为直接引用的过程,也就是得到类或字段、方法在内存的地址或偏移量
- 初始化:是直接<clinit>方法的过程,是类加载的最后一步,这一步JVM 才开始执行类中定义Java程序代码(字节码)
类卸载过程:即该类的 Class 对象被 GC 。需满足 3 个条件:该类的所有实例都已被 GC,即堆里不存在该类的实例对象;该类没在其他任何地方引用;该类的类加载器已被 GC。
5、介绍下 Java 内存区域(运行时数据区)
A:分为线程私有区、线程共享区,线程私有区包括程序计数器、虚拟机栈、本地方法栈,线程共享区包括方法区、堆。
- 程序计数器(Program Counter Register):存储下个指令的地方,无垃圾回收、无 OOM
- 虚拟机栈(VM Stack):一句话理解,栈是用来解决程序运行问题的;二句话理解,调用方法,创建栈帧,入栈,方法结束,出栈。栈帧是一块内存区域,维系着方法执行的各种数据,包含局部变量表、操作数栈、动态链接、方法返回地址。常见问题:若栈的大小不允许动态扩展,超过设置的固定大小,报StackOverFlowError;若栈允许动态拓展,没有空闲的内存空间时,报 OutOfMemoryError
- 本地方法栈(Native Method Stack):与虚拟机栈作用类似,也会出现StackOverFlowError和 OutOfMemoryError。虚拟机栈是为 Java 程序服务的,本地方法栈是虚拟机使用的 native 方法服务
- 方法区:虚拟机使用一个类时,会把 Class字节码文件加载到方法区存储。方法区会存储类信息、字段信息、方法信息、 JIT 代码缓存、运行时常量等。永久代和元空间是 HotSpot对方法区的两种实现,永久代是 JDK1.8 之前的实现,元空间是 JDK1.8 及以后的实现。元空间使用的本地内存,受本机内存限制,元空间溢出时,报OutOfMemoryError
- 堆(Head):一句话,解决对象分配存放的问题。从JDK1.7,字符串常量池和静态变量存储在堆。堆分为新生代(Young Generation)、老年代(Old Generation),其中新生代包括 Eden 、 S0 、S1。两种情况会出现OutOfMemoryError,当 JVM 花太多时间执行 Full GC,但却只能回收少量空间;或堆空间不足以存放新对象。
Q:常见概念
A:新生代垃圾回收:Minor GC/Young GC;老年代垃圾回收:Major GC;整堆垃圾回收:Full GC,回收整个堆和方法区;混合收集(Mix):Mix GC, G1中的概念,收集整个新生代和部分老年代
6、对象在堆上分配的过程?
A:情况 1:对象首先会分配到 Eden 区;情况 2:如果是大对象,直接进入老年代;如果 Eden 容纳不下,发生一次 minor GC,Eden 和 S0 一起转移到 S1区,此时仍有存活对象(S1 区),其年龄+1;下次 Eden 又满了, 发生一次minor GC,Eden+S1进入 S0,年龄又+1,当年龄大于阈值,进入老年代。
7、进入老年代的情况?
A:大对象直接进入老年代;长期存活的对象进入老年代;对象年龄动态判断,s区相同年龄的对象大小总和>s区空间一半,那么大于等于此年龄的对象进入老年代;minor GC过程中如果 S 区容纳不下,则通过分配担保机制进入老年代。
8、堆、栈、方法区的关系?
A:对象引用分配到栈,对象分配到堆,类信息存储在方法区
9、Java 对象的创建过程
- 步骤 1、类加载检查
虚拟机遇到new指令时,首先检查常量池中是否存在类的符号引用,若未加载则触发类加载流程(加载.class文件到方法区,解析并初始化类元数据)。若类已加载,则直接进入内存分配阶段。
关键点:
符号引用:编译时用类全限定名代替实际内存地址,类加载时解析为元数据指针。
懒加载机制:仅当使用类时才会加载。 - 步骤 2、内存分配
在堆中为对象划分内存空间,具体方式取决于堆内存是否规整:
指针碰撞(Bump The Pointer),堆内存规整(已用内存与空闲内存分离),通过移动分界指针快速分配空间。适用场景:使用Serial、ParNew等带压缩功能的垃圾收集器。
空闲列表(Free List),堆内存不规整时,维护空闲块列表,按需分配并更新列表。适用场景:CMS等不带压缩功能的收集器。
并发安全处理:
CAS + 重试:通过原子操作保证指针移动的线程安全。
TLAB(线程本地分配缓冲):为每个线程预分配堆内存区域,减少竞争。 - 步骤 3、初始化零值
将分配的内存空间初始化为默认值(如int为0,引用类型为null),确保未显式赋值的字段可安全使用。若使用TLAB,此步骤可能提前执行。 - 步骤 4、设置对象头
在对象头中存储元数据信息,包括:类型指针:指向类的元数据(如class对象)。哈希码:对象内存首地址的哈希值(默认)。GC信息:分代年龄、锁状态等。对象布局:对象头(含Mark Word和元数据指针)、实例数据、对齐填充。 - 步骤 5、执行构造方法(方法)
从Java程序视角,对象初始化正式开始:调用构造函数,执行字段显式赋值、实例初始化块等操作。将堆内存地址赋值给引用变量,完成对象可用性。 - 步骤 6、返回对象引用
将对象的内存地址返回给变量,供后续使用。 - 总结:Java对象创建流程可概括为:类加载 → 内存分配 → 零值初始化 → 对象头设置 → 构造方法执行 → 返回引用。核心难点在于内存分配策略(指针碰撞/空闲列表)和并发安全机制(CAS/TLAB),需结合垃圾收集器特性理解。掌握此流程有助于优化内存管理和排查内存泄漏问题。
10、对象的访问定位的两种方式(句柄和直接指针两种方式)
- 方式 1:句柄访问(Handle)
[ 栈(Stack) ]
| 引用变量 → [ 句柄池(Handle Pool) ] → [ 对象实例数据 ]
| |
| → [ 类元数据(Class Data) ] (存储在方法区)
图示说明:
- 栈:存储引用变量,指向句柄池中的句柄。
- 句柄池:包含对象实例数据地址和类型指针。
- 对象实例数据:堆中的实际对象内容。
- 类元数据:方法区中的类信息(如字段、方法)。
特点: - 引用地址稳定,对象移动时只需修改句柄中的实例指针。
- 额外一次间接寻址,性能略低。
方式 2:直接指针(Direct Pointer)
[ 栈(Stack) ]
| 引用变量 → [ 对象头(Header) ] → [ 类元数据(Class Data) ]
| [ 实例数据(Instance Data) ]
| [ 对齐填充(Padding) ]
图示说明:
- 栈:引用变量直接指向堆中的对象头。
- 对象头:包含类型指针(指向方法区类元数据)和哈希码等元信息。
- 实例数据:对象的实际字段内容。
特点: - 一次直接寻址,访问速度快。
- 对象移动时需更新所有相关引用地址。
HotSpot虚拟机使用的直接指针。
参考文档











网友评论