Dlaczego dołączenie „” do String oszczędza pamięć?

193

Powiedzmy, że użyłem zmiennej z dużą ilością danych String data. Chciałem użyć niewielkiej części tego ciągu w następujący sposób:

this.smallpart = data.substring(12,18);

Po kilku godzinach debugowania (za pomocą wizualizatora pamięci) dowiedziałem się, że pole obiektów smallpartzapamiętało wszystkie dane data, chociaż zawierało tylko podłańcuch.

Kiedy zmieniłem kod na:

this.smallpart = data.substring(12,18)+""; 

..problem został rozwiązany! Teraz moja aplikacja zużywa teraz bardzo mało pamięci!

Jak to możliwe? Czy ktoś może to wyjaśnić? Wydaje mi się, że to. Mała część ciągle odnosiła się do danych, ale dlaczego?

AKTUALIZACJA: Jak mogę zatem wyczyścić duży Ciąg? Czy dane = nowy ciąg (data.substring (0,100)) to zrobi?

hsmit
źródło
Przeczytaj więcej o swoich ostatecznych zamiarach poniżej: skąd bierze się przede wszystkim duży sznurek? Jeśli odczytujesz plik CLOB z pliku lub bazy danych, czy coś, to optymalne jest tylko czytanie tego, czego potrzebujesz podczas analizowania.
PSpeed
4
Niesamowite ... Pracuję w Javie od ponad 4 do 5 lat, ale dla mnie to nowość :). dzięki za info bracie.
Parth
1
Korzystanie z subtelności new String(String); patrz stackoverflow.com/a/390854/8946 .
Lawrence Dol

Odpowiedzi:

159

Wykonując następujące czynności:

data.substring(x, y) + ""

tworzy nowy (mniejszy) obiekt String i odrzuca odwołanie do obiektu String utworzonego przez substring (), umożliwiając w ten sposób wyrzucanie elementów bezużytecznych.

Ważne jest, aby zdawać sobie sprawę, że substring()daje okno na istniejący Ciąg - a raczej tablicę znaków leżącą u podstaw oryginalnego Ciągu. Dlatego zajmie tę samą pamięć co oryginalny ciąg. Może to być korzystne w niektórych okolicznościach, ale problematyczne, jeśli chcesz uzyskać podciąg i pozbyć się oryginalnego ciągu (jak się dowiedziałeś).

Spójrz na metodę substring () w źródle String JDK, aby uzyskać więcej informacji.

EDYCJA: Aby odpowiedzieć na dodatkowe pytanie, zbudowanie nowego ciągu znaków z podciągu zmniejszy zużycie pamięci, pod warunkiem, że bin będzie odwoływał się do oryginalnego ciągu.

UWAGA (styczeń 2013). Powyższe zachowanie zmieniło się w Javie 7u6 . Wzór flyweight nie jest już używany i substring()będzie działał zgodnie z oczekiwaniami.

Brian Agnew
źródło
89
Jest to jeden z niewielu przypadków, w których String(String)konstruktor (tj. Konstruktor String przyjmujący Ciąg jako dane wejściowe) jest użyteczny: new String(data.substring(x, y))robi to samo, co dołączanie "", ale czyni intencję nieco jaśniejszą.
Joachim Sauer
3
dokładnie, podciąg wykorzystuje valueatrybut oryginalnego łańcucha. Myślę, że właśnie dlatego referencja jest przechowywana.
Valentin Rocher
@Bishiboosh - tak, zgadza się. Nie chciałem ujawniać szczegółów wdrożenia, ale właśnie to się dzieje.
Brian Agnew
5
Technicznie jest to szczegół implementacji. Jest to jednak frustrujące i łapie wielu ludzi.
Brian Agnew
1
Zastanawiam się, czy można zoptymalizować to w JDK przy użyciu słabych referencji lub tym podobnych. Jeśli jestem ostatnią osobą, która potrzebuje tego char [], a potrzebuję tylko trochę, stwórz nową tablicę do wewnętrznego użytku.
WW.
28

Jeśli spojrzysz na źródło substring(int, int), zobaczysz, że zwraca:

new String(offset + beginIndex, endIndex - beginIndex, value);

gdzie valuejest oryginał char[]. Otrzymujesz nowy ciąg znaków, ale z tym samym instrumentem bazowym char[].

Kiedy to zrobisz, data.substring() + ""otrzymasz nowy Ciąg z nowym instrumentem bazowym char[].

W rzeczywistości twój przypadek użycia jest jedyną sytuacją, w której powinieneś użyć String(String)konstruktora:

String tiny = new String(huge.substring(12,18));
Pascal Thivent
źródło
1
Korzystanie z subtelności new String(String); patrz stackoverflow.com/a/390854/8946 .
Lawrence Dol
17

Kiedy używasz substring, tak naprawdę nie tworzy nowego ciągu. Nadal odnosi się do twojego oryginalnego ciągu, z ograniczeniem przesunięcia i rozmiaru.

Tak więc, aby umożliwić zbieranie oryginalnego ciągu, musisz utworzyć nowy ciąg (używając new Stringlub tego, co masz).

Chris Jester-Young
źródło
5

Wydaje mi się, że to. Mała część ciągle odnosiła się do danych, ale dlaczego?

Ponieważ łańcuchy Java składają się z tablicy char, przesunięcia początkowego i długości (oraz buforowanego kodu skrótu). Niektóre operacje na łańcuchach, takie jak substring()tworzenie nowego obiektu String, który dzieli tablicę znaków oryginału i po prostu ma inne pola przesunięcia i / lub długości. Działa to, ponieważ tablica znaków ciągu nie jest nigdy modyfikowana po utworzeniu.

Może to zaoszczędzić pamięć, gdy wiele podciągów odnosi się do tego samego ciągu podstawowego bez replikacji nakładających się części. Jak zauważyłeś, w niektórych sytuacjach może to uniemożliwić zbieranie niepotrzebnych danych.

„Poprawnym” sposobem naprawienia tego jest new String(String)konstruktor, tj

this.smallpart = new String(data.substring(12,18));

BTW, ogólnie najlepszym rozwiązaniem byłoby uniknięcie posiadania bardzo dużych Ciągów w pierwszej kolejności i przetwarzanie wszelkich danych wejściowych w mniejszych porcjach, po kilka KB na raz.

Michael Borgwardt
źródło
Korzystanie z subtelności new String(String); patrz stackoverflow.com/a/390854/8946 .
Lawrence Dol
5

W Javie ciągi są przypisywalnymi obiektami, a po utworzeniu ciąg pozostaje w pamięci, dopóki nie zostanie wyczyszczony przez moduł wyrzucający elementy bezużyteczne (a tego czyszczenia nie można brać za pewnik).

Po wywołaniu metody substring Java nie tworzy prawdziwie nowego ciągu, ale po prostu przechowuje zakres znaków w oryginalnym ciągu.

Kiedy więc utworzyłeś nowy ciąg z tym kodem:

this.smallpart = data.substring(12, 18) + ""; 

faktycznie utworzyłeś nowy ciąg, łącząc wynik z pustym ciągiem. Dlatego.

Kico Lobo
źródło
3

Jak udokumentował jwz w 1997 roku :

Jeśli masz ogromny ciąg, wyciągnij podłańcuch (), trzymaj się podłańcucha i pozwól, aby dłuższy ciąg stał się śmieciem (innymi słowy, podciąg ma dłuższą żywotność), bajty dużego łańcucha nigdy nie idą z dala.

Rozpoznać
źródło
2

Podsumowując, jeśli tworzysz wiele podciągów z niewielkiej liczby dużych ciągów, użyj

   String subtring = string.substring(5,23)

Ponieważ używasz tylko miejsca do przechowywania dużych ciągów, ale jeśli wyciągasz garść małych ciągów, z utraconych dużych ciągów, to

   String substring = new String(string.substring(5,23));

Zmniejszy zużycie pamięci, ponieważ duże łańcuchy można odzyskać, gdy nie są już potrzebne.

To, co wywołujesz, new Stringjest przydatnym przypomnieniem, że naprawdę otrzymujesz nowy ciąg, a nie odniesienie do oryginalnego.

MDMA
źródło
Korzystanie z subtelności new String(String); patrz stackoverflow.com/a/390854/8946 .
Lawrence Dol
2

Po pierwsze, wywołanie java.lang.String.substringtworzy nowe okno oryginałuString z wykorzystaniem przesunięcia i długości zamiast kopiowania znacznej części podstawowej tablicy.

Jeśli przyjrzymy się bliżej substringmetodzie, zauważymy wywołanie konstruktora łańcuchaString(int, int, char[]) i przekazanie go w całości, char[]który reprezentuje łańcuch . Oznacza to, że podciąg zajmie tyle samo pamięci, co oryginalny ciąg .

Ok, ale dlaczego + ""powoduje zapotrzebowanie na mniej pamięci niż bez niej?

Wykonanie +włączenia stringsjest realizowane za StringBuilder.appendpomocą wywołania metody. Spójrz na implementację tej metody w AbstractStringBuilderklasie powie nam, że w końcu robi to arraycopyz częścią, której naprawdę potrzebujemy (substring ).

Wszelkie inne obejście?

this.smallpart = new String(data.substring(12,18));
this.smallpart = data.substring(12,18).intern();
laika
źródło
0

Czasami dołącza się ciąg „” oszczędzać pamięć.

Powiedzmy, że mam ogromny ciąg zawierający całą książkę, milion znaków.

Następnie tworzę 20 ciągów znaków zawierających rozdziały książki jako podciągi.

Następnie tworzę 1000 ciągów zawierających wszystkie akapity.

Następnie tworzę 10 000 ciągów zawierających wszystkie zdania.

Następnie tworzę 100 000 ciągów zawierających wszystkie słowa.

Nadal używam tylko 1 000 000 znaków. Jeśli dodasz „” do każdego rozdziału, akapitu, zdania i słowa, użyjesz 5 000 000 znaków.

Oczywiście jest zupełnie inaczej, jeśli wyodrębnisz tylko jedno słowo z całej książki, a cała książka może zostać wyrzucona do śmieci, ale nie dlatego, że to jedno słowo zawiera odniesienie do niej.

I znowu jest inaczej, jeśli masz milion znaków i usuwasz tabulatory i spacje na obu końcach, wykonując powiedzmy 10 wywołań w celu utworzenia podłańcucha. Sposób, w jaki działa lub działa Java, pozwala uniknąć kopiowania miliona znaków za każdym razem. Jest kompromis i dobrze, jeśli wiesz, jakie są kompromisy.

gnasher729
źródło