学习CLR源码:连续内存块数据操作的性能优化

简介: 本文主要介绍 C# 命名空间 System.Buffers.Binary 中的一些二进制处理类和 Span 的简单使用方法,这些二进制处理类型是上层应用处理二进制数据的基础,掌握这些类型后,我们可以很容易地处理类型和二进制数据之间的转换以及提高程序性能。

C# 原语类型


按照内存分配来区分,C# 有值类型、引用类型;

按照基础类型类型来分,C# 有 内置类型、通用类型、自定义类型、匿名类型、元组类型、CTS类型(通用类型系统);


C# 的基础类型包括:

  1. 整型: sbyte, byte, short, ushort, int, uint, long, ulong
  2. 实数类型: float, double, decimal
  3. 字符类型: char
  4. 布尔类型: bool
  5. 字符串类型: string


C# 中的原语类型,是基础类型中的值类型,不包括 string。原语类型可以使用 sizeof() 来获取字节大小,除 bool 外,都有 MaxValueMinValue 两个字段。


sizeof(uint);
uint.MaxValue
uint.MinValue


我们也可以在泛型上进行区分,上面的教程类型,除了 string,其他类型都是 struct。

<T>() where T : struct
{
}


更多说明,可以戳这里了解:https://www.programiz.com/csharp-programming/variables-primitive-data-types


1,利用 Buffer 优化数组性能


Buffer 可以操作基元类型(int、byte等)的数组,利用.NET 中的 Buffer 类,通过更快地访问内存中的数据来提高应用程序的性能。


Buffer 可以直接从基元类型的数组中,直接取出指定数量的字节,或者给其某个字节设置值。


Buffer 主要在直接操作内存数据、操作非托管内存时,使用 Buffer 可以带来安全且高性能的体验。


方法 说明
BlockCopy(Array, Int32, Array, Int32, Int32) 将指定数目的字节从起始于特定偏移量的源数组复制到起始于特定偏移量的目标数组。
ByteLength(Array) 返回指定数组中的字节数。
GetByte(Array, Int32) 检索指定数组中指定位置的字节。
MemoryCopy(Void, Void, Int64, Int64) 将指定为长整型值的一些字节从内存中的一个地址复制到另一个地址。此 API 不符合 CLS。
MemoryCopy(Void, Void, UInt64, UInt64) 将指定为无符号长整型值的一些字节从内存中的一个地址复制到另一个地址。此 API 不符合 CLS。
SetByte(Array, Int32, Byte) 将指定的值分配给指定数组中特定位置处的字节。

CLS 指公共语言标准,请参考 https://www.cnblogs.com/whuanle/p/14141213.html#5,clscompliantattribute


下面来介绍一下 Buffer 的一些使用方法。

BlockCopy 可以复制数组的一部分到另一个数组,其使用方法如下:

int[] arr1 = new int[] { 1, 2, 3, 4, 5 };
        int[] arr2 = new int[10] { 0, 0, 0, 0, 0, 6, 7, 8, 9, 10 };
        // int = 4 byte
        // index:       0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 ... ...
        // arr1:        01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
        // arr2:        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00
        // Buffer.ByteLength(arr1) == 20 ,
        // Buffer.ByteLength(arr2) == 40
        Buffer.BlockCopy(arr1, 0, arr2, 0, 19);
        for (int i = 0; i < arr2.Length; i++)
        {
            Console.Write(arr2[i] + ",");
        }



.SetByte() 则可细粒度地设置数组的值,即可以直接设置数组中任意一位的值,其使用方法如下:

//source data:
        // 0000,0001,0002,00003,0004
        // 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
        int[] a = new int[] { 0, 1, 2, 3, 4 };
        foreach (var item in a)
        {
            Console.Write(item + ",");
        }
        Console.WriteLine("\n------\n");
        // see : https://stackoverflow.com/questions/26455843/how-are-array-values-stored-in-little-endian-vs-big-endian-architecture
        // memory save that data:
        // 0000    1000    2000    3000    4000
        for (int i = 0; i < Buffer.ByteLength(a); i++)
        {
            Console.Write(Buffer.GetByte(a, i));
            if (i != 0 && (i + 1) % 4 == 0)
                Console.Write("    ");
        }
        // 16 进制
        // 0000    1000    2000    3000    4000
        Console.WriteLine("\n------\n");
        Buffer.SetByte(a, 0, 4);
        Buffer.SetByte(a, 4, 3);
        Buffer.SetByte(a, 8, 2);
        Buffer.SetByte(a, 12, 1);
        Buffer.SetByte(a, 16, 0);
        foreach (var item in a)
        {
            Console.Write(item + ",");
        }
        Console.WriteLine("\n------\n");


建议复制代码自行测试,断点调试,观察过程。


2,BinaryPrimitives 细粒度操作字节数组


System.Buffers.Binary.BinaryPrimitives 用来以精确的方式读取或者字节数组,只能对 bytebyte 数组使用,其使用场景非常广泛。


BinaryPrimitives 的实现原理是 BitConverter,BinaryPrimitives 对 BitConverter 做了一些封装。BinaryPrimitives 的主要使用方式是以某种形式从 byte 或 byte 数组中读取出信息。


例如,BinaryPrimitives 在 byte 数组中,一次性读取四个字节,其示例代码如下:

// source data:  00 01 02 03 04
        // binary data:  00000000 00000001 00000010 00000011 000001000
        byte[] arr = new byte[] { 0, 1, 2, 3, 4, };
        // read one int,4 byte
        int head = BinaryPrimitives.ReadInt32BigEndian(arr);
        // 5 byte:             00000000 00000001 00000010 00000011 000001000
        // read 4 byte(int) :  00000000 00000001 00000010 00000011
        //                     = 66051
        Console.WriteLine(head);


在 BinaryPrimitives 中有大端小端之分。在 C# 中,应该都是小端在前大端在后的,具体可能会因处理器架构而不同。

你可以使用 BitConverter.IsLittleEndian 来判断在当前处理器上,C# 程序是大端还是小端在前。


.Read...() 开头的方法,可以以字节为定位访问 byte 数组上的数据。

.Write...() 开头的方法,可以向某个位置写入数据。


下面举个例子:

// source data:  00 01 02 03 04
        // binary data:  00000000 00000001 00000010 00000011 000001000
        byte[] arr = new byte[] { 0, 1, 2, 3, 4, };
        // read one int,4 byte
        // 5 byte:             00000000 00000001 00000010 00000011 000001000
        // read 4 byte(int) :  00000000 00000001 00000010 00000011
        //                     = 66051
        int head = BinaryPrimitives.ReadInt32BigEndian(arr);
        Console.WriteLine(head);
        // BinaryPrimitives.WriteInt32LittleEndian(arr, 1);
        BinaryPrimitives.WriteInt32BigEndian(arr.AsSpan().Slice(0, 4), 0b00000000_00000000_00000000_00000001);
        // to : 00000000 00000000 00000000 00000001 |  000001000
        // read 4 byte
        head = BinaryPrimitives.ReadInt32BigEndian(arr);
        Console.WriteLine(head);

建议复制代码自行测试,断点调试,观察过程。


提高代码安全性


C#和.NET Core 有的许多面向性能的 API,C# 和 .NET 的一大优点是可以在不牺牲内存安全性的情况下编写快速出高性能的库。我们在避免使用 unsafe 代码的情况下,通过二进制处理类,我们可以编写出高性能的代码和具有安全性的代码。


在 C# 中,我们有以下类型可以高效操作字节/内存:

  • Span 和C#类型可以快速安全地访问内存。表示任意内存的连续区域。使用 span 使我们可以序列化为托管.NET数组,堆栈分配的数组或非托管内存,而无需使用指针。.NET可以防止缓冲区溢出。
  • ref structSpan
  • stackalloc 用于创建基于堆栈的数组。stackalloc 是在需要较小缓冲区时避免分配的有用工具。
  • 低级方法,并在原始类型和字节之间直接转换。MemoryMarshal.GetReference()Unsafe.ReadUnaligned()Unsafe.WriteUnaligned()
  • BinaryPrimitives具有用于在.NET基本类型和字节之间进行有效转换的辅助方法。例如,读取小尾数字节并返回无符号的64位数字。所提供的方法经过了最优化,并使用了向量化。BinaryPrimitives.ReadUInt64LittleEndianBinaryPrimitive


.Reverse...() 开头的方法,可以置换基元类型的大小端。

short value = 0b00000000_00000001;
        // to endianness: 0b00000001_00000000 == 256
        BinaryPrimitives.ReverseEndianness(0b00000000_00000000_00000000_00000001);
        Console.WriteLine(BinaryPrimitives.ReverseEndianness(value));
        value = 0b00000001_00000000;
        Console.WriteLine(BinaryPrimitives.ReverseEndianness(value));
        // 1


3,BitConverter、MemoryMarshal


BitConverter 可以基元类型和 byte 相互转换,例如 int 和 byte 互转,或者任意取出、写入基元类型的任意一个字节。


其示例如下:

// 0b...1_00000100
        int value = 260;
        // byte max value:255
        // a = 0b00000100; 丢失 int ... 00000100 之前的位数。
        byte a = (byte)value;
        // a = 4
        Console.WriteLine(a);
        // LittleEndian
        // 0b 00000100 00000001 00000000 00000000
        byte[] b = BitConverter.GetBytes(260);
        Console.WriteLine(Buffer.GetByte(b, 1)); // 4
        if (BitConverter.IsLittleEndian)
            Console.WriteLine(BinaryPrimitives.ReadInt32LittleEndian(b));
        else
            Console.WriteLine(BinaryPrimitives.ReadInt32BigEndian(b));


MemoryMarshal 提供与 Memory<T>ReadOnlyMemory<T>Span<T>ReadOnlySpan<T> 进行交互操作的方法。


MemoryMarshalSystem.Runtime.InteropServices 命名空间中。

我们先介绍 MemoryMarshal.Cast(),它可以将一种基元类型的范围强制转换为另一种基元类型的范围。


// 1 int  = 4 byte
        // int [] {1,2}
        // 0001     0002
        var byteArray = new byte[] { 1, 0, 0, 0, 2, 0, 0, 0 };
        Span<byte> byteSpan = byteArray.AsSpan();
        // byte to int 
        Span<int> intSpan = MemoryMarshal.Cast<byte, int>(byteSpan);
        foreach (var item in intSpan)
        {
            Console.Write(item + ",");
        }


最简单的说法是,MemoryMarshal 可以将一种结构转换为另一种结构


我们可以将一个结构转换为字节:

public struct Test
{
    public int A;
    public int B;
    public int C;
}
... ...
        Test test = new Test()
        {
            A = 1,
            B = 2,
            C = 3
        };
        var testArray = new Test[] { test };
        ReadOnlySpan<byte> tmp = MemoryMarshal.AsBytes(testArray.AsSpan());
        // socket.Send(tmp); ...


还可以逆向还原字节为结构体:

// bytes = socket.Accept(); .. 
        ReadOnlySpan<Test> testSpan = MemoryMarshal.Cast<byte,Test>(tmp);
        // or
        Test testSpan = MemoryMarshal.Read<Test>(tmp);


例如,我们要对比两个结构体数组中,每个结构体是否相等,可以采用以下代码:

static void Main(string[] args)
        {
            int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            int[] b = new int[] { 1, 2, 3, 4, 5, 6, 7, 0, 9 };
            _ = Compare64(a,b);
        }
        private static bool Compare64<T>(T[] t1, T[] t2)
            where T : struct
        {
            var l1 = MemoryMarshal.Cast<T, long>(t1);
            var l2 = MemoryMarshal.Cast<T, long>(t2);
            for (int i = 0; i < l1.Length; i++)
            {
                if (l1[i] != l2[i]) return false;
            }
            return true;
        }


后面有个更好的性能提升方案。

程序员基本都学习过 C 语言,应该了解 C 语言中的结构体字节对齐,在 C# 中也是一样,两种类型相互转换,除了 C# 结构体转 C# 结构体,也可以 C 语言结构体转 C# 结构体,但是要考虑好字节对齐,如果两个结构体所占用的内存大小不一样,则可能在转换时出现数据丢失或出现错误。


4,Marshal


Marshal 提供了用于分配非托管内存,复制非托管内存块以及将托管类型转换为非托管类型的方法的集合,以及与非托管代码进行交互时使用的其他方法,或者用来确定对象的大小。


例如,来确定 C# 中的一些类型大小:

Console.WriteLine("SystemDefaultCharSize={0}, SystemMaxDBCSCharSize={1}",
         Marshal.SystemDefaultCharSize, Marshal.SystemMaxDBCSCharSize);


输出 char 占用的字节数。

例如,在调用非托管代码时,需要传递函数指针,C# 一般使用委托传递,很多时候为了避免各种内存问题异常问题,需要转换为指针传递。


IntPtr p = Marshal.GetFunctionPointerForDelegate(_overrideCompileMethod)


Marshal 也可以很方便地获得一个结构体的字节大小:

public struct Point
{
    public Int32 x, y;
}
Marshal.SizeOf(typeof(Point));


从非托管内存中分配一块内存和释放内存,我们可以避免 usafe 代码的使用,代码示例:

IntPtr hglobal = Marshal.AllocHGlobal(100);
        Marshal.FreeHGlobal(hglobal);


实践


合理利用前面提到的二进制处理类,可以在很多方面提升代码性能,在前面的学习中,我们大概了解这些对象,但是有什么应用场景?真的能够提升性能?有没有练习代码?


这里笔者举个例子,如何比较两个 byte[] 数组是否相等?


最简单的代码示例如下:

public bool ForBytes(byte[] a,byte[] b)
        {
            if (a.Length != b.Length)
                return false;
            for (int i = 0; i < a.Length; i++)
            {
                if (a[i] != b[i]) return false;
            }
            return true;
        }


这个代码很简单,循环遍历字节数组,一个个判断是否相等。

如果用上前面的二进制处理对象类,则可以这样写代码:

private static bool EqualsBytes(byte[] b1, byte[] b2)
        {
            var a = b1.AsSpan();
            var b = b2.AsSpan();
            Span<byte> copy1 = default;
            Span<byte> copy2 = default;
            if (a.Length != b.Length)
                return false;
            for (int i = 0; i < a.Length;)
            {
                if (a.Length - 8 > i)
                {
                    copy1 = a.Slice(i, 8);
                    copy2 = b.Slice(i, 8);
                    if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2))
                        return false;
                    i += 8;
                    continue;
                }
                if (a[i] != b[i])
                    return false;
                i++;
            }
            return true;
        }


你可能会在想,第二种方法,这么多代码,这么多判断,还有各种函数调用,还多创建了一些对象,这特么能够提升速度?这样会不会消耗更多内存??? 别急,你可以使用以下完整代码测试:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
namespace BenTest
{
    [SimpleJob(RuntimeMoniker.NetCoreApp31)]
    [SimpleJob(RuntimeMoniker.CoreRt31)]
    [RPlotExporter]
    public class Test
    {
        private byte[] _a = Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666");
        private byte[] _b = Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666");
        private int[] A1 = new int[] { 41544444, 4487, 841, 8787, 4415, 7, 458, 4897, 87897, 815, 485, 4848, 787, 41, 5489, 74878, 84, 89787, 8456, 4857489, 784, 85489, 47 };
        private int[] B2 = new int[] { 41544444, 4487, 841, 8787, 4415, 7, 458, 4897, 87897, 815, 485, 4848, 787, 41, 5489, 74878, 84, 89787, 8456, 4857489, 784, 85489, 47 };
        [Benchmark]
        public bool ForBytes()
        {
            for (int i = 0; i < _a.Length; i++)
            {
                if (_a[i] != _b[i]) return false;
            }
            return true;
        }
        [Benchmark]
        public bool ForArray()
        {
            return ForArray(A1, B2);
        }
        private bool ForArray<T>(T[] b1, T[] b2) where T : struct
        {
            for (int i = 0; i < b1.Length; i++)
            {
                if (!b1[i].Equals(b2[i])) return false;
            }
            return true;
        }
        [Benchmark]
        public bool EqualsArray()
        {
            return EqualArray(A1, B2);
        }
        [Benchmark]
        public bool EqualsBytes()
        {
            var a = _a.AsSpan();
            var b = _b.AsSpan();
            Span<byte> copy1 = default;
            Span<byte> copy2 = default;
            if (a.Length != b.Length)
                return false;
            for (int i = 0; i < a.Length;)
            {
                if (a.Length - 8 > i)
                {
                    copy1 = a.Slice(i, 8);
                    copy2 = b.Slice(i, 8);
                    if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2))
                        return false;
                    i += 8;
                    continue;
                }
                if (a[i] != b[i])
                    return false;
                i++;
            }
            return true;
        }
        private bool EqualArray<T>(T[] t1, T[] t2) where T : struct
        {
            Span<byte> b1 = MemoryMarshal.AsBytes<T>(t1.AsSpan());
            Span<byte> b2 = MemoryMarshal.AsBytes<T>(t2.AsSpan());
            Span<byte> copy1 = default;
            Span<byte> copy2 = default;
            if (b1.Length != b2.Length)
                return false;
            for (int i = 0; i < b1.Length;)
            {
                if (b1.Length - 8 > i)
                {
                    copy1 = b1.Slice(i, 8);
                    copy2 = b2.Slice(i, 8);
                    if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2))
                        return false;
                    i += 8;
                    continue;
                }
                if (b1[i] != b2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Test>();
            Console.ReadKey();
        }
    }
}


使用 BenchmarkDotNet 的测试结果如下:

BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1052 (21H1/May2021Update)
Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=5.0.301
  [Host]        : .NET Core 3.1.16 (CoreCLR 4.700.21.26205, CoreFX 4.700.21.26205), X64 RyuJIT
  .NET Core 3.1 : .NET Core 3.1.16 (CoreCLR 4.700.21.26205, CoreFX 4.700.21.26205), X64 RyuJIT
|      Method |           Job |       Runtime |     Mean |    Error |   StdDev |
|------------ |-------------- |-------------- |---------:|---------:|---------:|
|    ForBytes | .NET Core 3.1 | .NET Core 3.1 | 76.95 ns | 0.064 ns | 0.053 ns |
|    ForArray | .NET Core 3.1 | .NET Core 3.1 | 66.37 ns | 1.258 ns | 1.177 ns |
| EqualsArray | .NET Core 3.1 | .NET Core 3.1 | 17.91 ns | 0.027 ns | 0.024 ns |
| EqualsBytes | .NET Core 3.1 | .NET Core 3.1 | 26.26 ns | 0.432 ns | 0.383 ns |


可以看到,byte[] 比较中,使用了二进制对象的方式,耗时下降了近 60ns,而在 struct 的比较中,耗时也下降了 40ns。


在第二种代码中,我们使用了 Span、切片、 MemoryMarshal、BinaryPrimitives,这些用法都可以给我们的程序性能带来很大的提升。


这里示例虽然使用了 Span 等,其最主要是利用了 64位 CPU ,64位 CPU 能够一次性读取 8个字节(64位),因此我们使用 ReadUInt64BigEndian 一次读取从字节数组中读取 8 个字节去进行比较。如果字节数组长度为 1024 ,那么第二种方法只需要 比较 128次。


当然,这里并不是这种代码性能是最强的,因为 CLR 有很多底层方法具有更猛的性能。不过,我们也看到了,合理使用这些类型,能够很大程度上提高代码性能。上面的数组对比只是一个简单的例子,在实际项目中,我们也可以挖掘更多使用场景。


更高性能


虽然第二种方法,快了几倍,但是性能还不够强劲,我们可以利用 Span 中的 API,来实现更快的比较。


[Benchmark]
        public bool SpanEqual()
        {
            return SpanEqual(_a,_b);
        }
        private bool SpanEqual(byte[] a, byte[] b)
        {
            return a.AsSpan().SequenceEqual(b);
        }


可以试试

StructuralComparisons.StructuralEqualityComparer.Equals(a, b);


性能测试结果:

|      Method |           Job |       Runtime |      Mean |     Error |    StdDev |
|------------ |-------------- |-------------- |----------:|----------:|----------:|
|    ForBytes | .NET Core 3.1 | .NET Core 3.1 | 77.025 ns | 0.0502 ns | 0.0419 ns |
|    ForArray | .NET Core 3.1 | .NET Core 3.1 | 66.192 ns | 0.6127 ns | 0.5117 ns |
| EqualsArray | .NET Core 3.1 | .NET Core 3.1 | 17.897 ns | 0.0122 ns | 0.0108 ns |
| EqualsBytes | .NET Core 3.1 | .NET Core 3.1 | 25.722 ns | 0.4584 ns | 0.4287 ns |
|   SpanEqual | .NET Core 3.1 | .NET Core 3.1 |  4.736 ns | 0.0099 ns | 0.0093 ns |


可以看到,Span.SequenceEqual() 的速度简直是碾压。

对于 C# 中的二进制处理技巧就介绍到这里,阅读 CLR 源码 时,我们可以学习到很多骚操作,读者可以多阅读 CLR 源码,对技术提升有很大的帮助。

相关文章
|
9天前
|
Android开发 开发者
Android性能优化——内存管理的艺术
Android性能优化——内存管理的艺术
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
62 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
17天前
|
存储 缓存 监控
|
23天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
3月前
|
存储 监控 算法
Java内存管理:从垃圾收集到性能优化
【8月更文挑战第4天】在Java的世界中,内存管理是一块神秘的领域,它不仅关乎程序的稳定运行,更直接影响到系统的性能表现。本文将带你深入理解Java的垃圾收集机制,探讨如何通过合理的内存管理策略来提升应用效率。我们将一起分析JVM内存结构,探索不同的垃圾收集算法,并借助实际代码示例,学习如何监控和调优内存使用,以期达到减少延迟、防止内存泄漏的目的。
|
3月前
|
存储 JavaScript 前端开发
学习JavaScript 内存机制
【8月更文挑战第23天】学习JavaScript 内存机制
34 3
|
3月前
|
存储 缓存 编译器
Linux源码阅读笔记06-RCU机制和内存优化屏障
Linux源码阅读笔记06-RCU机制和内存优化屏障
|
3月前
|
开发框架 监控 .NET
|
4月前
|
NoSQL Redis
Redis性能优化问题之禁用内存大页,如何解决
Redis性能优化问题之禁用内存大页,如何解决