首页 > 修改教程 > 如何用gg修改器修改游戏资源_gg修改器怎么修改游戏
如何用gg修改器修改游戏资源_gg修改器怎么修改游戏
  • gg修改器最新版
  • 大小:18.09MB版本:v2.14
  • 语言:简体中文系统:Android
绿色无毒,安全可靠!部分设备误报拦截请通过!

如何用gg修改器修改游戏资源_gg修改器怎么修改游戏

作者:佚名 来源:网友分享 日期:2024-04-27 05:42:03

大家好,今天小编为大家分享关于如何用gg修改器修改游戏资源_gg修改器怎么修改游戏的内容,赶快来一起来看看吧。

这一节, 将重点来讨论NIO中的Buffer。 彻底理解清楚Buffer是个什么样的东西。

一、 堆内内存与堆外内存

关于Buffer, 在基础篇中已经做过简单介绍。他是网络IO数据与本地数据的缓冲。Nio中相关数据都 是通过Buffer来携带。在这一部分,就来深入看看这个Buffer到底是什么。

实际上Buffer就是映射的一段内存数据, 而在内存中就全是由0和1组成的数据。 Nio中的Buffer有很 多实现类, 其中最为根本的就是ByteBuffer, 因为所有的数据形式最终都可以通过Byte来描述。 但是 java.nio.ByteBuffer只是一个抽象类, 在他的下面有两个主要的实现类: DirectByteBuffer和 HeapedByteBuffer。 整体类图如下:

其中这个HeapByteBuffer就是对应JVM堆内内存。 而DirectBuffer就对应堆外内存。

另外一个MappedByteBuffer则是一个文件的映射内存, 通常配合FileChannel使用。 在下一个章节中会再来盘他。

堆内内存有JVM的GC进行管理, 使用起来靠谱很多。 而堆外内存则是使用的操作系统的内存,使用 起来风险就会大很多。 需要手动进行管理, 包括分配、 读写、 回收等过程都要自己管理。尤其是要注 意回收。 如果堆外内存没有正确回收, 这块内存就无法被其他应用程序使用, 造成内存泄漏。 最终会 影响整个系统的安全性。

但是, 也正是因为没有GC的管理, 所以堆外内存的使用效率相对会更高。 例如他就不会有GC中一直 困扰的STW问题, 更深入一点, 数据在内存态与内核态之间的拷贝次数也会相对较少。 JVM虚拟机针对DirectBuffer的IO操作也做了大量的优化。例如在JVM底层会尽量避免数据在不同ByteBuffer之间 的拷贝。

关于DirectBuffer, 他可以代表一段具体的内存数据, 同时也可以是其他DirectBuffer的view视 图, 这样才能有效的将内存地址进行传递。而其中提供了一个attachement()方法, 这个方法就 会绕过各层的view, 直接找到最终的内存内容。

堆外适合用来存放一些需要长期存储, 且变化不会太多, 结构也不太复杂的数据。 基本上所有追求极 致性能的场景都会拿这个DirectBuffer开刀。 像Netty、 RocketMQ、 EHcache等很多开源框架都大 量的使用了堆外内存。

另外两个DirectByteBufferR 和 HeapByteBufferR就是对应的只读版本。

接下来就从初始化、 读写数据、 内存回收三个步骤来深入理解下HeapByteBuffer和 DirectByteBuffer。

二、 内存对象初始化

HeapByteBuffer的初始化方式通常只有一个, 就是ByteBuffer的allocate()方法。 这个创建方法比较简单

// # java.nio.ByteBuffer
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
// # java.nio.HeapByteBuffer
HeapByteBuffer(int cap, int lim) { // package ‐private
super( ‐1, 0, lim, cap, new byte[cap], 0);
}

这里面, 关于mark,position.limit,cap这些参数, 就是ByteBuffer的基础使用机制, 可以去看下基础 篇就行。 这里重点讨论在他们内部如何保存数据。可以看到, 对于HeapByteBuffer, 就直接使用一 个byte数据来保存数据。

DirectBuffer的初始化方式主要有两个, 一个是ByteBuffer的allocateDirect()方法。 另一个是通过 FileChannel的map方法创建映射。 先来看第一个方法。 其中主要的步骤都加了注释。

// # java.nio.ByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
// # java.nio.DirectByteBuffer
DirectByteBuffer(int cap) {
//1、 调用父构造器时, 没有传入保存数据的结构。 而是会由后续的unsafe类来直接操作内存数据。
super( ‐1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//2、 记录相关内存信息。
Bits.reserveMemory(size, cap);long base = 0;
//3、 分配内存地址
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;
}
//4、 构建内存回收对象
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

从这个初始化过程中会看到, DirectBuffer的缓存数据并没有通过JVM中的对象来保存, 而是通过 unsafe类直接操作的内存数据。

另一个初始化的方式可以参见示例代码中的com.roy.zerocopy.MappedByteBufferDemo, 也就是 常说的零拷贝的一种方式。 其中核心的创建方式是

MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

关于这几个参数, 第一个是打开的模式, READ_WRITE表示可读可写。

第二个参数是position 表示可以直接修改的起始位置。

第三个参数是size 表示映射到内存的大小。

创建时需要指定一个映射的范围, 只有映射范围内的文件内容是可以修改的, 映射范围外的文件内容 如果尝试修改会抛异常IndexOutOfBoundsException。 这跟ByteBuffer的工作机制一一致的。 这种 方式在程序内存中实际上是映射的一些文件相关的元数据信息, 而不需要拷贝完整的文件内容, 所以 能够减少用户态到JVM内存的拷贝过程。 在RocketMQ中就大量的使用了这种机制来管理本地的落盘 文件。

但是这个源码就很难看了, 因为这里面涉及到了很多跟操作系统内核对应的一些代码。 例如对FileDescriptor的操作。

三、 内存数据读写

其实数据读写操作都是基于他们不同的数据存储方式。HeapByteBuffer是以一个byte[]数组来保存 数据,所有的数据读写操作最终都落地为对byte[]数组的操作, 相对就简单很多。

但是DirectByteBuffer不缓存数据,所有的数据操作都是通过unsafe类来对内存进行实际的操作。

有很多人在面试时很难解释清楚零拷贝到底是个什么玩意。其实,从这里就可以体现。通过 DirectByteBuffer不存储内存数据,所以也就少了一次数据的拷贝过程。

实际上,这种机制在java中使用是非常非常频繁的。具体可以看下上一章节介绍的lsof指令。

四、 内存释放

每个内存使用完了都需要释放,回收。对于ByteBuffer也不例外。这其中,HeapByteBuffer比较简单, 直接交由GC处理就行。但是对于DirectByteBuffer,这个回收过程就有点麻烦了。

因为对于一个DirectByteBuffer,变量要在JVM中的栈内存中分配, 而实际的内存空间又需要分配在堆空间上。 而堆空间并不保存实际的内存数据,只保存一个到对外内存的映射。像这样:

这时如果程序只是简单的终止, 那么JVM中的栈内存和堆内存都可以由GC回收。但是操作系统内存 中的直接内存地址就没办法回收了。如果不回收, 这就是内存泄漏的问题。所以,DirectByteBuffer 中还专门设计了内存回收的机制,保证直接内存地址会在堆内存地址被GC回收时, 一起回收。

其实如何对GC过程进行干预,是一直伴随java发展的一个问题。对于Object类的finalize方法,也是一直被人说道的地方。那怎么在对象GC回收过程中进行人工的干预呢? 这个DirectByteBuffer就提供了最好的示例。

对DirectByteBuffer的内存回收机制, 可以从这样一条路线串起来。

Step1: 在DirectByteBuffer构建时, 就创建了一个Cleaner对象

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

在创建Cleaner时, 传入了一个Deallocator对象。 这个对象是一个实现了runnable的线程资源类, 在他的run方法中就实现了对实际内存地址的回收逻辑。

private static class Deallocator
implements Runnable
{
...
public void run() {
...
//回收对外内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}

接下来就要寻找这个资源回收线程在什么时候启动。 而启动的机制就在这个Cleaner中了。Step2: 在Cleaner对象中, 有一个clean()方法, 这个方法中实现了具体的对象GC销毁逻辑。

// # sun.misc.Cleaner
public void clean() {
//1、 将自己移出双向队列
if (remove(this)) {
try {
//2、 启动销毁线程
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;
}
});
}
}
}

注解1、 在Cleaner类内部, 维护了一个Cleaner对象组成的双向链表结构。在调用Cleaner的 create方法时, 会将创建出来的cleaner对象加入到这个双向链表中。

注解2、 Cleaner在销毁时,会启动一个对象销毁的线程。Cleaner类只管启动销毁线程, 并不管 销毁的逻辑。 销毁的具体逻辑, 是在创建Cleaner时传入的。在我们讨论的这个场景, 这个thunk就是传入的Deallocator对象。

到了这一步后, 我们就可以有一种手动的方式来回收对外内存。 cleaner方法就会返回这个Cleaner 对象, 直接调用clean方法就会进行堆外内存的回收。

public static void clean(final ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
((DirectBuffer)byteBuffer).cleaner().clean();
}
}

在RocketMQ中, 他的本地存盘文件, 都是以这种堆外内存的形式映射到内存中来操作的。 而他 在应用停止时, 正是用的这种方法来回收内存的。 有兴趣可以自己去翻阅一下源码。

只不过要注意下,DirectByteBuffer在使用时,即可以代表一块堆外内存,也可以代表一个DirectByteBuffer的映射,可以多次传递。这时要先通过attachment()方法, 获得原始的对外内 存。下面这段是DirectByteBuffer源码中对于attachment的注解

// An object attached to this buffer. If this buffer is a view of another
// buffer then we use this field to keep a reference to that buffer to
// ensure that its memory isn ’t freed before we are done with it.
private final Object att;

资源销毁的步骤又往前找了一步。 接下来就是要找到DirectBuffer中是如何自动触发Cleaner的clean 方法了。 毕竟不可能每次都要求应用程序自己去手动触发堆外内存的回收。

Step3: DirectByteBuffer的自动内存回收, 要从Cleaner的构造函数说起。 Cleaner继承了 PhantomReference类, 表示这是一个虚引用。java中有强引用、 软引用、 弱引用、 虚引用四种引用 类型, 这四种引用类型的区别, 会在下一章继续整理。 我们先来梳理清楚DirectByteBuffer自动内存 回收的主线。

// # sun.misc.Cleaner
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}

在Cleaner的构造函数中, 引用了父类的构造函数, 并传入了一个ReferenceQueue<Object> dummyQueue队列。 而在Cleaner的父类java.lang.ref.Reference中, 有一段静态代码块, 实现了 DirectByteBuffer的自动内存回收。

static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 核心就在这个线程中。
Thread handler = new ReferenceHandler(tg, "Reference Handler");
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}

在这个静态代码块中, 启动了一个ReferenceHandler线程, 并且这个线程是一个守护线程, 并且优 先级很高。 也就是说他会一直随着java进程执行。 而这个ReferenceHandler的定义, 就在这段静态 代码块的上方。

private static class ReferenceHandler extends Thread {
...
public void run() {
for (;;) {
Reference<Object> r;
synchronized (lock) {
if (pending != null) {
r = pending;
pending = r.discovered;
r.discovered = null;
} else {
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// 在这里调用Cleaner的clean方法。
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}

以上就是关于如何用gg修改器修改游戏资源_gg修改器怎么修改游戏的全部内容,希望对大家有帮助。

相关文章

热门下载

大家还在搜