内存结构

程序计数器

1、Program Counter Register:程序计数器(寄存器)

(1)程序计数器是 Java 对物理硬件的屏蔽与抽象

(2)物理上,通过寄存器实现程序计数器

2、作用:记录下一条 JVM 指令的执行地址

(1)解释器会解释指令为机器码,交给 CPU 执行,程序计数器记录下一条指令的地址行号,下一次解释器会从程序计数器拿到指令,然后进行解释执行

(2)多线程的环境下,如果两个线程发生上下文切换,则程序计数器记录线程下一行指令的地址行号,便于继续往下执行

4、特点

(1)线程私有

(2)在 JVM 中,唯一一个,不会出现内存溢出的部分

 

虚拟机栈

1、定义

(1)每个线程运行需要的内存空间,称为虚拟机栈

(2)每个栈由多个栈帧(Frame)组成,对应着每次调用方法时,所占用的内存(参数、局部变量、返回地址等)

(3)每个线程只能有一个活动栈帧,对应着当前正在执行的方法

2、解析

(1)垃圾回收不涉及栈内存:栈内存由方法调用产生,方法调用结束后会自动弹出栈

(2)栈内存并非分配越大越好:因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少,一般采用系统默认大小

(3)如果方法局部变量没有逃离方法的作用范围,它是线程安全的

(4)需要考虑线程安全:局部变量引用对象(引用传参)、逃离方法的作用范围(作为返回值)

3、栈内存溢出(java.lang.stackOverflowError)

(1)栈帧过多

(2)栈帧过大:较少出现

(3)第三方类库操作

(4)-Xss:指定栈内存大小

4、本地方法栈

(1)带有 native 关键字的方法

(2)需要 Java 调用本地 C / C++ 方法,因为 Java 有时无法直接与操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法

 

1、Heap

2、定义:通过 new 创建的对象,都会存放在堆内存

3、特点

(1)它是线程共享的,堆中对象都需要考虑线程安全的问题

(2)拥有垃圾回收机制

4、堆内存溢出

(1)java.lang.OutofMemoryError: java heap space

(2)-Xmx:指定堆内存大小

5、堆内存诊断工具

(1)jps:命令行,查看当前系统中的 Java 进程

(2)jmap:命令行,jmap - heap 进程 id,查看堆内存占用情况

(3)jconsole:图形界面,多功能的监测工具,可以连续监测

(4)jvisualvm:图形界面,类似 jconsole,可以进行堆转储(Dump)

 

方法区

1、定义(Java 1.8)

(1)JVM 有一个方法区,在所有 JVM 线程之间共享

(2)方法区类似于传统语言的编译代码的存储区,或类似于操作系统进程中的“text”段

(3)存储每个类的结构,如:运行时常量池、字段、方法数据、方法和构造函数的代码,包括用于类和实例初始化,以及接口初始化的特殊方法

(4)方法区在虚拟机启动时被创建,尽管方法区在逻辑上是堆的一部分,但简单的实现可以选择不对其进行垃圾收集或压缩

(5)规范没有规定方法区的位置,或用于管理编译代码的策略

(6)方法区可以是一个固定的大小,也可以根据计算的需要进行扩展,如果不需要更大的方法区,也可以进行收缩

(7)方法区的内存不需要是连续的

(8)JVM 的实现可以为程序员或用户提供对方法区初始大小的控制,以及在不同大小的方法区中,控制最大、最小方法区大小

(9)如果方法区的内存不能被用来满足分配请求,JVM 会抛出 OutOfMemoryError

2、方法区为逻辑上的概念

(1)Java 8 之前,永久代实现方法区,属于 JVM 内存结构

(2)Java 8 之后(包括 8),元空间实现方法区,属于本地内存

3、方法区内存溢出

(1)1.8 之前,导致永久代内存溢出

(2)-XX:MaxPermSize=:指定永久代内存大小

(3)1.8 之后,导致元空间内存溢出

(4)-XX:MaxMetaspaceSize=:指定元空间大小

(5)实际场景:动态加载类,如:Spring、MyBaits

4、运行时常量池

(1)二进制字节码:类的基本信息、常量池、类方法定义(包含虚拟机的指令)

(2)每条指令都对应常量池表中的一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息

(3)常量池即为一张表,虚拟机指令根据常量池表,查找执行的类名、方法名、参数类型、字面量等信息

(4)常量池在 .class 文件中,当该类被加载,其常量池信息被放入运行时常量池,并把其中符号地址转换为真实地址

(5)方法区内部包含运行时常量池(Runtime Constant Pool)

 

字符串常量池

1、StringTable

2、常量池、字符串常量池的关系

(1)常量池中的信息,加载到运行时常量池中,为常量池中的符号,未转变为 Java 字符串对象

(2)虚拟机指令 ldc,会将符号转变为字符串对象,并且为延迟加载

3、特性

(1)常量池中的字符串仅是符号,第一次使用时才转换为字符串对象

(2)利用串池的机制,来避免重复创建字符串对象

(3)字符串变量拼接原理(JDK 1.8):StringBuilder、append()、toString()

(4)字符串常量拼接原理:Java 编译器(前端编译器)在编译期优化

(5)使用 intern(),把有新字面值的字符串地址驻留到常量池里

4、位置

(1)JDK 1.6 之前:在永久代中的运行时常量池

(2)JDK 1.7 之后:在堆中

5、性能调优

(1)StringTable 由 HashTable 实现,可以适当增加 HashTable 桶的个数,以减少字符串放入串池所需时间

(2)-XX:StringTableSize=桶个数(最少设置为 1009 以上)

(3)考虑是否需要将字符串对象入池

(4)通过 intern() 减少重复入池

 

直接内存

1、Direct Memory

(1)不属于 JVM 组成

(2)由操作系统直接管理

2、定义

(1)常见于 NIO 操作时,用于数据缓冲区

(2)分配回收成本较高,但读写性能高

(3)不受 JVM GC 管理,存在内存泄漏、内存溢出

3、文件 I/O 过程

(1)Java 不具备磁盘读写能力,调用磁盘读写,需要调用操作系统提供的函数

(2)CPU:用户态(Java 方法)-> 内核态(System 本地方法)

(3)内存:读取磁盘文件 -> 在操作系统中,划出系统缓冲区,多次分批读取磁盘 -> Java 堆内存中,划分出 Java 缓冲区,多次分批读取内存

(4)Java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 Java 堆内存中

(5)缺点:数据存储两份,分别在系统内存、Java 堆,造成了不必要的复制

4、使用直接内存,文件 I/O 过程

(1)CPU:用户态(Java 方法)-> 内核态(System 本地方法)

(2)内存:在操作系统中,划分出直接内存,如:ByteBuffer 中的 public static ByteBuffer allocateDirect(int capacity)

(3)直接内存:操作系统、Java 代码都可以访问,无需将代码从系统内存,复制到 Java 堆内存,从而提高效率

 

直接内存回收原理

1、ByteBuffer 类

(1)allocateDirect 方法

public static ByteBuffer allocateDirect(int capacity) {
    //allocateDirect的实现,底层创建一个DirectByteBuffer对象
    return new DirectByteBuffer(capacity);
}

2、DirectByteBuffer 类

(1)构造器

// Primary constructor
DirectByteBuffer(int cap) {                   // package-private
    
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    //调用Unsafe类的allocateMemory、setMemory,完成直接内存的分配
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    //ByteBuffer实现内部,使用Cleaner(虚引用)检测ByteBuffer是否被回收
    //通过虚引用,实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现Runnable接口,run方法中通过unsafe释放内存
    //后台线程监测虚引用的对象,如果虚引用的实际对象(此处为DirectByteBuffer)被回收后,则调用Cleaner的clean方法,清除直接内存中占用的内存
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

3、Clean 类 

(1)继承 PhantomReference,为虚引用类型

public class Cleaner extends PhantomReference<Object>

(2)create 方法

//通过虚引用,实现直接内存的释放,var0为虚引用的实际对象, var1是一个回调,实现Runnable接口,run方法中通过unsafe释放内存
public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null ? null : add(new Cleaner(var0, var1));
}

(3)clean 方法

//ReferenceHandler(守护线程)监测虚引用的对象,如果虚引用的实际对象被回收后,则调用Cleaner的clean方法
public void clean() {
    if (remove(this)) {
        try {
            //在ByteBuffer中,thunk为Deallocator对象,其run方法会释放直接内存
            this.thunk.run();
        } catch (final Throwable var2) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.err != null) {
                        (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                    }

                    System.exit(1);
                    return null;
                }
            });
        }

    }
}

(4)thunk 属性

private final Runnable thunk;

4、Deallocator 类

(1)DirectByteBuffer 的静态内部类

(2)实现 Runnable 接口,为回调任务对象

private static class Deallocator implements Runnable {

    private static Unsafe unsafe = Unsafe.getUnsafe();

    private long address;
    private long size;
    private int capacity;

    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }

    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        //主动调用freeMemory,释放直接内存
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
}

5、Unsafe 类

(1)底层类,分配、释放直接内存,不建议使用,由 JDK 开发人员使用

(2)使用 Unsafe 类,完成直接内存的分配、回收,回收不是由 JVM GC 释放,而是需要主动调用 freeMemory 方法

(3)分配内存

//返回值:直接内存的地址
public native long allocateMemory(long var1);
public void setMemory(long var1, long var3, byte var5) {
    this.setMemory((Object)null, var1, var3, var5);
}
public native void setMemory(Object var1, long var2, long var4, byte var6);

(4)释放内存

public native void freeMemory(long var1);

6、禁用显式回收

(1)一般 JVM 调优时,加上参数 -XX:+DisableExplicitGC,表示禁止显示 GC,即 System.gc() 无效

(2)如:使手动 System.gc() 无效,它是一种 Full GC,会回收新生代、老年代,造成程序执行时间较长

(3)可能影响回收直接内存,只能等待 JVM 自动 GC,所以需要通过 Unsafe 对象,调用 freeMemory 方法释放内存

posted @   半条咸鱼  阅读(79)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示