[Java-jvm] 内存管理
[Java-jvm] 内存管理
JVM的内存结构介绍一下
注意
JMM 和 JVM内存结构是两个不同的概念。
- 线程私有:程序计数器、虚拟机栈、本地方法栈。
- 线程共享:堆、方法区。
什么是程序计数器?
可以看作是当前线程所执行的字节码的行号指示器。
介绍一下java虚拟机栈
每个方法在执行时都会创建一个栈帧,存储局部变量、操作数栈、动态链接和方法出口等信息。
栈帧会在方法调用时入栈,方法返回时出栈。
- 其特点在于:线程私有、生命周期与线程相同。
介绍一下本地方法栈
本地方法栈与虚拟机栈类似,但它为虚拟机使用到的本地方法服务。
扩展-什么是native方法
native方法是用于调用非Java代码的方法。可以通过JNI(Java Native Interface)实现与底层交互。
介绍一下 Java 堆
堆是JVM最大的一块内存区域,被所有线程共享,用于存放对象实例。
- 堆是垃圾回收器管理的主要区域
因此从内存回收的角度看,堆可以细分为新生代和老年代。
- 新生代(Young Generation):存放新创建的对象。又可以细分为 Eden、From Survivor 和 To Survivor 区域。
- 老年代(Old Generation):存放长时间存活的对象。
堆和栈的区别
- 堆属于线程共享的内存区域,存储对象实例,其生命周期由垃圾回收器管理。
- 栈属于线程私有的内存区域,存储方法调用的局部变量和操作数等信息,一般方法调用结束后自动释放。
介绍一下方法区
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- JDK8 以前被称为“永久代”,是堆的一部分。
- JDK8 开始被称为“元空间”,使用本地内存,不在受堆大小限制。
重点- 对象创建的过程了解吗?
当实例化一个对象时,JVM会经历以下几个步骤:
- 类加载检查:检查类是否已经被加载,如果没有则加载类。
- 分配内存:在堆上为对象分配内存空间。
- 对象内存初始化:将对象的成员变量初始化为默认值。
- 设置对象头:为对象设置对象头信息,包括类指针和一些标志位。
- 执行构造方法:调用对象的构造方法进行初始化。
对象的销毁过程了解吗?
对象的销毁过程主要由垃圾回收器负责。当一个对象不再被任何引用所指向时,垃圾回收器会将其标记为可回收,并在适当的时候回收。
堆内存是如何分配的?
使用指针碰撞和空闲列表。指针碰撞适合内存碎片化较少的内存区域,如新生代;空闲列表适合内存碎片化严重的区域,如老年代。
扩展-什么是指针碰撞
指针碰撞是指JVM会在以使用内存和空闲内存之间维护一个指针,当需要分配内存时,之间将这个指针向空闲内存方向移动即可。
扩展-什么是空闲列表
空闲列表是指JVM会维护一个链表,记录所有空闲内存块的信息。当需要分配内存时,JVM会遍历这个链表,找到一个足够大的空闲块进行分配。
new 对象时,堆会发生抢占吗?
会。
JVM为了解决内存抢占问题,会为线程保留一个线程本地分配缓冲区(TLAB),当线程需要分配对象时,会直接从TLAB分配,如果TLAB空间不足,才会使用全局分配指针。
重点- 对象的内存布局?
对象头、实例数据、对齐填充。
扩展-什么是对象头
对象头:对象的元信息,包含 Mark Word 和 类型指针等信息。
- Mark Word:存储对象在运行时的状态信息,包括锁状态、哈希码、GC标记等信息。
- 类型指针:指向对象所属类的元数据(即Class对象),用来支持多态和方法调用等功能。
扩展-什么是实例数据
实例数据:即对象的成员变量。
扩展-什么是对齐填充
对齐填充:为了满足CPU的内存访问效率,将内存地址对齐到特定的边界(通常是8字节)。
JVM 怎么访问对象的?
句柄 和 直接指针 两种方式。
- 句柄:
说一下对象有哪几种引用?
强引用、软引用、弱引用、虚引用
- 强引用:最常见的引用类型,只要对象被引用,则不会被垃圾回收器回收,即使内存不足。
- 软引用:当内存不足时,垃圾回收器会回收被软引用关联的对象。
- 弱引用:垃圾回收器在下一次回收时回收。
- 虚引用:虚引用任何时候都有可能被回收。
Java 堆的内存分区了解吗?
分为新生代和老年代。
- 新生代(Young Generation):存放新创建的对象。可以细分为Eden、From Survivor 和 To Survivor 区域。
- 老年代(Old Generation):当新生代经历多次GC仍然存活,会被移动到老年代。
说一下新生代的区域划分?
分为 Eden、From Survivor 和 To Survivor 区域。这种划分是为了配合“标记-复制算法”进行垃圾回收。
- 首先新创建的对象会被分配到Eden区,当Eden区被填满,会将存活的对象复制到空闲的 Survivor 区,并清空Eden区。
当下一次Eden区再被填满时,会将Survivor和Eden区的存活对象,复制到另一个Survivor区,并清空Eden区和前一个Survivor区。
循环这个过程,From 和 To 角色交替。
重点- 对象什么时候会进入老年代?
当新生代的对象经过多轮GC后仍然未被回收,超过一定阈值(一般为15次),则会将新生代的该对象移动到老年代。
STW 了解吗?
在进行垃圾回收时,会进行对象移动,为了保证对象在移动时不被修改,会暂停所有用户线程。这个过程被称为 Stop The World。
如何暂停线程
JVM 采用“主动式中断”策略。
- 当需要暂停所有线程时,JVM 会设置一个全局标志位
- 正在运行的 Java 线程在执行到安全点时,会主动检查这个标志位。
- 如果发现需要暂停,线程就会在安全点处挂起自己。
- 等待 JVM 完成全局操作后再恢复运行。
扩展-什么是安全点
安全点是JVM预设的、可以安全暂停线程的位置。
对象一定分配在堆中吗?
不一定。
因为JVM 可能会进行逃逸分析,如果对象的生命周期只在方法内部,那么这个对象就可以直接分配在栈中。
扩展-什么是逃逸
根据对象逃逸的范围,可以分为方法逃逸和线程逃逸。
- 方法逃逸:对象被方法外的代码引用,可能会被返回或者传递到其他方法中。
- 线程逃逸:对象被其他线程引用,可能会被多个线程共享。
上述情况都需要分配到堆中。
扩展-什么是逃逸分析
逃逸分析是JVM的一种优化技术,用于分析对象的动态作用域,判断对象是否会逃逸出方法或线程的范围。
内存溢出和内存泄漏了解吗?
- 内存溢出(OutOfMemoryError,OOM):当JVM无法为对象分配足够的内存时,会抛出内存溢出错误。
- 内存泄漏:当程序不再使用某个对象,但该对象仍然被引用,导致垃圾回收器无法回收该对象,从而占用内存资源。
内存泄漏可能由哪些原因导致呢?
- 长生命周期对象持有短生命周期对象的引用,导致短生命周期对象无法被回收。
- 静态集合类(如 HashMap、ArrayList)中存储了大量对象的引用,导致这些对象无法被回收。
- 资源未正确释放,如数据库连接、文件流等,导致相关对象无法被回收。
什么情况下会发生栈溢出?
当程序调用过深的递归方法,或者创建过多的线程时,可能会导致栈溢出(StackOverflowError)。
- 因为每个线程都有一个固定大小的栈空间,当超过这个空间时,就会发生栈溢出。