Netty内存管理

Netty内存管理,第1张

ByteBuf底层是一个字节数组,内部维护了两个索引:readerIndex与writerIndex。其中0 --> readerIndex部分为可丢弃字节,表示已被读取过,readerIndex --> writerIndex部分为可读字节,writerIndex --> capacity部分为可写字节。ByteBuf支持动态扩容,在实例化时会传入maxCapacity,当writerIndex达到capacity且capacity小于maxCapacity时会进行自动扩容。

ByteBuf子类可以按照以下三个纬度进行分类:

在进入内存分配核心逻辑前,我们先对Netty内存分配相关概念做下了解。Netty内存管理借鉴jemalloc思想,为了提高内存利用率,根据不同内存规格使用不同的分配策略,并且使用缓存提高内存分配效率。

Netty有四种内存规格,tiny表示16B ~ 512B之间的内存块,samll表示512B ~ 8K之间的内存块,normal表示8K ~ 16M的内存块,Huge表示大于16M的内存块。

Chunk是Netty向 *** 作系统申请内存的单位,默认一次向 *** 作系统申请16M内存,Netty内部将Chunk按照Page大小划分为2048块。我们申请内存时如果大于16M,则Netty会直接向 *** 作系统申请对应大小内存,如果申请内存在8k到16M之间则会分配对应个数Page进行使用。如果申请内存远小于8K,那么直接使用一个Page会造成内存浪费,SubPage就是对Page进行再次分配,减少内存浪费。

如果申请内存小于8K,会对Page进行再次划分为SubPage,SubPage大小为Page大小/申请内存大小。SubPage又划分为tiny与small两种。

负责管理从 *** 作系统中申请到的内存块,Netty为了减少多线程竞争arena,采用多arena设计,arena数量默认为2倍CPU核心数。线程与arena关系如下:

线程本地缓存,负责创建线程缓存PoolThreadCache。PoolThreadCache中会初始化三种类型MemoryRegionCache数组,用以缓存线程中不同规格的内存块,分别为:tiny、small、normal。tiny类型数组缓存的内存块大小为16B ~ 512B之间,samll类型数组缓存的内存块大小为512B ~ 8K之间的内存块,normal类型数组缓存的内存块大小受DEFAULT_MAX_CACHED_BUFFER_CAPACITY配置影响,默认只缓存8K、16K、32K三种类型内存块。

内存块缓存容器,负责缓存tiny、small、normal三种内存块。其内部维护一个队列,用于缓存同种内存大小的内存块。

负责管理从 *** 作系统申请的内存,内部采用伙伴算法以Page为单位进行内存的分配与管理。

负责管理Chunk列表,根据内存使用率,分为:qInit、q000、q025、q050、q075、q100六种。每个PoolChunkList中存储内存使用率相同的Chunk,Chunk以双向链表进行关联,同时不同使用率的PoolChunkList也以双向列表进行关联。这样做的目的是因为随着内存的分配,Chunk使用率会发生变化,以链表形式方便Chunk在不同使用率列表进行移动。

PoolSubpage负责tiny、small类型内存的管理与分配,实现基于SLAB内存分配算法。PoolArena中有两种PoolSubpage类型数组,分别为:tinySubpagePools、smallSubpagePools。tinySubpagePools负责管理tiny类型内存,数组大小为512/16=32种。smallSubpagePools负责管理small类型内存,数组大小为4。

PoolSubpage数组中存储不同内存大小的PoolSubpage节点,相同大小节点以链表进行关联。PoolSubpage内部使用位图数组记录内存分配情况。

Netty通过ByteBufAllocator进行内存分配,ByteBufAllocator有两个实现类:PooledByteBufAllocator与UnpooledByteBufAllocator,其中,是否在堆内存或者直接内存分配与是否使用unsafe进行读写 *** 作都封装在其实现类中。

我们先看下ByteBufAllocator类图:

PooledByteBufAllocator与UnpooledByteBufAllocator内存分配类似,可以通过newHeapBuffer与newDirectBuffer进行分配内存,我们以PooledByteBufAllocator为例分析下内存分配流程:

以PooledByteBufAllocator为例来分析下内存分配器实例化过程。首先调用PooledByteBufAllocator#DEFAULT方法实例化PooledByteBufAllocator

PooledByteBufAllocator实例化时会初始化几个比较重要的属性:

最终会调用PooledByteBufAllocator如下构造方法:

PooledByteBufAllocator构造方法主要做了两件事情,一是:初始化PoolThreadLocalCache属性,二是:初始化堆内存与直接内存类型PoolArena数组,我们进入PoolArenaDirectArena构造方法,来分析下PoolArena初始化时主要做了哪些事情:

DirectArena构造方法会调用其父类PoolArena构造方法,在PoolArena构造方法中会初始化tiny类型与small类型PoolSubpage数组,并初始化六种不同内存使用率的PoolChunkList,每个PoolChunkList以双向链表进行关联。

以分配直接内存为例,分析内存分配的主要流程:

PooledByteBufAllocator#directBuffer方法最终会调用如下构造方法,其中maxCapacity为IntegerMAX_VALUE:

该方法主要分三步,第一步:获取线程缓存,第二步:分配内存,第三步:将ByteBuf转为具有内存泄漏检测功能的ByteBuf,我们来分析下每一步具体做了哪些事情:

1获取线程缓存,从PoolThreadLocalCache中获取PoolThreadCache,首次调用会先进行进行初始化,并将结果缓存下来:

初始化方法在PoolThreadLocalCache中,首先会循环找到使用最少的PoolArena,然后调用PoolThreadCache构造方法创建PoolThreadCache:

PoolThreadCache构造方法中会初始化tinySubPageDirectCaches、smallSubPageDirectCaches、normalDirectCaches这三种MemoryRegionCache数组:

createSubPageCaches方法中会创建并初始化MemoryRegionCache数组,其中tiny类型数组大小为32,small类型数组大小为4,normal类型数组大小为3:

最终会调用MemoryRegionCache构造方法进行创建,我们看下MemoryRegionCache结构:

2分配内存,首先会获取PooledByteBuf,然后进行内存分配:

newByteBuf方法会尝试从对象池里面获取pooledByteBuf,如果没有则进行创建。allocate方法为内存分配核心逻辑,主要分为两种分配方式:page级别内存分配(8k 16M)、subPage级别内存分配(0 8K)、huge级别内存分配(>16M)。page与subPage级别内存分配首先会尝试从缓存上进行内存分配,如果分配失败则重新申请内存。huge级别内存分配不会通过缓存进行分配。我们看下allocate方法主要流程:

首先尝试从缓存中进行分配:

cacheForTiney方法先根据分配内存大小定位到对应的tinySubPageDirectCaches数组中MemoryRegionCache,如果没有定位到则不能在缓存中进行分配。如果有则从MemoryRegionCache对应的队列中d出一个PooledByteBuf对象进行初始化,同时为了复用PooledByteBuf对象,会将其缓存下来。

如果从缓存中分配不成功,则会从对应的PoolSubpage数组上进行分配,如果PoolSubpage数组对应的内存大小下标中有可分配空间则进行分配,并对PooledByteBuf进行初始化。

如果在PoolSubpage数组上分配不成功,则表示没有可以用来分配的SubPage,则会尝试从Page上进行分配。先尝试从不同内存使用率的ChunkList进行分配,如果仍分配不成功,则表示没有可以用来分配的Chunk,此时会创建新的Chunk进行内存分配。

进入PoolChunk#allocate方法看下分配流程:

allocateRun方法用来分配大于等于8K的内存,allocateSubpage用来分配小于8K的内存,进入allocateSubpage方法:

内存分配成功后会调用initBuf方法初始化PoolByteBuf:

Page级别内存分配和SubPage级别类似,同样是先从缓存中进行分配,分配不成功则尝试从不同内存使用率的ChunkList进行分配,如果仍分配不成功,则表示没有可以用来分配的Chunk,此时会创建新的Chunk进行内存分配,不同点在allocate方法中:

因为大于16M的内存分配Netty不会进行缓存,所以Huge级别内存分配会直接申请内存并进行初始化:

调用ByteBuf#release方法会进行内存释放,方法中会判断当前byteBuf 是否被引用,如果没有被引用, 则调用deallocate方法进行释放:

进入deallocate方法看下内存释放流程:

free方法会把释放的内存加入到缓存,如果加入缓存不成功则会标记这段内存为未使用:

recycle方法会将PoolByteBuf对象放入到对象池中:

这个 需要一些基础的计算机原理知识

拿整数类型为例(int,smallintinyintbigint) 后面的括号写多少数字 就是这个数字最大10进制的位数+1 的存放空间。 另外还要看是否是支持负值。

举例 Tinyint(4) uz 就是 8位bit的整数 取值范围 0-255

三位数最大999 +1 就是4 所以4代表了 3位数,而三位数的 bit位是byte 也就是8位(二进制)整数 uz无符号。就是不支持负值。所以是0-255

tinyint(4) 就是有符号的8位整数 取值范围 -127~127

smallint(6) uz 最大值65535 最小0 也就是16位整数

int(11) 最大值 2147483647 最小 -2147483647 10位数(10进制),所以括号里写11

int(11) uz 最大值就是4294967295,最小0

以此类推,所以 bigint(20) 对应的就是长整形(64位long), tinyint(1) 对应的就是1bit 也就是 0或者1 用于表示bool

enum枚举实际上可以是 8位、16位、32位整数的 枚举型式。mysql的美剧比较奇怪 是从1开始算 不是0

set 的话 实际上 也可以是 8位 16 位 32位 64位等等的 集合类型。 64个元素就是64位的bits

至于字符串实际上是 char的数组 如果是utf8编码实际对应的bits还不一定相等。utf16的话 如果支持Notnull的字符串,每个字符一定需要16bits 如果可以为null的话 支持的长度 相当于少两个字节 也就是 少16bits

比如varchar(16) notnull 实际需要 16~48个字节的存储空间 utf-8编码就是这么蛋疼 utf-16

编码就固定为32个字节。(英文字符多的话,反而浪费空间。全汉字省空间)

varchar(15) 可以为null的话 实际需要16~48个字节的存储空间。

之前在SpringBoot项目中一直使用的是SpringFox提供的Swagger库,上了下官网发现已经有接近两年没出新版本了!前几天升级了SpringBoot 26x 版本,发现这个库的兼容性也越来越不好了,有的常用注解属性被废弃了居然都没提供替代!无意中发现了另一款Swagger库SpringDoc,试用了一下非常不错,推荐给大家!

SpringDoc简介

SpringDoc是一款可以结合SpringBoot使用的API文档生成工具,基于OpenAPI 3,目前在Github上已有17K+Star,更新发版还是挺勤快的,是一款更好用的Swagger库!值得一提的是SpringDoc不仅支持Spring WebMvc项目,还可以支持Spring WebFlux项目,甚至Spring Rest和Spring Native项目,总之非常强大,下面是一张SpringDoc的架构图。

使用

接下来我们介绍下SpringDoc的使用,使用的是之前集成SpringFox的mall-tiny-swagger项目,我将把它改造成使用SpringDoc。

集成

首先我们得集成SpringDoc,在pomxml中添加它的依赖即可,开箱即用,无需任何配置。

<!--springdoc 官方Starter-->orgspringdocspringdoc-openapi-ui166

从SpringFox迁移

我们先来看下经常使用的Swagger注解,看看SpringFox的和SpringDoc的有啥区别,毕竟对比已学过的技术能该快掌握新技术;

接下来我们对之前Controller中使用的注解进行改造,对照上表即可,之前在@Api注解中被废弃了好久又没有替代的description属性终于被支持了!

/

品牌管理Controller

Created by macro on 2019/4/19

/@Tag(name ="PmsBrandController", description ="商品品牌管理")@Controller@RequestMapping("/brand")publicclassPmsBrandController{@AutowiredprivatePmsBrandService brandService;privatestaticfinalLogger LOGGER = LoggerFactorygetLogger(PmsBrandControllerclass);@Operation(summary ="获取所有品牌列表",description ="需要登录后访问")@RequestMapping(value ="listAll", method = RequestMethodGET)@ResponseBodypublicCommonResult> getBrandList() {returnCommonResultsuccess(brandServicelistAllBrand());    }@Operation(summary ="添加品牌")@RequestMapping(value ="/create", method = RequestMethodPOST)@ResponseBody@PreAuthorize("hasRole('ADMIN')")publicCommonResult createBrand(@RequestBodyPmsBrand pmsBrand) {        CommonResult commonResult;        int count = brandServicecreateBrand(pmsBrand);if(count ==1) {            commonResult = CommonResultsuccess(pmsBrand);            LOGGERdebug("createBrand success:{}", pmsBrand);        }else{            commonResult = CommonResultfailed(" *** 作失败");            LOGGERdebug("createBrand failed:{}", pmsBrand);        }returncommonResult;    }@Operation(summary ="更新指定id品牌信息")@RequestMapping(value ="/update/{id}", method = RequestMethodPOST)@ResponseBody@PreAuthorize("hasRole('ADMIN')")publicCommonResult updateBrand(@PathVariable("id")Longid,@RequestBodyPmsBrand pmsBrandDto, BindingResult result) {        CommonResult commonResult;        int count = brandServiceupdateBrand(id, pmsBrandDto);if(count ==1) {            commonResult = CommonResultsuccess(pmsBrandDto);            LOGGERdebug("updateBrand success:{}", pmsBrandDto);        }else{            commonResult = CommonResultfailed(" *** 作失败");            LOGGERdebug("updateBrand failed:{}", pmsBrandDto);        }returncommonResult;    }@Operation(summary ="删除指定id的品牌")@RequestMapping(value ="/delete/{id}", method = RequestMethodGET)@ResponseBody@PreAuthorize("hasRole('ADMIN')")publicCommonResult deleteBrand(@PathVariable("id")Longid) {        int count = brandServicedeleteBrand(id);if(count ==1) {            LOGGERdebug("deleteBrand success :id={}", id);returnCommonResultsuccess(null);        }else{            LOGGERdebug("deleteBrand failed :id={}", id);returnCommonResultfailed(" *** 作失败");        }    }@Operation(summary ="分页查询品牌列表")@RequestMapping(value ="/list", method = RequestMethodGET)@ResponseBody@PreAuthorize("hasRole('ADMIN')")publicCommonResult> listBrand(@RequestParam(value ="pageNum", defaultValue ="1")@Parameter(description ="页码")Integer pageNum,@RequestParam(value ="pageSize", defaultValue ="3")@Parameter(description ="每页数量")Integer pageSize) {        List brandList = brandServicelistBrand(pageNum, pageSize);returnCommonResultsuccess(CommonPagerestPage(brandList));    }@Operation(summary ="获取指定id的品牌详情")@RequestMapping(value ="/{id}", method = RequestMethodGET)@ResponseBody@PreAuthorize("hasRole('ADMIN')")publicCommonResult brand(@PathVariable("id")Longid) {returnCommonResultsuccess(brandServicegetBrand(id));    }}

接下来进行SpringDoc的配置,使用OpenAPI来配置基础的文档信息,通过GroupedOpenApi配置分组的API文档,SpringDoc支持直接使用接口路径进行配置。

/

SpringDoc API文档相关配置

Created by macro on 2022/3/4

/@ConfigurationpublicclassSpringDocConfig{@BeanpublicOpenAPImallTinyOpenAPI(){returnnewOpenAPI()                info(newInfo()title("Mall-Tiny API")                        description("SpringDoc API 演示")                        version("v100")                        license(newLicense()name("Apache 20")url(">

一般来说当内存空间span不足时,需要进行扩容。而在扩容前需要将当前没有剩余空间的内存块相关状态解除,以便后续的垃圾回收期能够进行扫描和回收,接着在从中间部件(central)提取新的内存块放回数组中。

需要注意由于中间部件有scan和noscan两种类型,则申请的内存空间最终获取的可能是其两倍,并由heap堆进行统一管理。中间部件central是通过两个链表来管理其分配的所有内存块:

1、empty代表“无法使用”状态,没有剩余的空间或被移交给缓存的内存块

2、noempty代表剩余的空间,并这些内存块能够提供服务

由于golang垃圾回收器使用的累增计数器(heapsweepgen)来表达代龄的:

从上面内容可以看到每次进行清理 *** 作时 该计数器 +2

再来看下mcentral的构成

当通过mcentral进行空间span获取时,第一步需要到noempty列表检查剩余空间的内存块,这里面有一点需要说明主要是垃圾回收器的扫描过程和清理过程是同时进行的,那么为了获取更多的可用空间,则会在将分配的内存块移交给cache部件前,先完成清理的 *** 作。第二步当noempty没有返回时,则需要检查下empty列表(由于empty里的内存块有可能已被标记为垃圾,这样可以直接清理,对应的空间则可直接使用了)。第三步若是noempty和empty都没有申请到,这时需要堆进行申请内存的

通过上面的源码也可以看到中间部件central自身扩容 *** 作与大对象内存分配差不多类似。

在golang中将长度小于16bytes的对象称为微小对象(tiny),最常见的就是小字符串,一般会将这些微小对象组合起来,并用单块内存存储,这样能够有效的减少内存浪费。

当微小对象需要分配空间span,首先缓存部件会按指定的规格(tiny size class)取出一块内存,若容量不足,则重新提取一块;前面也提到会将微小对象进行组合,而这些组合的微小对象是不能包含指针的,因为垃圾回收的原因,一般都是当前存储单元里所有的微小对象都不可达时,才会将该块内存进行回收。

而当从缓冲部件cache中获取空间span时, 是通过偏移位置(tinyoffset)先来判断剩余空间是否满足需求。若是可以的话则以此计算并返回内存地址;若是空间不足,则提取新的内存块,直接返回起始地址便可; 最后在对比新旧两块内存,空间大的那块则会被保留。

可以安装。1 安装tiny11比较简单。

2 首先需要下载tiny11安装包,并按照提示进行安装。

建议从官方网站下载,并防备第三方软件搭配安装时携带的病毒等风险。

安装过程中需要选择安装路径、添加路径、选择可选组件等,并按照提示完成。

3 安装后,使用时需要注意调试和编译环境的搭建。

Tiny11是一种微型单片机,其使用需要相应的编译器和开发环境支持。

建议在安装完成后,阅读相关的开发教程,掌握基本的编程和开发技能。

1如何快速升级,这是每个人问我的问题,我的答案只有一种:黑鸡流+小麦(排除有些人刷神草+白鸡流,这个我尝试了一天放弃了,太累人,太BT,不过大家可以尝试,升级太快了)。

(1)黑鸡流+小麦,每一个小时看一次,时间有保证,也不是特别累,赚钱也快;前期可以这样攒钱买动物证和农田,其他任何装饰物,还是免了吧,因为以后一个动物证要你上万上十万的时候你会后悔的。

(2)黑鸡流的意思,要一直保持你的黑鸡上限,不要卖掉(除非你的黑鸡到101级的时候,果断配种,把101级的卖掉)如果为了升级快,还是保证动物槽是满的,全部是黑鸡,如果爱心够的话,也有一个剩余的动物槽,用级别高的两只种(zhong三声)黑鸡配种;生一只黑鸡,这样循环下去。(黑鸡配种不是100%出黑鸡的哦,如果想靠升黑鸡赚钱,那你绝对亏大了)

2如何攒钱更快,主要还是黑鸡+小麦;别的有人说养白猪或者羊,也行,但是没有黑鸡爽,毕竟黑鸡的卖价高,而且成长也快(1个小时);所以想快速升级+攒钱的同学可以采纳;

(1)农作物,如果在一定时间内不收割的话,会坏掉哦,各位亲请一定要及时收割自己所种植的蔬菜哦。

3任务,只跟着“羊Sir”的任务走就行,至于下面规划农场的任务,实在是没什么意义;

4农场规划:必须要有的东西,爱心屋最好配备上,也最好升到满级,很有用(爱心屋的作用:再升满级后,每六个小时产生5个爱心);至于食品屋和茶餐厅,鸡肋啊;没什么用,给那点东西还不够塞牙缝的。(没置办的同学别买了,浪费)

5铃铛获取,每个动物升级到M3和M5,都会给一个铃铛,这个铃铛要留好,为了以后买铃铛动物准备。

(1)铃铛最好只用来买农田和开动物槽,如果大家不爱在猎人酒吧抓动物的话,用铃铛买shop里的铃铛动物也可以;毕竟用金币抓动物,失败率太高了。

6猎人酒吧,我研究了一下,每六个小时刷新一次;(如果用铃铛抓动物,最好用一个铃铛加速,比较快,你都花了几十,几百铃铛了,不在乎那一个;所以果断加速吧。抓铃铛动物,9999%成功,不知道会不会失败,所以没写100%,见谅)

(1)用金币抓动物,再用铃铛加速,有很大程度失败,当然了,金币抓动物本来失败率就高。

7扩建农场,一定要扩建比较有用,不然到后期,你的农场会比较拥挤的。

(1)如果只养黑鸡的话,可以到28级左右再扩建农场,毕竟黑鸡占不了多少地方;

(2)有些时候黑鸡比较小,不用于用手点到,大家可以建几个格子,把黑鸡都放到一个格子里;左上角的那个扳手里面有个“REDESIGN”可以很方便的完成这个工作。

(3)规划农场尽量在满级之前规划,这样买的材料增加的经验不至于浪费。

8动物配种,这是个关键性问题,跟群里的朋友交流了很久,也没什么好的经验,我感觉每个人都会被某一种动物卡死,配种了几十次都不出新物种;不过这里有一些经验大家可以借鉴,不一定准确,但值得尝试;

(1)动物都有一个配种高峰期,大家要善于总结,善于记录;

(2)羊Sir让大家在动物商店买的动物,大家最好不要卖掉,这个是合成的关键,如果卖掉的话,出新物种的几率会少点;(具体少多少不知,会稍微困难些)

(3)最好攒一下爱心,这里说下攒爱心,每个人的爱心都有上限,到达上限后,就不会增加了,但是你如果买了爱心屋,这个不受上限控制的,还有就是访问好友农场也会增加爱心+1;所以大家要多多增加好友,大家的好友最好增加2页就够了,多了没用,现在这个系统一次只能访问15名好友,多了也不会增加爱心了,所以没意义。

(4)集中交配,就是如果这个动物不容易出的话,最好采用2连击,或者3连击,冲击下一级物种,这个方法还不错,大家可以尝试;

(5)如果遇到配种N次,还不能出新物种,那就别冲动了,冲动是魔鬼,要努力把动物升一级,之后再配种,几率会高一些;(动物的级别越高,出现新物种的几率会增加)

(6)杂交,这个方法也不错,1级物种+2级物种,配种很可能会出3级物种,不一定非要按照羊Sir的来,哈哈。

(7)一旦出来两只二代动物,果断把一代动物卖掉,腾出空间给其他的动物。

9羊Sir任务,这里说明一下,大家可以提前完成羊Sir的下一级任务,比如说,当前任务让配种羊,你可以再从SHOP里买两只鸡,同时配种,如果鸡的任务也配种到第三代后,羊的任务做完后,鸡的任务会自动跳过的,直接做猪的任务了;强调下,用铃铛买的动物不在羊Sir任务中,所以大家要注意。

(1)配种动物任务如下:绵羊--鸡--猪--奶牛--波中猪--景黄鸡--牛--马--耗牛--须野猪--斑马--斑点兔(新增物种)

(2)SHOP卖不到的动物,比如说,野猪,独角兽,只能从猎人酒吧抓了。

10关于动物的去留,如果大家纯粹是为了收集雕塑,那就可以把动物都卖掉,如果大家有更高的追求(比如AC老师,ID:Acred),那大家还是果断的留下第三级物种吧,以后会有帮助的,如果有条件的话,最后弄两只三代动物。还有铃铛买来的动物,第三极物种要留好,别手贱卖了,以后配种起来麻烦的要死。

11BUG:需要大家注意,world map里面进去后,管理好友只能翻4页,第五页,点开后直接秒退,很烦人啊;

(1)解决方法:可以直接翻页到第六页 再按管理,就可以直接看到所有的好友

12成就:大家玩的时候,最好先登录GameCenter,从那里面”玩游戏“这样大家在完成羊Sir的任务后就会得到一个成就的哦。如果不登录,有可能成就完成不了的。(这些暂时是个BUG,希望程序组早日修复。)

13满级增加经验,群中各位好友已经研究出来了,只有几个 *** 作对增加XP有帮助,大家自己去探索吧;这里就不细说了。

14最后说一下怎么加邻居;在大家农场的左下角,都有一个"world map”,点击进入;之后右上角有一个“manage friends”,点击之后就可以对邻居进行维护了,可以INVITE好友,也可以DELETE好友。

(1)增加邻居的好处,每访问一次邻居可以增加爱心一个;同时抚摸对方的动物,也会给邻居的动物增加一个爱心;抚摸动物,同时也会增加经验,抚摸级别越高的动物,所获得的经验越多。所以大家要多多增加邻居,这样邻居访问你的同时,也会抚摸你的动物呢。

(2)别忘了在邻居留言板留言哦,这样才能更快的被邻居发现。

(3)农场留言一般去别人留言那比较好,不然别人都不知道你回复他了。

游戏内,共有12个道具:4个能力道具,4把钥匙,4个爱心(增加血量上限),均由开宝箱固定获取。打开地图,带小点的地方是还未获取的道具,尤其注意深绿色的方框;有“凸”标志的地方代表存档点。

四个能力道具(按获取顺序)分别为:

动力手套。可以推动重物,其实就是类似于俄罗斯方块一样的方块;

biubiu刀。鬼见也发愁的无敌小飞刀;

踏水无痕鞋。展示绝世轻功的时候到了,不过去取BOSS房间钥匙的路上会变成熔岩,此鞋无效;

销魂钉。白子画都快受不鸟的钉子,小怪哪能受得了。当然除了杀怪之外,还可以踩怪,借力蹦跶,最终实现“更高、更远、更强”的奥运口号。

整张地图最难之处,便是获取BOSS房间钥匙的,而且还要返回存档点,否则白拿。

一个深绿色房间又下层跳上去;

一处由瀑布之下的一个移动踏板摆渡进入暗门。

一处进门后发现右侧全是瀑布,此时将左侧上方的方块推下来,推倒瀑布边,便可以跳过去了。

最终BOSS:

拿到最终钥匙之后,一进门,小人立即开始蹦跶,谁知平地一声吼,蜘蛛BOSS来也。

BOSS攻击较为简单,就一招(如图),间隔30°,同时吐出三个泥蛋蛋。

BOSS走位也是简单,就像打砖块一样,d来d去,往你身上蹭,倒是走位风骚点,很容易应对。

BOSS打法更是简单,十刀,就是十刀,轻松搞定。

打爆之后,通关。

继续游戏后,会从打BOSS之前那个存档开始。

以上就是关于Netty内存管理全部的内容,包括:Netty内存管理、请教高手:SQL如何获取某个数据类型的最大值、神器 SpringDoc 横空出世!最适合 SpringBoot 的API文档工具来了等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/web/9356501.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-27
下一篇2023-04-27

发表评论

登录后才能评论

评论列表(0条)

    保存