gc优化策略-下篇

何文西 何文西     2022-11-09     538

关键词:

原文:http://www.cnblogs.com/zblade/

英文:英文地址

 

造成不必要的堆内存分配的因素

  我们已经知道值类型变量在堆栈上分配,其他的变量在堆内存上分配,但是任然有一些情况下的堆内存分配会让我们感到吃惊。下面让我们分析一些常见的不必要的堆内存分配行为并对其进行优化。

  字符串  

   在c#中,字符串是引用类型变量而不是值类型变量,即使看起来它是存储字符串的值的。这就意味着字符串会造成一定的内存垃圾,由于代码中经常使用字符串,所以我们需要对其格外小心。

  c#中的字符串是不可变更的,也就是说其内部的值在创建后是不可被变更的。每次在对字符串进行操作的时候(例如运用字符串的“加”操作),unity会新建一个字符串用来存储新的字符串,使得旧的字符串被废弃,这样就会造成内存垃圾。

  我们可以采用以下的一些方法来最小化字符串的影响:

  1)减少不必要的字符串的创建,如果一个字符串被多次利用,我们可以创建并缓存该字符串。

  2)减少不必要的字符串操作,例如如果在Text组件中,有一部分字符串需要经常改变,但是其他部分不会,则我们可以将其分为两个部分的组件,对于不变的部分就设置为类似常量字符串即可,见下面的例子。

  3)如果我们需要实时的创建字符串,我们可以采用StringBuilderClass来代替,StringBuilder专为不需要进行内存分配而设计,从而减少字符串产生的内存垃圾。

  4)移除游戏中的Debug.Log()函数的代码,尽管该函数可能输出为空,对该函数的调用依然会执行,该函数会创建至少一个字符(空字符)的字符串。如果游戏中有大量的该函数的调用,这会造成内存垃圾的增加。

  在下面的代码中,在Update函数中会进行一个string的操作,这样的操作就会造成不必要的内存垃圾:

1
2
3
4
5
6
7
public Text timerText;
private float timer;
void Update()
    timer += Time.deltaTime;
    timerText.text = "Time:"+ timer.ToString();

  通过将字符串进行分隔,我们可以剔除字符串的加操作,从而减少不必要的内存垃圾:

1
2
3
4
5
6
7
8
9
10
11
12
public Text timerHeaderText;
public Text timerValueText;
private float timer;
void Start()
    timerHeaderText.text = "TIME:";
 
void Update()
   timerValueText.text = timer.ToString();

  Unity函数调用

  在代码编程中,当我们调用不是我们自己编写的代码,无论是Unity自带的还是插件中的,我们都可能会产生内存垃圾。Unity的某些函数调用会产生内存垃圾,我们在使用的时候需要注意它的使用。

  这儿没有明确的列表指出哪些函数需要注意,每个函数在不同的情况下有不同的使用,所以最好仔细地分析游戏,定位内存垃圾的产生原因以及如何解决问题。有时候缓存是一种有效的办法,有时候尽量降低函数的调用频率是一种办法,有时候用其他函数来重构代码是一种办法。现在来分析unity中常见的造成堆内存分配的函数调用。

  在Unity中如果函数需要返回一个数组,则一个新的数组会被分配出来用作结果返回,这不容易被注意到,特别是如果该函数含有迭代器,下面的代码中对于每个迭代器都会产生一个新的数组:

void ExampleFunction()

    for(int i=0; i < myMesh.normals.Length;i++)
    
        Vector3 normal = myMesh.normals[i];
    

  对于这样的问题,我们可以缓存一个数组的引用,这样只需要分配一个数组就可以实现相同的功能,从而减少内存垃圾的产生:

1
2
3
4
5
6
7
8
void ExampleFunction()
    Vector3[] meshNormals = myMesh.normals;
    for(int i=0; i < meshNormals.Length;i++)
    
        Vector3 normal = meshNormals[i];
    

  此外另外的一个函数调用GameObject.name 或者 GameObject.tag也会造成预想不到的堆内存分配,这两个函数都会将结果存为新的字符串返回,这就会造成不必要的内存垃圾,对结果进行缓存是一种有效的办法,但是在Unity中都对应的有相关的函数来替代。对于比较gameObject的tag,可以采用GameObject.CompareTag()来替代。

  在下面的代码中,调用gameobject.tag就会产生内存垃圾:

1
2
3
4
5
private string playerTag="Player";
void OnTriggerEnter(Collider other)
    bool isPlayer = other.gameObject.tag == playerTag;

  采用GameObject.CompareTag()可以避免内存垃圾的产生:

1
2
3
4
5
private string playerTag = "Player";
void OnTriggerEnter(Collider other)
    bool isPlayer = other.gameObject.CompareTag(playerTag);

  不只是GameObject.CompareTag,unity中许多其他的函数也可以避免内存垃圾的生成。比如我们可以用Input.GetTouch()和Input.touchCount()来代替Input.touches,或者用Physics.SphereCastNonAlloc()来代替Physics.SphereCastAll()。

  装箱操作

  装箱操作是指一个值类型变量被用作引用类型变量时候的内部变换过程,如果我们向带有对象类型参数的函数传入值类型,这就会触发装箱操作。比如String.Format()函数需要传入字符串和对象类型参数,如果传入字符串和int类型数据,就会触发装箱操作。如下面代码所示:

1
2
3
4
5
void ExampleFunction()
    int cost = 5;
    string displayString = String.Format("Price:0 gold",cost);

  在Unity的装箱操作中,对于值类型会在堆内存上分配一个System.Object类型的引用来封装该值类型变量,其对应的缓存就会产生内存垃圾。装箱操作是非常普遍的一种产生内存垃圾的行为,即使代码中没有直接的对变量进行装箱操作,在插件或者其他的函数中也有可能会产生。最好的解决办法是尽可能的避免或者移除造成装箱操作的代码。

  协程

  调用 StartCoroutine()会产生少量的内存垃圾,因为unity会生成实体来管理协程。所以在游戏的关键时刻应该限制该函数的调用。基于此,任何在游戏关键时刻调用的协程都需要特别的注意,特别是包含延迟回调的协程。

  yield在协程中不会产生堆内存分配,但是如果yield带有参数返回,则会造成不必要的内存垃圾,例如:

1
yield return 0;

  由于需要返回0,引发了装箱操作,所以会产生内存垃圾。这种情况下,为了避免内存垃圾,我们可以这样返回:

1
yield return null;

  另外一种对协程的错误使用是每次返回的时候都new同一个变量,例如:

1
2
3
4
while(!isComplete)
    yield return new WaitForSeconds(1f);

  我们可以采用缓存来避免这样的内存垃圾产生:

1
2
3
4
5
WaitForSeconds delay = new WaiForSeconds(1f);
while(!isComplete)
    yield return delay;

  如果游戏中的协程产生了内存垃圾,我们可以考虑用其他的方式来替代协程。重构代码对于游戏而言十分复杂,但是对于协程而言我们也可以注意一些常见的操作,比如如果用协程来管理时间,最好在update函数中保持对时间的记录。如果用协程来控制游戏中事件的发生顺序,最好对于不同事件之间有一定的信息通信的方式。对于协程而言没有适合各种情况的方法,只有根据具体的代码来选择最好的解决办法。

  foreach 循环

  在unity5.5以前的版本中,在foreach的迭代中都会生成内存垃圾,主要来自于其后的装箱操作。每次在foreach迭代的时候,都会在堆内存上生产一个System.Object用来实现迭代循环操作。在unity5.5中解决了这个问题,比如,在unity5.5以前的版本中,用foreach实现循环:

1
2
3
4
5
6
7
void ExampleFunction(List listOfInts)
    foreach(int currentInt in listOfInts)
    
        DoSomething(currentInt);
    

  如果游戏工程不能升级到5.5以上,则可以用for或者while循环来解决这个问题,所以可以改为:

1
2
3
4
5
6
7
8
void ExampleFunction(List listOfInts)
    for(int i=0; i < listOfInts.Count; i++)
    
        int currentInt = listOfInts[i];
        DoSomething(currentInt);
    

  函数引用

   函数的引用,无论是指向匿名函数还是显式函数,在unity中都是引用类型变量,这都会在堆内存上进行分配。匿名函数的调用完成后都会增加内存的使用和堆内存的分配。具体函数的引用和终止都取决于操作平台和编译器设置,但是如果想减少GC最好减少函数的引用。

  LINQ和常量表达式

  由于LINQ和常量表达式以装箱的方式实现,所以在使用的时候最好进行性能测试。

重构代码来减小GC的影响

  即使我们减小了代码在堆内存上的分配操作,代码也会增加GC的工作量。最常见的增加GC工作量的方式是让其检查它不必检查的对象。struct是值类型的变量,但是如果struct中包含有引用类型的变量,那么GC就必须检测整个struct。如果这样的操作很多,那么GC的工作量就大大增加。在下面的例子中struct包含一个string,那么整个struct都必须在GC中被检查:

1
2
3
4
5
6
7
public struct ItemData
    public string name;
    public int cost;
    public Vector3 position;
private ItemData[] itemData;

  我们可以将该struct拆分为多个数组的形式,从而减小GC的工作量:

1
2
3
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;

  另外一种在代码中增加GC工作量的方式是保存不必要的Object引用,在进行GC操作的时候会对堆内存上的object引用进行检查,越少的引用就意味着越少的检查工作量。在下面的例子中,当前的对话框中包含一个对下一个对话框引用,这就使得GC的时候会去检查下一个对象框:

1
2
3
4
5
6
7
8
9
public class DialogData
     private DialogData nextDialog;
     public DialogData GetNextDialog()
     
           return nextDialog;
                     
     

  通过重构代码,我们可以返回下一个对话框实体的标记,而不是对话框实体本身,这样就没有多余的object引用,从而减少GC的工作量:

1
2
3
4
5
6
7
8
public class DialogData
    private int nextDialogID;
    public int GetNextDialogID()
    
       return nextDialogID;
    

  当然这个例子本身并不重要,但是如果我们的游戏中包含大量的含有对其他Object引用的object,我们可以考虑通过重构代码来减少GC的工作量。 

定时执行GC操作

  主动调用GC操作

   如果我们知道堆内存在被分配后并没有被使用,我们希望可以主动地调用GC操作,或者在GC操作并不影响游戏体验的时候(例如场景切换的时候),我们可以主动的调用GC操作:

1
System.GC.Collect()

  通过主动的调用,我们可以主动驱使GC操作来回收堆内存。

总结

  通过本文对于unity中的GC有了一定的了解,对于GC对于游戏性能的影响以及如何解决都有一定的了解。通过定位造成GC问题的代码以及代码重构我们可以更有效的管理游戏的内存。

  接着我会继续写一些Unity相关的文章。翻译的工作,在后面有机会继续进行。

jvm配置常用参数和常用gc调优策略(代码片段)

...记住下面的原则:多数的Java应用不需要在服务器上进行GC优化;多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);减少创建对象的数量;... 查看详情

从实际案例聊聊java应用的gc优化

当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化。但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这... 查看详情

javagc调优

当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化。但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加... 查看详情

从golang的垃圾回收说起(下篇)(代码片段)

文章来自网易云社区 4Golang垃圾回收的相关参数 4.1触发GC gc触发的时机:2分钟或者内存占用达到一个阈值(当前堆内存占用是上次gc后对内存占用的两倍,当GOGC=100时)  # 表示当前应用占用的内存是上次GC... 查看详情

45.jvm调优策略常见问题:内存泄漏(年老代堆空间被占满持久代被占满堆栈溢出线程堆栈满系统内存被占满)优化方法:优化目标优化gc步骤优化总结;案例分析(公司系统参数网上给的配置参数)(代码片段)

...1.3.堆栈溢出45.1.1.4.线程堆栈满45.1.1.5.系统内存被占满45.2.优化方法45.2.1.优化目标45.2.2.优化GC步骤45.2.3.优化总结45.3.案例分析45.3.1.案例1IntellijIDEA2016优化45.3.2.公司系统参数 查看详情

hbase最佳实践-cmsgc调优(从gc本身参数调优)

...重要的不能再重要了1、HBase发展到当下,对其进行的各种优化从未停止,而GC优化更是其中的重中之重。hbasegc调优方向从0.94版本提出MemStoreLAB策略、MemstoreChuckPool策略对写缓存Memstore进行优化开始,到0.96版本提出BucketCache以及堆... 查看详情

从golang的垃圾回收说起(下篇)(代码片段)

文章来自网易云社区4Golang垃圾回收的相关参数4.1触发GCgc触发的时机:2分钟或者内存占用达到一个阈值(当前堆内存占用是上次gc后对内存占用的两倍,当GOGC=100时) # 表示当前应用占用的内存是上次GC时占用内存的两倍... 查看详情

常见的一些反爬虫策略(下篇)-java网络爬虫系统性学习与实战系列(10)

常见的一些反爬虫策略(下篇)-Java网络爬虫系统性学习与实战系列(10)文章目录联系方式反爬虫策略文本混淆SVG映射CSS文字偏移图片混淆伪装字体反爬Referer字段反爬数据分段加载权限控制反爬加密反爬总结系列文章地址:Java网... 查看详情

常见的一些反爬虫策略(下篇)-java网络爬虫系统性学习与实战系列(10)

常见的一些反爬虫策略(下篇)-Java网络爬虫系统性学习与实战系列(10)文章目录联系方式反爬虫策略文本混淆SVG映射CSS文字偏移图片混淆伪装字体反爬Referer字段反爬数据分段加载权限控制反爬加密反爬总结系列文章地址:Java网... 查看详情

jvm优化

参数基本策略各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是FullGC后堆中老年... 查看详情

jvm05-jvm垃圾收集策略(代码片段)

GC策略新生代GC策略:串行GC:SerialCopying并行回收GC:ParallelScavenge并行GC:ParNew老年代GC策略:串行GC:SerialMSC并行GC:ParallelMSC并发GC:CMC新生代GC策略新生代--串行GC(SerialCopying)算法:复制清理算法操作步骤:扫描新生代中所有... 查看详情

android性能优化之内存泄漏检测以及内存优化(上)

...以记录一下针对Android应用的内存泄漏的检测,处理和优化的相关内容,上篇主要会分析Java/Android的内存分配以及GC的详细分析,中篇会阐述Android内存泄漏的检测和内存泄漏的常见产生情景,下篇会分析一下内存优... 查看详情

jvm分代gc策略分析

JVM分代GC策略分析 我们以SunHotSpotVM来进行分析,首先应该知道,如果我们没有指定任何GC策略的时候,JVM默认使用的GC策略。Java虚拟机是按照分代的方式来回收垃圾空间,我们应该知道,垃圾回收主要是针对堆(Heap)内存进... 查看详情

jvm分代gc策略分析

...otVM来进行分析,首先应该知道,如果我们没有指定任何GC策略的时候,JVM默认使用的GC策略。Java虚拟机是按照分代的方式来回收垃圾空间,我们应该知道,垃圾回收主要是针对堆(Heap)内存进行分代回收,将对内存可以分成新... 查看详情

4kw机柜无通道封闭cfd模拟分析及优化(下篇)

...上篇文章,【技术分析】4kw机柜无通道封闭CFD模拟分析及优化(上篇)进行解析。2、方案2(单侧送风)CFD模型分析1)、方案2(单侧送风)模型基本参数方案2(单侧送风):?数据机房总面积:600m2,其中精密空调间面积:160m2。?IT机柜数量160... 查看详情

unity优化之gc——合理优化unity的gc

...工作进行的有点缓慢=。=  本文续接前面的unity的渲染优化,进一步翻译Unity中的GC优化,英文链接在下:英文地址介绍:  在游戏运行的时候,数据主要存储在内存中,当游戏的数据不在需要的时候,存储当前数据的内存就... 查看详情

chromev8系列--浅析chromev8引擎中的垃圾回收机制和内存泄露优化策略[转](代码片段)

 V8实现了准确式GC,GC算法采用了分代式垃圾回收机制。因此,V8将内存(堆)分为新生代和老生代两部分。 一、前言V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存。垃圾回收是一把双刃剑,其好处是可以... 查看详情

jvm之gc日志分析与对象内存分配回收策略(代码片段)

GC日志分析与内存分配回收策略一.GC日志分析的引入二.GC的分类与GC日志结构剖析三.JDK1.9以前的日志分析四.JDK1.9以后的日志分析五.对象内存分配回收策略一.GC日志分析的引入🐬GC日志分析的重要性:阅读分析虚拟机和垃... 查看详情