java堆和内存的区别,哪个更快?

本文探讨了Java中如何利用sun.misc.Unsafe访问和管理本地内存,包括分配和回收内存的方法,并对比了Java堆内存与本地内在的读写性能。

使用Java的一个好处就是你可以不用亲自来管理内存的分配和释放。当你用
new
关键字来实例化一个对象时,它所需的内存会自动的在Java堆中分配。堆会被垃圾回收器进行管理,并且它会在对象超出作用域时进行内存回收。但是在JVM中有一个‘后门’可以让你访问不在堆中的本地内存(native memory)。在这篇文章中,我会给你演示一个对象是怎样以连续的字节码的方式在内存中进行存储,并且告诉你是应该怎样存储这些字节,是在Java堆中还是在本地内存中。最后我会就怎样从JVM中访问内存更快给一些结论:是用Java堆还是本地内存。

使用方法

Unsafe

来分配和回收内存

sun.misc.Unsafe
可以让你在Java中分配和回收本地内存,就像C语言中的
malloc

free
通过它分配的内存不在Java堆中,并且不受垃圾回收器的管理,因此在它被使用完的时候你需要自己来负责释放和回收。下面是我写的一个使用:
Unsafe
来管理本地内存的一个工具类:
public class Direct implements Memory {

    private static Unsafe unsafe;
    private static boolean AVAILABLE = false;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField(“theUnsafe”);
            field.setAccessible(true);
            unsafe = (Unsafe)field.get(null);
            AVAILABLE = true;
        } catch(Exception e) {
            // NOOP: throw exception later when allocating memory
        }
    }

    public static boolean isAvailable() {
        return AVAILABLE;
    }

    private static Direct INSTANCE = null;

    public static Memory getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Direct();
        }
        return INSTANCE;
    }

    private Direct() {

    }

    @Override
    public long alloc(long size) {
        if (!AVAILABLE) {
            throw new IllegalStateException(“sun.misc.Unsafe is not accessible!”);
        }
        return unsafe.allocateMemory(size);
    }

    @Override
    public void free(long address) {
        unsafe.freeMemory(address);
    }

    @Override
    public final long getLong(long address) {
        return unsafe.getLong(address);
    }

    @Override
    public final void putLong(long address, long value) {
        unsafe.putLong(address, value);
    }

    @Override
    public final int getInt(long address) {
        return unsafe.getInt(address);
    }

    @Override
    public final void putInt(long address, int value) {
        unsafe.putInt(address, value);
    }
}

在本地内存中分配一个对象

让我们来将下面的Java对象放到本地内存中:

public class SomeObject {

    private long someLong;
    private int someInt;

    public long getSomeLong() {
        return someLong;
    }
    public void setSomeLong(long someLong) {
        this.someLong = someLong;
    }
    public int getSomeInt() {
        return someInt;
    }
    public void setSomeInt(int someInt) {
        this.someInt = someInt;
    }
}

我们所做的仅仅是把对象的属性放入到
Memory
中:

public class SomeMemoryObject {
    private final static int someLong_OFFSET = 0;
    private final static int someInt_OFFSET = 8;
    private final static int SIZE = 8 + 4; // one long + one int

    private long address;
    private final Memory memory;

    public SomeMemoryObject(Memory memory) {
        this.memory = memory;
        this.address = memory.alloc(SIZE);
    }

    @Override
    public void finalize() {
        memory.free(address);
    }

    public final void setSomeLong(long someLong) {
        memory.putLong(address + someLong_OFFSET, someLong);
    }

    public final long getSomeLong() {
        return memory.getLong(address + someLong_OFFSET);
    }

    public final void setSomeInt(int someInt) {
        memory.putInt(address + someInt_OFFSET, someInt);
    }

    public final int getSomeInt() {
        return memory.getInt(address + someInt_OFFSET);
    }
}

现在我们来看看对两个数组的读写性能:其中一个含有数百万的
SomeObject
对象,另外一个含有数百万的
SomeMemoryObject
对象。

// with JIT:
Number of Objects:  1,000     1,000,000     10,000,000    60,000,000
Heap Avg Write:      107         2.30          2.51         2.58       
Native Avg Write:    305         6.65          5.94         5.26
Heap Avg Read:       61          0.31          0.28         0.28
Native Avg Read:     309         3.50          2.96         2.16

// without JIT: (-Xint)
Number of Objects:  1,000     1,000,000     10,000,000    60,000,000
Heap Avg Write:      104         107           105         102       
Native Avg Write:    292         293           300         297
Heap Avg Read:       59          63            60          58
Native Avg Read:     297         298           302         299

结论:跨越JVM的屏障来读本地内存大约会比直接读Java堆中的内存慢10倍,而对于写操作会慢大约2倍。但是需要注意的是,由于每一个SomeMemoryObject对象所管理的本地内存空间都是独立的,因此读写操作都不是连续的。那么我们接下来就来对比下读写连续的内存空间的性能。

访问一大块的连续内存空间

这个测试分别在堆中和一大块连续本地内存中包含了相同的测试数据。然后我们来做多次的读写操作看看哪个更快。并且我们会做一些随机地址的访问来对比结果。

// with JIT and sequential access:
Number of Objects:  1,000     1,000,000     1,000,000,000
Heap Avg Write:      12          0.34           0.35 
Native Avg Write:    102         0.71           0.69 
Heap Avg Read:       12          0.29           0.28 
Native Avg Read:     110         0.32           0.32

// without JIT and sequential access: (-Xint)
Number of Objects:  1,000     1,000,000      10,000,000
Heap Avg Write:      8           8              8
Native Avg Write:    91          92             94
Heap Avg Read:       10          10             10
Native Avg Read:     91          90             94

// with JIT and random access:
Number of Objects:  1,000     1,000,000     1,000,000,000
Heap Avg Write:      61          1.01           1.12
Native Avg Write:    151         0.89           0.90 
Heap Avg Read:       59          0.89           0.92 
Native Avg Read:     156         0.78           0.84

// without JIT and random access: (-Xint)
Number of Objects:  1,000     1,000,000      10,000,000
Heap Avg Write:      55          55              55
Native Avg Write:    141         142             140
Heap Avg Read:       55          55              55 
Native Avg Read:     138         140             138

结论:在做连续访问的时候,Java堆内存通常都比本地内存要快。对于随机地址访问,堆内存仅仅比本地内存慢一点点,并且是针对大块连续数据的时候,而且没有慢很多。

最后的结论

在Java中使用本地内存有它的意义,比如当你要操作大块的数据时(>2G)并且不想使用垃圾回收器(GC)的时候。从延迟的角度来说,直接访问本地内存不会比访问Java堆快。这个结论其实是有道理的,因为跨越JVM屏障肯定是有开销的。这样的结论对使用本地还是堆的
ByteBuffer
同样适用。使用本地ByteBuffer的速度提升不在于访问这些内存,而是它可以直接与操作系统提供的本地IO进行操作。

### Java 中栈内存堆内存区别及用途 #### 栈内存 (Stack Memory) 栈内存主要用于存储局部变量方法调用的上下文信息。它是一种线程私有的内存区域,每个线程都有自己的栈空间。栈内存的特点如下: - **用途**: 主要用于存储方法调用过程中的临时数据,包括方法参数、局部变量以及返回地址等[^4]。 - **分配方式**: 栈内存的分配回收是自动完成的。每当一个方法被调用时,会为其创建一个新的栈帧(Stack Frame),并将该栈帧压入当前线程的栈中;当方法执行完成后,对应的栈帧会被弹出并释放。 - **生命周期**: 栈内存中的数据生命周期较短,仅在方法调用期间有效。一旦方法结束,其关联的栈帧就会被销毁[^2]。 - **性能特点**: 由于栈的操作遵循先进后出(LIFO)原则,因此内存分配释放的速度非常快。 #### 堆内存 (Heap Memory) 堆内存是一个由所有线程共享的全局内存区域,主要用于存储对象实例其他动态分配的数据结构。以下是堆内存的主要特征: - **用途**: 堆内存主要用于存储通过 `new` 关键字或其他动态分配机制创建的对象实例。这些对象可以在多个线程之间共享。 - **分配方式**: 对象的内存分配是由垃圾回收器(Garbage Collector, GC)管理的。这意味着开发人员无需显式地释放上的内存资源,而是依赖于GC来清理不再使用的对象。 - **生命周期**: 堆内存中的对象通常具有较长的生命周期,只有在其没有任何引用指向时才可能被GC回收。 - **性能特点**: 虽然提供了更大的存储容量,但由于需要进行复杂的内存管理垃圾回收操作,其分配释放速度相对较慢。 #### 总结对比表 | 特性 | 栈内存 (Stack) | 堆内存 (Heap) | |-----------------|-----------------------------------|------------------------------------| | 存储内容 | 局部变量、方法参数 | 对象实例 | | 内存大小 | 较小 | 较大 | | 生命周期 | 方法调用期间短暂存在 | 可能跨越多个方法甚至整个应用周期 | | 访问权限 | 私有(线程独占) | 共享(多线程间可访问) | | 分配/释放 | 自动 | 手动或由GC处理 | ```java // 示例代码展示栈的关系 public class StackAndHeapExample { public static void main(String[] args) { int stackVariable = 10; // 这个整数存储在栈上 Person heapObject = new Person(); // 'Person' 实例存储在上,而引用存储在栈上 System.out.println(heapObject.getName()); changeValue(stackVariable); // 修改的是副本,不影响原值 modifyObject(heapObject); // 修改的是实际对象的内容 } private static void changeValue(int value){ value += 5; } private static void modifyObject(Person person){ person.setName("Modified Name"); } } class Person{ private String name; public String getName() {return this.name;} public void setName(String newName){this.name=newName;} } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值