-
JVM内存模型是什么?(B站)JVM内存模型中的堆和栈的区别?
(1)JVM的内存模型
JVM 运行时内存共分为虚拟机栈、本地方法栈、堆、元空间(方法区)、程序计数器五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存。
- 虚拟机栈:每个线程都会有一个虚拟机栈,用来存储局部变量、操作栈、动态链接和方法出口等信息。每个方法在执行时都会创建一个栈帧,用来存储该方法的局部变量和中间计算结果。
- 本地方法栈:与虚拟机栈类似,本地方法栈是用于处理调用本地方法(Native Method)的栈,存放本地方法的参数和返回地址等。
- 堆:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例。
- 方法区:用于存储类的信息、常量、静态变量、即时编译器(JIT)编译后的代码等。在 JDK 8 中,方法区被分为元空间(Metaspace)。Metaspace 不是在 JVM 堆中分配的,而是使用本地内存。
- 程序计数器:每个线程都有一个独立的程序计数器,用于记录当前线程正在执行的字节码指令的地址。它是线程私有的,不会被其他线程共享。
(2)JVM内存模型里的堆和栈的区别?
栈 堆 用途 存储局部变量、方法调用的参数、方法返回地址以及一些临时数据 用于存储该方法的信息,当方法执行完毕,栈帧也会被移除。 生命周期 生命周期确定,每当一个方法被调用,一个栈帧(stack frame)就会在栈中创建,用于存储该方法的信息,当方法执行完毕,栈帧也会被移除。 生命周期不确定,用于存储该方法的信息,当方法执行完毕,栈帧也会被移除。 存取速度 栈遵循先进后出(LIFO, Last In First Out)的原则,存取速度比堆快。 堆的存取速度相对较慢,因为对象在堆上的分配和回收需要更多的时间而且垃圾回收机制的运行也会影响性能。 存储空间 栈的空间相对较小,且固定,由操作系统管理。当栈溢出时,通常是因为递归过深或局部变量过大。 堆的空间较大,动态扩展,由JVM管理。堆溢出通常是由于创建了太多的大对象或未能及时回收不再使用的对象。 可见性 栈中的数据对线程是私有的,每个线程有自己的栈空间。 堆中的数据对线程是共享的,所有线程都可以访问堆上的对象。 - 栈中存的到底是指针还是对象?
栈中存储的不是对象,而是对象的引用。也就是说,当你在方法中声明一个对象,比如
MyObject obj= new MyObject();
,这里的 obj 实际上是一个存储在栈上的引用,指向堆中实际的对象实例。这个引用是一个固定大小的数据(例如在64位系统上是8字节),它指向堆中分配给对象的内存区域 -
内存泄漏和内存溢出?(腾讯)ThreadLocal如何导致内存泄漏?
-
内存泄露:内存泄漏是指程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。虽然在Java中,垃圾回收机制会自动回收不再使用的对象,但如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,最终可能导致程序的内存使用不断增加。
- 静态集合:使用静态数据结构(如 Hashmap或 ArrayList )存储对象,且未清理。
- 事件监听:未取消对事件源的监听,导致对象持续被引用。
- 线程:未停止的线程可能持有对象引用,无法被回收。
-
内存溢出:内存溢出是指Java虚拟机(IM)在申请内存时,无法找到足够的内存,最终引发这通常发生在堆内存不足以存放新创建的对象时。OutOfMemoryError。
- 大量对象创建:程序中不断创建大量对象,超出JM堆的限制。
- 持久引用:大型数据结构(如缓存、集合等)长时间持有对象引用,导致内存累积。
- 递归调用:深度递归导致栈溢出。
ThreadLocal如何导致内存泄漏?
ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的value。
如果当前线程迟迟不结束的话,这些kev为null的Entry的value就会一直存在一条强引用链:Thread Ref ->Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
如何解决此问题? -
移除值:使用
ThreadLocal.remove()
方法来清除当前线程中的值,避免内存泄漏。- 不要使用
set(null)
:set(null)
并不会清除ThreadLocal
中的值,而是将其值设置为null
,导致内存泄漏。 - 确保清理:应当始终在
finally
块中使用remove()
方法清除ThreadLocal
中的值,确保资源能够及时释放,避免在多线程环境中引发内存泄漏或数据污染问题。
- 不要使用
-
JVM的垃圾回收机制?判断垃圾(对象死亡)的方法?
垃圾回收(Garbage Collection,GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。
基本原理:
垃圾回收的核心任务是寻找并回收不再使用的对象(即垃圾对象)。这些对象通常是堆内存中不再被引用的对象。JVM垃圾回收分为以下几个阶段:
- 标记(Mark):识别哪些对象是“活着的”,即被引用的对象。
- 清除(Sweep):将不再使用的对象进行清理,释放其占用的内存。
- 压缩(Compact):为了避免内存碎片,回收后的内存区域会进行整理,将存活的对象压缩到堆的一个连续区域。
回收的内存区域:
-
新生代(Young Generation):包含新创建的对象。新生代又分为三个区域:
Eden区
和两个Survivor区
(S0
和S1
)。新生代的垃圾回收称为 Minor GC,回收速度较快。 -
老年代(Old Generation):存放生命周期较长的对象。当新生代对象经历多次垃圾回收仍然存活时,它们会被移到老年代。老年代的垃圾回收称为 Major GC 或 Full GC,回收较慢。
-
元空间(Metaspace):用来存储类的元数据(类信息、方法信息等),并不在堆内存中。从 Java 8 起,元空间替代了之前的
PermGen
区域。
判断垃圾(对象死亡)的方法:
- 引用计数法:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数.器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。
- 可达性分析算法:从一组称为GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到GC Roots没有任何引用链相连,即从GC Roots到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。
回收算法 和 回收器:
-
CMS(Concurrent Mark sweep)收集器(标记-清除算法): 老年代并行收集器。
标记-清除算法:标记-清除算法分为“标记“和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有一个缺陷,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
-
G1(Garbage first)收集器 (标记-整理算法): Java堆并行收集器,G1回收的范围是整个Java堆(包括新生代,老年代)。
标记-整理算法:标记-整理算法的“标记“过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
JVM 的垃圾回收器不仅仅会对堆进行垃圾回收,它还会对方法区进行垃圾回收。
-
jvm的哪些区域会被回收,方法区会被回收吗?
- 堆(Heap):
- 堆是JVM中最大的一块内存区域,主要用于存储对象实例。垃圾回收器主要回收的是堆区中未使用的内存区域,并对相应的区域进行整理。
- 堆主要分为新生代(Young Generation)和老年代(Old Generation)。新生代中的对象大多是朝生夕死,适合使用复制算法进行垃圾回收。老年代中的对象存活率高,适合使用标记-清理或者标记-整理算法进行回收。
- 方法区(Method Area):
- 方法区用于存储已被JVM加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。方法区也有垃圾回收机制,主要是对不再使用的类进行回收。
- 方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类(主要是针对于类相关的变量/方法,也就是static修饰的)。
- 要判定一个类是否可以被卸载,需要同时满足以下三个条件:此类所有的实例对象都已经被回收;加载该类的类加载器已经被回收;该类对应的java.lang.Class对象没有被其他对象引用。
- 堆(Heap):
-
如何监控和调整JVM的垃圾回收行为?你是如何使用jmap,你用过哪些命令?(B站)
jps、jstat、jmp
-
Linux哪个命令可以查看CPU使用情况?(字节)如何查看进程?你说的“ps -ef | grep 关键字”,关键字查不到,但是进程确定是活的,这是什么原因导致的?(百度)
CPU使用情况:top命令、htop命令。
查看进程:
- ps -aux 查看当前用户的所有进程
- ps -ef 查看当前终端的进程
- ps -p 查看某个特定进程的详细信息
- ps -ef | grep 根据进程名称查找进程的匹配行
查不到关键字的原因:
-
进程属于不同的用户或权限问题
-
如果在容器化环境中运行进程(如 Docker 容器),在宿主机上直接使用
ps -ef
可能无法看到容器内部的进程。这是因为容器内的进程可能并未暴露在宿主机的进程表中。
-
如果我想查询一个日志里面某一个单词,不含有空格,出现在多少行里面,如何写linux命令?比如说在日志里面查设备编号id,在哪一行?(百度)
grep -n "id" /path/to/logfile
grep
:用于搜索文件中的内容。-n
:显示匹配行的行号。"id"
:要查找的单词或字符串,注意如果有特殊字符或需要精确匹配,可以加上引号。/path/to/logfile
:日志文件的路径。其他的选项:-c 选项会返回匹配到的行数,可以统计该单词(
id
)出现了多少行 -
描述git的存储结构;描述一下git的三棵树;介绍一下git中reset、restore、revert指令的区别;使用 git完成的过程是怎样的?Commit之后发生了什么?(百度)
(1)git的存储结构
Git 使用一种叫做对象数据库(Object Store)的机制来存储提交记录。在执行
commit
时,Git 会为当前的提交内容生成唯一的 SHA-1 哈希值(commit hash),并将这个哈希值与提交内容(包括文件差异、元数据等)存储在本地.git/objects
目录中。提交内容本身(文件的快照)也会被存储为 Git 对象。
(2)git的三棵树
-
Git 仓库(.git directory):它存储了项目的所有提交历史。通常位于项目根目录下的
.git
文件夹内,它是一个隐藏的目录,包含了项目的所有版本控制数据。存储所有的提交记录(历史版本)、对象(如 Blob、Tree 和 Commit)以及相关的配置信息。 -
工作目录(Working Directory):这是你当前查看和编辑的文件夹。它存储了项目的所有文件,是你本地的实际文件系统视图。这些文件可能是修改过的、未修改的或者已删除的,Git并不直接跟踪这些文件,只有在文件被添加到暂存区(staging area)时才会被Git管理。
-
暂存区域(Staging Area):暂存区是一个用于保存你对文件的修改准备提交的区域。它是一个临时区域,用户可以选择哪些修改会包含在下一次提交中。使用
git add
命令时,Git会将文件的当前状态放入暂存区,但这些更改还没有被提交到本地仓库。
(3)git中的reset、restore、revert指令的区别
reset
:用于重置当前分支,修改暂存区和工作目录的状态。可以用来回退到之前的某个提交,可以选择保留或丢弃更改。git reset --soft <commit>
:将当前分支指针移动到指定的<commit>
,但是保留工作目录和暂存区的更改。也就是说,提交历史回退,但是你未提交的修改保持在暂存区。
restore
:用于恢复文件或目录的状态,可以撤销工作目录或暂存区中的更改。git restore <file>
:将某个文件恢复到最新提交的状态,撤销工作目录中的更改。
revert
:用于撤销某个提交,创建一个新的提交来反向应用之前的更改,不改变历史。git revert <commit>
:撤销指定的提交,并创建一个新的提交记录。revert
会保留撤销的提交,并且将撤销操作作为新的提交进行记录。reset
是改变历史和状态,restore
是撤销文件级别的更改,而revert
是通过创建新的提交来“撤销”之前的某个提交,适用于保持公共历史不变的场景。(4)工作流程
-
在工作目录中修改文件。
-
暂存文件,将文件的快照放入暂存区域。
-
提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。
(5)Commit之后发生了什么?
Commit 后,Git 会将暂存区中的更改保存为一个新的提交对象,并将该提交记录到当前分支的历史中。
HEAD
指针会更新到新的提交对象,工作目录和暂存区的状态保持不变,但暂存区内容被清空,表示更改已经被记录。 -