JVM内存模型
以Math类为例,体会下图JVM内存模型
            
            
              java
              
              
            
          
          public class Math {
    public static final int initData = 666;
    public static User user = new User();
    public int compute(int a, int b) {
        return a + b;
    }
}
        JVM内存模型如下:

- 堆和方法区都是线程共享的(上图JVM中淡橙色方块表示);
 - 栈、程序计数器、本地方法栈都是线程私有的(上图JVM中紫色方块表示);
 - 字节码执行引擎负责:执行字节码指令、修改程序计数器用于上下文切换找到指定代码行、执行gc等工作;
 - 元空间是方法区的一种实现,它将方法区中的元数据存储在直接内存中,而不是在堆内存中;
 - 堆和方法区都会导致full gc,也就是说直接内存不够时也会full gc;
 - 动态链接:将方法调用的符号转为内存地址,从而可以找到对应的对象或其他方法;
 - 方法出口:记录方法结束回到上一个栈帧后从哪里继续执行;
 - 本地方法栈:用于执行本地方法(如C/C++编写的方法),栈帧与Java栈帧不同,栈帧中包含了本地变量表、操作数栈、动态链接、方法出口等信息
 - 如上图局部变量表中的math的值是分配在堆上的math对象的地址值,所以说栈会有很多指针指向堆中的对象。
 - 在Math类中可以看到user静态变量,因此会分配到方法区,但是new user()对象还是会分配到堆中,类似这种情况也会使得方法区有很多指针指向堆。
 
在minor gc过程中对象挪动后,引用如何修改?
1. 对象复制机制
对象在堆内部挪动的过程其实是复制,原有区域对象还在,一般不直接清理。JVM内部清理过程只是将对象分配指针移动到区域的头位置即可:
- 扫描S0区域,扫到GC Root引用的非垃圾对象时,将这些对象复制到S1或老年代
 - 扫描完成后,将S0区域的对象分配指针移动到区域的起始位置
 - S0区域之前的对象并不直接清理,当有新对象分配时,原有区域里的对象才会被覆盖
 
2. 引用更新机制
Minor GC在根扫描过程中会记录所有被扫描到的对象引用(在年轻代这些引用很少,因为大部分都是垃圾对象不会扫描到),如果引用的对象被复制到新地址了,最后会一并更新引用指向新地址。
具体的引用更新过程:
第一阶段:标记和复制
            
            
              css
              
              
            
          
          1. 从GC Root开始遍历所有可达对象
2. 将存活对象从Eden区和From Survivor区复制到To Survivor区或老年代
3. 在复制过程中,JVM会维护一个转发表(Forwarding Table)
        第二阶段:引用更新
            
            
              markdown
              
              
            
          
          1. 遍历所有已知的引用位置(栈、方法区、其他堆对象)
2. 检查这些引用是否指向已移动的对象
3. 如果指向已移动对象,则根据转发表更新引用地址
        3. 转发表(Forwarding Table)机制
转发表是JVM用来跟踪对象移动的关键数据结构:
- 记录映射关系:原地址 → 新地址
 - 快速查找:通过哈希表或类似结构实现O(1)查找
 - 临时存在:只在GC过程中存在,GC完成后释放
 
4. 引用更新的范围
需要更新引用的位置包括:
- 栈中的引用:局部变量表中的对象引用
 - 方法区中的引用:静态变量、常量池中的引用
 - 堆中的引用:其他对象的字段引用
 - JNI引用:本地方法中的对象引用
 
5. 性能优化策略
记忆集(Remembered Set):
- 记录跨代引用关系,避免全堆扫描
 - 只需要更新记忆集中记录的引用位置
 - 大大减少了引用更新的工作量
 
并发更新:
- 在某些GC算法中(如G1),引用更新可以并发进行
 - 减少STW(Stop The World)时间
 
6. 示例说明
            
            
              java
              
              
            
          
          // GC前的状态
Object obj1 = new Object(); // 地址:0x1000
Object obj2 = new Object(); // 地址:0x2000
obj1.field = obj2;          // obj1引用obj2
// Minor GC过程中
// 1. obj1复制到新地址:0x3000
// 2. obj2复制到新地址:0x4000
// 3. 更新引用:obj1.field = 0x4000
// GC后的状态
// obj1现在在地址0x3000
// obj2现在在地址0x4000
// obj1.field正确指向0x4000
        这种机制确保了在对象移动后,所有引用都能正确指向新的内存位置,保证了程序的正确性。
关于元空间(Metaspace)和方法区(Method Area)的关系
1. 方法区的概念
- 方法区是JVM规范中定义的一个逻辑区域,用于存储类的元数据信息
 - 它是线程共享的内存区域,存储了每个类的结构信息,如:
- 类的字段和方法数据
 - 方法的字节码
 - 常量池信息
 - 类和方法的访问修饰符
 
 
2. 元空间的本质
- 元空间是HotSpot JVM对方法区的具体实现方式
 - 在JDK 8及以后版本中,元空间取代了之前的永久代(PermGen)
 - 它将类的元数据存储在直接内存(Native Memory)中,而非Java堆内存
 
3. 直接内存 vs 堆内存的区别
堆内存存储特点:
- 受JVM管理,有明确的大小限制(-Xmx参数控制)
 - 参与垃圾回收过程
 - 内存分配和释放由JVM控制
 
直接内存存储特点:
- 属于操作系统内存,不受JVM堆大小限制
 - 默认情况下只受操作系统可用内存限制
 - 可以动态扩展,避免了永久代的内存溢出问题
 
4. 这种设计的优势
- 避免OOM问题:不再受限于固定的永久代大小
 - 更好的内存管理:元数据的生命周期与类加载器绑定,更容易管理
 - 性能提升:减少了堆内存的压力,GC过程更加高效
 
因此,这句话准确地描述了元空间作为方法区实现的本质特征:它改变了元数据的存储位置,从堆内存迁移到直接内存,从而解决了传统永久代的一些限制。