Debugowanie błędów LMK

Rozwiązywanie problemów z wyczerpaniem pamięci w grze w Unity to systematyczny proces:

Rysunek 1. Kroki rozwiązywania problemów z wyczerpaniem pamięci w grach w Unity.

Pobieranie zrzutu pamięci

Użyj profilera Unity, aby uzyskać zrzut pamięci zarządzanej przez Unity. Rysunek 2 przedstawia warstwy zarządzania pamięcią, których Unity używa do obsługi pamięci w grze.

Rysunek 2. Omówienie zarządzania pamięcią w Unity.

Pamięć zarządzana

Zarządzanie pamięcią w Unity obejmuje warstwę pamięci kontrolowanej która używa zarządzanej sterty i modułu odśmiecania do automatycznego przydzielania i przypisywania pamięci. System pamięci zarządzanej to środowisko skryptowe C# oparte na Mono lub IL2CPP. Zaletą systemu pamięci zarządzanej jest to, że wykorzystuje on moduł odśmiecania do automatycznego zwalniania przydzielonej pamięci.

Pamięć niezarządzana w C#

Warstwa pamięci niezarządzanej w C# zapewnia dostęp do warstwy pamięci natywnej, co umożliwia precyzyjną kontrolę nad przydzielaniem pamięci podczas korzystania z kodu C# . Dostęp do tej warstwy zarządzania pamięcią można uzyskać za pomocą przestrzeni nazw Unity.Collections oraz funkcji takich jak UnsafeUtility.Malloc i UnsafeUtility.Free.

Pamięć natywna

Wewnętrzny rdzeń C/C++ Unity używa natywnego systemu pamięci do zarządzania scenami, zasobami, interfejsami API grafiki, sterownikami, podsystemami i buforami wtyczek. Chociaż bezpośredni dostęp jest ograniczony, możesz bezpiecznie manipulować danymi za pomocą interfejsu API C# Unity i korzystać z wydajnego kodu natywnego. Pamięć natywna rzadko wymaga bezpośredniej interakcji, ale możesz monitorować jej wpływ na wydajność za pomocą profilera i dostosowywać ustawienia w celu optymalizacji wydajności.

Pamięć nie jest współdzielona między kodem C# a kodem natywnym, jak pokazano na rysunku 3. Dane wymagane przez C# są przydzielane w zarządzanej przestrzeni pamięci za każdym razem, gdy są potrzebne.

Aby kod zarządzanej gry (C#) mógł uzyskać dostęp do danych z pamięci natywnej silnika, np. wywołanie GameObject.transform powoduje wywołanie natywne w celu uzyskania dostępu do danych w pamięci natywnej, a następnie zwraca wartości do C# za pomocą Bindings. Powiązania zapewniają odpowiednie konwencje wywoływania dla każdej platformy i obsługują automatyczne przekształcanie typów zarządzanych na ich odpowiedniki natywne.

Dzieje się tak tylko za pierwszym razem, ponieważ zarządzana powłoka do uzyskiwania dostępu do właściwości transform jest zachowywana w kodzie natywnym. Buforowanie właściwości transformacji może zmniejszyć liczbę wywołań między kodem zarządzanym a kodem natywnym, ale przydatność buforowania zależy od tego, jak często ta właściwość jest używana. Pamiętaj też, że Unity nie kopiuje części pamięci natywnej do pamięci zarządzanej, gdy uzyskujesz dostęp do tych interfejsów API.

Rysunek 3. Uzyskiwanie dostępu do pamięci natywnej z kodu zarządzanego C#.

Więcej informacji znajdziesz w artykule Wprowadzenie do pamięci w Unity.

Ponadto ustalenie budżetu pamięci jest kluczowe, aby gra działała płynnie, a wdrożenie systemu analizy lub raportowania zużycia pamięci zapewnia, że każda nowa wersja nie przekracza budżetu pamięci. Kolejną strategią pozwalającą uzyskać lepszy wgląd jest zintegrowanie testów w trybie odtwarzania z ciągłą integracją (CI), aby sprawdzić zużycie pamięci w określonych obszarach gry.

Zarządzanie zasobami

Jest to najbardziej skuteczna i praktyczna część zużycia pamięci. Profiluj jak najwcześniej.

Wykorzystanie pamięci w grach na Androida może się znacznie różnić w zależności od typu gry, liczby i typów zasobów oraz strategii optymalizacji pamięci. Do typowych czynników wpływających na wykorzystanie pamięci należą jednak tekstury, siatki, pliki audio, shadery, animacje i skrypty.

Wykrywanie zduplikowanych zasobów

Pierwszym krokiem jest wykrycie źle skonfigurowanych zasobów i zduplikowanych zasobów za pomocą profilera pamięci, narzędzia do raportowania kompilacji lub narzędzia Project Auditor.

Tekstury

Przeanalizuj obsługę urządzeń w grze i wybierz odpowiedni format tekstury. Możesz podzielić pakiety tekstur na urządzenia z wyższej i niższej półki za pomocą Play Asset Delivery, Addressable, lub bardziej ręcznego procesu z AssetBundle.

Postępuj zgodnie z najbardziej znanymi zaleceniami dostępnymi w artykule Optymalizacja wydajności gier mobilnych oraz w poście na forum Optimising Unity Texture Import Settings. Następnie wypróbuj te rozwiązania:

Siatki i modele

Zacznij od sprawdzenia podstawowych ustawień (strona 27) i zweryfikuj te ustawienia importowania siatki:

  • Scal zbędne i mniejsze siatki.
  • Zmniejsz liczbę wierzchołków obiektów w scenach (np. obiektów statycznych lub odległych).
  • Generuj grupy LOD (Level of Detail) dla zasobów o wysokiej geometrii.

Materiały i shadery

  • Usuń nieużywane warianty shaderów programowo podczas procesu kompilacji.
  • Konsoliduj często używane warianty shaderów w uber shadery, aby uniknąć duplikowania shaderów.
  • Włącz dynamiczne wczytywanie shaderów, aby rozwiązać problem z dużym zużyciem pamięci przez wstępnie wczytane shadery w VRAM/RAM. Zwróć jednak uwagę, czy kompilacja shaderów nie powoduje zacinania się klatek.
  • Użyj dynamicznego wczytywania shaderów, aby zapobiec wczytywaniu wszystkich wariantów. Więcej informacji znajdziesz w poście na blogu Improvements to shader build times and memory usage.
  • Prawidłowo używaj instancji materiałów, korzystając z MaterialPropertyBlocks.

Dźwięk

Zacznij od sprawdzenia podstawowych ustawień (strona 41) i zweryfikuj te ustawienia importowania siatki:

  • Usuń nieużywane lub zbędne odwołania do AudioClip podczas korzystania z zewnętrznych silników audio, takich jak FMOD czy Wwise.
  • Wstępnie wczytaj dane audio. Wyłącz wstępne wczytywanie klipów, które nie są od razu potrzebne podczas działania lub uruchamiania sceny. Pomaga to zmniejszyć obciążenie pamięci podczas inicjowania sceny.

Animacje

  • Dostosuj ustawienia kompresji animacji w Unity, aby zminimalizować liczbę klatek kluczowych i wyeliminować zbędne dane.
    • Redukcja klatek kluczowych: automatycznie usuwa niepotrzebne klatki kluczowe.
    • Kompresja kwaternionów: kompresuje dane rotacji, aby zmniejszyć wykorzystanie pamięci.

Ustawienia kompresji możesz dostosować w sekcji Animation Import Settings na karcie Rig lub Animation.

  • Używaj ponownie klipów animacji zamiast duplikować je dla różnych obiektów.

    Użyj kontrolerów zastępowania animatora, aby ponownie użyć kontrolera animatora i zastąpić określone klipy dla różnych postaci.

  • Wypalanie animacji opartych na fizyce: jeśli animacje są oparte na fizyce lub proceduralne, wypal je w klipach animacji, aby uniknąć obliczeń w czasie działania.

  • Optymalizacja szkieletu: używaj mniejszej liczby kości w szkieletach, aby zmniejszyć złożoność i zużycie pamięci.

    • Unikaj nadmiernej liczby kości w przypadku małych lub statycznych obiektów.
    • Jeśli niektóre kości nie są animowane lub potrzebne, usuń je ze szkieletu.
  • Skróć długość klipu animacji.

    • Przytnij klipy animacji, aby zawierały tylko niezbędne klatki. Unikaj przechowywania nieużywanych lub zbyt długich animacji.
    • Używaj animacji w pętli zamiast tworzyć długie klipy do powtarzających się ruchów.
  • Upewnij się, że jest dołączony lub aktywowany tylko 1 komponent animacji. Jeśli używasz Animator, wyłącz lub usuń np. komponenty animacji Legacy animation.

  • Unikaj używania animatora, jeśli nie jest to konieczne. W przypadku prostych efektów VFX użyj bibliotek tweening lub zaimplementuj efekt wizualny w skrypcie. System animatora może być zasobożerny, zwłaszcza na urządzeniach mobilnych z niższej półki.

  • W przypadku obsługi dużej liczby animacji używaj systemu zadań, ponieważ został on całkowicie przeprojektowany, aby był bardziej wydajny pod względem pamięci.

Sceny

Po wczytaniu nowych scen są one wczytywane jako zależności. Jednak bez odpowiedniego zarządzania cyklem życia zasobów, te zależności nie są monitorowane przez liczniki odwołań. W rezultacie zasoby mogą pozostać w pamięci nawet po zwolnieniu nieużywanych scen, co powoduje fragmentację pamięci.

  • Używaj puli obiektów Unity, aby ponownie używać instancji GameObject w przypadku powtarzających się elementów rozgrywki, ponieważ pula obiektów używa stosu do przechowywania kolekcji instancji obiektów do ponownego użycia i nie jest bezpieczna dla wątków. Zminimalizowanie liczby wywołań Instantiate i Destroy poprawia zarówno wydajność procesora, jak i stabilność pamięci.
  • Zwalnianie zasobów:
    • Zwalniaj zasoby strategicznie w mniej krytycznych momentach, np. na ekranach powitalnych lub ekranach wczytywania.
    • Częste używanie Resources.UnloadUnusedAssets powoduje skoki w wykorzystaniu procesora ze względu na duże wewnętrzne operacje monitorowania zależności.
    • Sprawdź, czy w znaczniku profilu GC.MarkDependencies nie występują duże skoki wykorzystania procesora. Usuń lub zmniejsz częstotliwość jego wykonywania i zamiast tego ręcznie zwalniaj określone zasoby za pomocą Resources.UnloadAsset, a nie polegaj na wszechstronnym Resources.UnloadUnusedAssets().
  • Zamiast ciągle używać Resources.UnloadUnusedAssets, zmień strukturę scen.
  • Wywołanie Resources.UnloadUnusedAssets() w przypadku Addressables może spowodować nieumyślne zwolnienie dynamicznie wczytanych pakietów. Starannie zarządzaj cyklem życia dynamicznie wczytywanych zasobów.

Pozostałe postanowienia

  • Fragmentacja spowodowana przejściami między scenami – gdy wywoływana jest metoda Resources.UnloadUnusedAssets() jest wywoływana, Unity wykonuje te czynności:

    • Zwalnia pamięć dla zasobów, które nie są już używane.
    • Uruchamia operację podobną do modułu odśmiecania, aby sprawdzić stertę obiektów zarządzanych i natywnych pod kątem nieużywanych zasobów i je zwalnia.
    • Czyści pamięć tekstur, siatek i zasobów, o ile nie ma aktywnych odwołań.
  • AssetBundle lub Addressable – wprowadzanie zmian w tym obszarze jest złożone i wymaga wspólnego wysiłku zespołu w celu wdrożenia strategii. Gdy jednak opanujesz te strategie, znacznie poprawią one wykorzystanie pamięci, zmniejszą rozmiar pobierania i obniżą koszty chmury. Więcej informacji o zarządzaniu zasobami w Unity za pomocą znajdziesz w tym artykule.Addressables

  • Scentralizowane zależności współdzielone &mdash: grupuj zależności współdzielone, takie jak shadery, tekstury i czcionki, systematycznie w dedykowanych pakietach lub grupach Addressable. Zmniejsza to duplikowanie i zapewnia wydajne zwalnianie niepotrzebnych zasobów.

  • Używaj Addressables do śledzenia zależności – Addressables upraszcza wczytywanie i zwalnianie, a także może automatycznie zwalniać zależności, do których nie ma już odwołań. W zależności od konkretnego przypadku gry przejście na Addressables w celu zarządzania treścią i rozwiązywania zależności może być dobrym rozwiązaniem. Analizuj łańcuchy zależności za pomocą narzędzia Analyze tool, aby zidentyfikować niepotrzebne duplikaty lub zależności. Jeśli używasz AssetBundles, możesz też zapoznać się z narzędziami Unity Data Tools.

  • TypeTrees – jeśli Addressables i AssetBundles w Twojej grze są tworzone i wdrażane przy użyciu tej samej wersji Unity co gracz i nie wymagają zgodności wstecznej z innymi kompilacjami gracza, rozważ wyłączenie zapisu TypeTree, co powinno zmniejszyć rozmiar pakietu i wykorzystanie pamięci przez obiekty plików serializowanych. Zmodyfikuj proces kompilacji w lokalnym ustawieniu pakietu Addressables ContentBuildFlags na DisableWriteTypeTree.

Pisanie kodu przyjaznego dla modułu odśmiecania

Unity używa odśmiecania pamięci (GC) do zarządzania pamięcią przez automatyczne identyfikowanie i zwalnianie nieużywanej pamięci. Chociaż moduł odśmiecania jest niezbędny, może powodować problemy z wydajnością (np. skoki liczby klatek na sekundę), jeśli nie jest prawidłowo obsługiwany, ponieważ ten proces może na chwilę wstrzymać grę, co prowadzi do zacinania się i pogorszenia komfortu użytkowników.

Przydatne techniki zmniejszania częstotliwości przydzielania pamięci na stercie zarządzanej znajdziesz w podręczniku Unity, a przykłady – w UnityPerformanceTuningBible na stronie 271.

  • Zmniejsz przydzielanie pamięci przez moduł odśmiecania:

    • Unikaj LINQ, lambd i domknięć, które przydzielają pamięć na stercie.
    • Używaj StringBuilder do zmiennych ciągów znaków zamiast konkatenacji ciągów.
    • Używaj ponownie kolekcji, wywołując COLLECTIONS.Clear() zamiast tworzyć je ponownie.

    Więcej informacji znajdziesz w e-booku Ultimate Guide to Profiling Unity games.

  • Zarządzaj aktualizacjami płótna interfejsu:

    • Dynamiczne zmiany elementów interfejsu – gdy aktualizowane są elementy interfejsu, takie jak tekst, obraz lub właściwości RectTransform (np. zmiana treści tekstu, zmiana rozmiaru elementów lub animowanie pozycji), silnik może przydzielać pamięć na obiekty tymczasowe.
    • Przydzielanie ciągów znaków – elementy interfejsu, takie jak tekst, często wymagają aktualizacji ciągów znaków, ponieważ w większości języków programowania ciągi znaków są niezmienne.
    • Zanieczyszczone płótno – gdy coś się zmieni na płótnie (np. zmiana rozmiaru, włączanie i wyłączanie elementów lub modyfikowanie właściwości układu), całe płótno lub jego część może zostać oznaczona jako zanieczyszczona i przebudowana. Może to spowodować utworzenie tymczasowych struktur danych (np. danych siatki, buforów wierzchołków lub obliczeń układu), co zwiększa ilość generowanych śmieci.
    • Złożone lub częste aktualizacje – jeśli płótno ma dużą liczbę elementów lub jest często aktualizowane (np. co klatkę), te przebudowy mogą prowadzić do znacznej fragmentacji pamięci.
  • Włącz przyrostowe odśmiecanie, aby zmniejszyć duże skoki kolekcji przez rozłożenie czyszczenia przydzielonej pamięci na kilka klatek. Profiluj, aby sprawdzić, czy ta opcja poprawia wydajność i wykorzystanie pamięci w grze.

  • Jeśli gra wymaga kontrolowanego podejścia, ustaw tryb odśmiecania pamięci na ręczny. Następnie, gdy zmieniasz poziom lub w innym momencie bez aktywnej rozgrywki, wywołaj odśmiecanie pamięci.

  • Wywołuj ręczne odśmiecanie pamięci za pomocą wywołań GC.Collect() w przypadku przejść między stanami gry (np. zmiany poziomu).

  • Optymalizuj tablice, zaczynając od prostych praktyk kodowania, a w razie potrzeby używaj tablic natywnych lub innych kontenerów natywnych w przypadku dużych tablic.

  • Monitoruj obiekty zarządzane za pomocą narzędzi takich jak Unity Memory Profiler, aby śledzić odwołania do obiektów niezarządzanych, które pozostają po zniszczeniu.

    Aby zastosować automatyczne podejście, użyj znacznika profilera, aby przesłać dane do narzędzia do raportowania wydajności.

Unikanie wycieków pamięci i fragmentacji

Wycieki pamięci

Gdy w kodzie C# istnieje odwołanie do obiektu Unity po jego zniszczeniu, zarządzany obiekt otoki, zwany powłoką zarządzaną, pozostaje w pamięci. Pamięć natywna powiązana z odwołaniem jest zwalniana, gdy scena jest zwalniana lub gdy obiekt GameObject, do którego jest dołączona pamięć, lub którykolwiek z jego obiektów nadrzędnych, jest niszczony za pomocą metody Destroy(). Jeśli jednak inne odwołania do sceny lub obiektu GameObject nie zostały wyczyszczone, pamięć zarządzana może pozostać jako wyciekający obiekt powłoki. Więcej informacji o obiektach powłoki zarządzanej znajdziesz w podręczniku Obiekty powłoki zarządzanej.

Wycieki pamięci mogą być też spowodowane subskrypcjami zdarzeń, lambdami i domknięciami, łączeniem ciągów znaków oraz nieprawidłowym zarządzaniem obiektami w puli:

  • Aby zacząć, zobacz Znajdowanie wycieków pamięci, aby prawidłowo porównywać zrzuty pamięci Unity.
  • Sprawdź subskrypcje zdarzeń i wycieki pamięci. Jeśli obiekty subskrybują zdarzenia (np. za pomocą delegatów lub UnityEvents), ale nie anulują subskrypcji przed zniszczeniem, menedżer zdarzeń lub wydawca może zachować odwołania do tych obiektów. Uniemożliwia to odśmiecanie tych obiektów, co prowadzi do wycieków pamięci.
  • Monitoruj zdarzenia klas globalnych lub singletonów, które nie są wyrejestrowane podczas niszczenia obiektów. Na przykład anuluj subskrypcję lub odłącz delegaty w destruktorach obiektów.
  • Upewnij się, że zniszczenie obiektów w puli całkowicie zeruje odwołania do komponentów siatki tekstowej, tekstur i nadrzędnych obiektów GameObject.
  • Pamiętaj, że podczas porównywania zrzutów profilera pamięci Unity i obserwowania a różnicy w zużyciu pamięci bez wyraźnego powodu, różnica może być spowodowana przez sterownik graficzny lub sam system operacyjny.

Fragmentacja pamięci

Fragmentacja pamięci występuje, gdy wiele małych przydziałów jest zwalnianych w losowej kolejności. Przydziały sterty są tworzone sekwencyjnie, co oznacza, że nowe bloki pamięci są tworzone, gdy poprzedni blok się wyczerpie. W rezultacie nowe obiekty nie wypełniają pustych obszarów starych bloków, co prowadzi do fragmentacji. Ponadto duże przydziały tymczasowe mogą powodować trwałą fragmentację przez cały czas trwania sesji gry.

Ten problem jest szczególnie uciążliwy, gdy krótkotrwałe duże przydziały są tworzone w pobliżu długotrwałych.

Grupuj przydziały na podstawie ich czasu życia. Najlepiej, aby długotrwałe przydziały były tworzone razem na początku cyklu życia aplikacji.

Obserwatorzy i menedżerowie zdarzeń

  • Oprócz problemu wspomnianego w sekcji Wycieki pamięci wycieki pamięci mogą z czasem przyczyniać się do fragmentacji, ponieważ nieużywana pamięć jest przydzielana do obiektów, które nie są już używane.
  • Upewnij się, że zniszczenie obiektów w puli całkowicie zeruje odwołania do komponentów siatki tekstowej, tekstur i nadrzędnych GameObjects.
  • Menedżerowie zdarzeń często tworzą i przechowują listy lub słowniki do zarządzania subskrypcjami zdarzeń. Jeśli rosną i kurczą się dynamicznie podczas działania, mogą przyczyniać się do fragmentacji pamięci ze względu na częste przydzielanie i zwalnianie pamięci.

Kod

  • Korutyny czasami przydzielają pamięć, czego można łatwo uniknąć, buforując instrukcję return IEnumerator zamiast deklarować nową za każdym razem.
  • Ciągle monitoruj stany cyklu życia obiektów w puli, aby uniknąć zachowania odwołań do obiektów UnityEngine.Object.

Zasoby

  • Używaj dynamicznych systemów rezerwowych w przypadku gier opartych na tekście, aby uniknąć wstępnego wczytywania wszystkich czcionek w przypadku wielu języków.
  • Grupuj zasoby (np. tekstury i cząsteczki) według typu i oczekiwanego cyklu życia.
  • Kondensuj zasoby z atrybutami cyklu życia w stanie bezczynności, takie jak zbędne obrazy interfejsu i siatki statyczne.

Przydziały oparte na czasie życia

  • Przydzielaj długotrwałe zasoby na początku cyklu życia aplikacji, aby zapewnić kompaktowe przydziały.
  • Używaj NativeCollections lub niestandardowych alokatorów w przypadku struktur danych wymagających dużej ilości pamięci lub tymczasowych (np. klastrów fizycznych).

Plik wykonywalny gry i wtyczki również wpływają na wykorzystanie pamięci.

Metadane IL2CPP

IL2CPP generuje metadane dla każdego typu (np. klas, typów ogólnych i delegatów) podczas kompilacji, które są następnie używane podczas działania do odbicia, sprawdzania typów i innych operacji specyficznych dla działania. Te metadane są przechowywane w pamięci i mogą znacząco wpływać na całkowite wykorzystanie pamięci przez aplikację. Pamięć podręczna metadanych IL2CPP ma znaczący wpływ na czas inicjowania i wczytywania. Ponadto IL2CPP nie deduplikuje niektórych elementów metadanych (np. typów ogólnych lub informacji serializowanych), co może powodować nadmierne wykorzystanie pamięci. Problem ten jest pogłębiany przez powtarzające się lub zbędne użycie typów w projekcie.

Metadane IL2CPP można zmniejszyć przez:

Usuwanie kodu

Oprócz zmniejszenia rozmiaru kompilacji usuwanie kodu zmniejsza też zużycie pamięci. Podczas kompilowania z użyciem backendu skryptowego IL2CPP usuwanie zarządzanego kodu bajtowego (które jest domyślnie aktywowane) usuwa nieużywany kod z zarządzanych zestawów. Proces ten polega na zdefiniowaniu zestawów głównych, a następnie na użyciu statycznej analizy kodu w celu określenia, jakiego innego kodu zarządzanego używają te zestawy główne. Wszelki kod, do którego nie można dotrzeć, jest usuwany. Więcej informacji o usuwaniu kodu zarządzanego znajdziesz w poście na blogu TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS oraz w dokumentacji Usuwanie kodu zarządzanego.

Alokatory natywne

Eksperymentuj z alokatorami pamięci natywnej, aby dostroić alokatory pamięci. Jeśli w grze brakuje pamięci, używaj mniejszych bloków pamięci, nawet jeśli wiąże się to z wolniejszymi alokatorami. Więcej informacji znajdziesz w przykładzie alokatora sterty dynamicznej.

Zarządzanie wtyczkami natywnymi i pakietami SDK

  • Znajdź problematyczną wtyczkę – usuń każdą wtyczkę i porównaj zrzuty pamięci gry. Wymaga to wyłączenia wielu funkcji kodu za pomocą Scripting Define Symbols i refaktoryzacji klas ściśle powiązanych z interfejsami. Aby ułatwić proces wyłączania zależności zewnętrznych bez uniemożliwiania gry, zapoznaj się z artykułem Level up your code with game programming patterns.

  • Skontaktuj się z autorem wtyczki lub pakietu SDK – większość wtyczek nie jest dostępna na licencji open source.

  • Odtwórz wykorzystanie pamięci przez wtyczkę – możesz napisać prostą wtyczkę (użyj tej wtyczki Unity jako odniesienia), która przydziela pamięć. Sprawdź zrzuty pamięci za pomocą Androida Studio (ponieważ Unity nie śledzi tych przydziałów) lub wywołaj klasę MemoryInfo i metodę Runtime.totalMemory() w tym samym projekcie.

Wtyczka Unity przydziela pamięć Java i pamięć natywną. Oto jak to zrobić:

Java

byte[] largeObject = new byte[1024 * 1024 * megaBytes];
list.add(largeObject);

Natywna

char* buffer = new char[megabytes * 1024 * 1024];

// Random data to fill the buffer
for (int i = 1; i < megabytes * 1024 * 1024; ++i) {
   buffer[i] = 'A' + (i % 26); // Fill with letters A-Z
}