作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
学习必须往深处挖,挖的越深,基础越扎实!
阶段1、深入多线程
阶段2、深入多线程设计模式
阶段3、深入juc源码解析
码哥源码部分
码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】
码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】
码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】
码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】
打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】
什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
Java 虚拟机是 Java 程序运行环境的一部分,负责执行 Java 字节码,它是 Java 实现平台无关性的基石。
Java 程序首先被编译成一种中间形式,即字节码(.class 文件),这些字节码不针对任何特定的硬件或操作系统。当运行 Java 程序时,JVM 在实际的硬件平台上解释执行这些字节码,实现了“一次编写,到处运行”(Write Once, Run Anywhere, WORA)的理念。因此,Java 程序无需为不同平台重新编译,只要对应平台上有相应的 JVM 实现,Java 程序就可以在上面运行。
这种设计有如下几个好处:
-
跨平台性:只需编写一次代码,就可以在多个平台上运行,无需针对每个操作系统写特定的代码。
-
安全性:JVM 提供了一个隔离的运行环境,可以在执行 Java 程序时对其进行检查和限制,减少安全风险。
-
移植性:由于 JVM 屏蔽了底层硬件和操作系统的差异,Java 程序更容易从一个平台迁移到另一个平台。
内存管理
说一下 JVM 的主要组成部分及其作用?
JVM 的主要组成部分如下:
- 类加载器(Class Loaders):
- 负责加载 Java 类到 JVM 中。它在运行时将 Java 的
.class
文件加载到内存中,并为它们创建对应的Class
对象。 - 类加载器按照父级委派模型工作,首先会请求父类加载器加载类,只有在父类加载器加载失败时,才会尝试自己加载。
- 负责加载 Java 类到 JVM 中。它在运行时将 Java 的
- 运行时数据区(Runtime Data Areas):
- 方法区(Method Area):存储每个类的结构,如运行时常量池、字段和方法数据,方法和构造函数的代码,以及类型、方法和构造函数的特殊方法。
- 堆(Heap):JVM 中内存最大的一块,用于存储所有类实例和数组。垃圾收集器主要的工作区域也在这里。
- 栈(Stacks):存储局部变量和部分结果,并参与方法调用和返回。每个线程拥有自己的调用栈。
- 程序计数器(PC Register):当前线程所执行的字节码的行号指示器。
- 本地方法栈(Native Method Stacks):为 JVM 使用到的 Native 方法服务。
- 执行引擎(Execution Engine):
- 负责执行类文件中的指令。它包括一个虚拟处理器、解释器、及即时编译器(JIT 编译器)。
- 解释器:快速解释字节码,但执行同样代码的效率较低。
- 即时编译器(JIT):编译热点代码(频繁执行的代码)为本地机器码,提高执行效率。
- 垃圾回收器(Garbage Collector):
- 负责回收堆内存中不再被使用的对象。垃圾回收器的存在使得 Java 程序员不需要手动管理内存,减少了内存泄漏和指针失误的问题。
- 本地接口(Native Interface):
- 提供了一个接口,用于交互 Java 代码和本地库,允许 Java 程序调用或被其他语言如 C/C++ 写的程序调用。
- 本地库(Native Libraries):
- 由 JVM 使用的标准库,这些库通常用 C/C++ 编写,被本地接口调用。
32 位和 64 位的 JVM,int 类型变量的长度是多数?
在 Java 中,数据类型的大小不受 JVM 是 32 位还是 64 位的影响。这是 Java 语言的一部分规范,确保了 Java 程序的可移植性。因此,无论在 32 位还是 64 位的 JVM 上,int
类型的变量长度都是 32 位(4 字节)。这同样适用于其他基本数据类型,如 byte
、short
、long
、float
、double
、char
和 boolean
,它们的大小在不同的 JVM 架构中都是一致的。
请详细介绍下 JVM 运行时数据区
VM 运行时数据区域是 Java 虚拟机定义的内存区域,用于在 Java 程序运行期间存储数据。这些区域包括堆(Heap)、方法区(Method Area)、虚拟机栈(VM Stacks)、程序计数器(Program Counter Register)和本地方法栈(Native Method Stacks)。
- 堆(Heap):JVM中最大的一块内存区域,它被所有Java线程共享。在堆中,主要存放了Java应用创建的对象实例和数组。因为这些对象的生命周期不总是确定的,所以堆也是垃圾收集器的主要工作区域,以确保释放那些不再被任何引用的对象所占用的内存。
- 方法区(Method Area):同样是线程共享的内存区域,它用来存储每个类的结构信息,比如类的名称、直接父类的名称、方法和变量的信息以及编译后的代码等。在一些JVM实现中,方法区可能被称为"永久代"(PermGen),但从Java 8开始,已经被元空间(Metaspace)所取代。
- **Java栈(Java Stack)**则是线程私有的,它的生命周期与线程相同。每个栈由多个栈帧组成,每个栈帧对应着一次Java方法调用。栈帧中包含了局部变量表、操作数栈、动态链接信息以及方法返回时的操作。
- **程序计数器(Program Counter Register)**是每个线程私有的内存区域,它包含了当前线程所执行的字节码的行号指示器。如果执行的是Java方法,计数器记录的是字节码指令的地址;如果执行的是Native方法,计数器的值则是undefined。
- **本地方法栈(Native Method Stack)**与Java栈相似,但它被用来支持Java中的Native方法(即用其他语言如C或C++编写的方法)。当线程调用一个Native方法时,它的调用状态会被压入本地方法栈。
说一下堆栈的区别?
堆(Heap):
- 堆是由 JVM 运行时管理的内存区域,用于存放 Java 应用程序创建的对象和数组。
- 堆内存是在所有线程之间共享的,对象无固定的存活周期,存活时间可能从应用程序开始到结束。
- 内存的分配和回收是动态的,垃圾收集器负责回收那些不再被任何引用的对象,防止内存泄漏。
- 堆的大小和生命周期是可以调节的,通常有一个固定的开始大小,但它会随着对象的创建和回收动态扩展或收缩,直到达到 JVM 配置的限制。
栈(Stack):
- 栈是线程私有的内存区域,每个线程都有自己的栈,用于存储局部变量和部分结果,以及用于方法调用和返回。
- 栈内存中的数据有明确的生命周期,遵循“先进后出”(LIFO)的原则。每个方法调用时会创建一个栈帧,方法结束时,对应的栈帧会被销毁。
- 对于栈内存的分配和回收,操作更快速而且效率较高,因为它不需要复杂的垃圾收集算法,每个方法结束后,局部变量就会自动释放。
- 栈的大小通常比堆小,且不可动态扩展,每个线程的栈空间在线程创建时被分配。
总的来说,堆是用于存储对象和数组的内存区域,是线程共享的,其对象的生命周期不固定,由垃圾收集器管理。而栈是线程私有的内存区域,用于基本类型的局部变量和对象引用,其数据随着方法调用而创建,方法结束而销毁。
对象创建的过程了解吗?
详细过程如下:
- 类加载检查:
- 当代码试图创建对象时,JVM 首先检查这个类的
Class
对象是否已经被加载、链接和初始化。如果没有,那么JVM会执行类加载过程。
- 当代码试图创建对象时,JVM 首先检查这个类的
- 内存分配:
- 一旦确定类已经被加载,JVM 接下来将在堆上为新对象分配内存。
- 对象所需的内存大小在类加载完成后即已知,包括对象的所有实例变量和其他开销(如元数据信息、对齐填充等)。
- 内存分配可以通过「指针碰撞」(如果内存是绝对连续的)或者「空闲列表」(如果内存是非连续的)方式来实现。
- JVM 实现可能会采用垃圾回收算法中的分代收集算法,在这种情况下,新创建的对象通常会被放在堆的年轻代(Young Generation)。
- 初始化零值:
- 内存分配后,JVM 将分配的内存空间初始化为零值。这确保了对象的实例变量不会有未知的值。
- 设置对象头:
- JVM 会在对象的内存中设置对象头(包括类的元数据信息、哈希码、垃圾回收信息等)。
- 对象头标识了对象的运行时类型和锁状态等信息。
- 执行
<init>
方法:- 对象头设置完毕后,JVM 会调用构造函数。构造函数可能会调用父类的构造函数,并执行所有初始化块。
- 构造函数中的代码将负责设置实例变量的初始值,这些值可能与默认零值不同。
- 引用分配:
- 完成对象的初始化后,JVM 将分配的内存地址赋值给引用变量,此时对象才算是完全创建好了。
整个对象创建过程是由 JVM 的类加载器、内存分配子系统、执行引擎等协作完成的。这个过程中,可能会触发垃圾收集活动,特别是在堆内存不足时。同时,虚拟机也可能对对象的内存分配和初始化过程进行优化,如通过逃逸分析判断对象是否可以在栈上分配,或者通过即时编译器优化对象的创建代码。
怎么获取 Java 程序使用的内存?堆使用的百分比?
- 使用命令行工具:
jstat
命令可以用来查看堆内存以及其他各种运行时数据区的使用情况。- 使用
jstat -gc <pid>
可以看到年轻代、老年代以及永久代/元空间的当前使用情况。
- 使用 JConsole 或 VisualVM:
- 这些是 Java 提供的可视化工具,可以用来监控 Java 应用程序的内存使用、线程使用等运行时数据。
- 它们可以显示堆内存的使用情况,包括初始大小、已用大小、提交大小和最大大小。
- 使用 Runtime 类:
Runtime
类提供了与 Java 应用程序的运行时环境相关的方法。Runtime.getRuntime().totalMemory()
:获取 JVM 的总内存量。Runtime.getRuntime().freeMemory()
:获取 JVM 的空闲内存量。Runtime.getRuntime().maxMemory()
:获取 JVM 尝试使用的最大内存量。- 堆使