¿Por qué agregar memoria "" a una cadena ahorra memoria?

193

Usé una variable con muchos datos, digamos String data. Quería usar una pequeña parte de esta cadena de la siguiente manera:

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

Después de algunas horas de depuración (con un visualizador de memoria) descubrí que el campo de objetos smallpartrecordaba todos los datos data, aunque solo contenía la subcadena.

Cuando cambié el código a:

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

..¡El problema fue resuelto! ¡Ahora mi aplicación usa muy poca memoria ahora!

¿Cómo es eso posible? ¿Alguien puede explicar esto? Creo que this.smallpart siguió haciendo referencia a los datos, pero ¿por qué?

ACTUALIZACIÓN: ¿Cómo puedo borrar la cadena grande entonces? ¿Data = new String (data.substring (0,100)) hará lo mismo?

hsmit
fuente
Lea más sobre su intención final a continuación: ¿De dónde viene la cuerda grande en primer lugar? Si se lee desde un archivo o base de datos CLOB o algo así, solo leer lo que necesita mientras se analiza será óptimo en todos los sentidos.
PSpeed
44
Increíble ... Estoy trabajando en Java por más de 4 a 5 años, aún así esto es nuevo para mí :). Gracias por la información hermano.
Parth
1
Hay una sutileza en el uso new String(String); ver stackoverflow.com/a/390854/8946 .
Lawrence Dol el

Respuestas:

159

Haciendo lo siguiente:

data.substring(x, y) + ""

crea un nuevo objeto String (más pequeño) y descarta la referencia a la cadena creada por substring (), lo que permite la recolección de basura de este.

Lo importante a tener en cuenta es que substring()proporciona una ventana a una Cadena existente , o más bien, la matriz de caracteres subyacente a la Cadena original. Por lo tanto, consumirá la misma memoria que la cadena original. Esto puede ser ventajoso en algunas circunstancias, pero problemático si desea obtener una subcadena y deshacerse de la cadena original (como ha descubierto).

Eche un vistazo al método substring () en la fuente JDK String para obtener más información.

EDITAR: Para responder a su pregunta complementaria, la construcción de una nueva Cadena a partir de la subcadena reducirá el consumo de memoria, siempre y cuando haya cualquier referencia a la Cadena original.

NOTA (enero de 2013). El comportamiento anterior ha cambiado en Java 7u6 . El patrón de peso mosca ya no se usa y substring()funcionará como es de esperar.

Brian Agnew
fuente
89
Ese es uno de los pocos casos en los que el String(String)constructor (es decir, el constructor de cadenas que toma una cadena como entrada) es útil: new String(data.substring(x, y))efectivamente hace lo mismo que anexar "", pero aclara un poco la intención.
Joachim Sauer
3
solo para precisar, la subcadena utiliza el valueatributo de la cadena original. Creo que por eso se mantiene la referencia.
Valentin Rocher
@Bishiboosh: sí, es cierto. No quería exponer las particularidades de la implementación, pero eso es precisamente lo que está sucediendo.
Brian Agnew
55
Técnicamente es un detalle de implementación. Sin embargo, es frustrante y atrapa a mucha gente.
Brian Agnew
1
Me pregunto si es posible optimizar esto en el JDK utilizando referencias débiles o similares. Si soy la última persona que necesita este char [], y solo necesito un poco, cree una nueva matriz para que la use internamente.
WW.
28

Si observa la fuente de substring(int, int), verá que devuelve:

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

donde valuees el original char[]. Entonces obtienes una nueva cadena pero con el mismo subyacente char[].

Cuando lo haces, data.substring() + ""obtienes una nueva cadena con un nuevo subyacente char[].

En realidad, su caso de uso es la única situación en la que debe usar el String(String)constructor:

String tiny = new String(huge.substring(12,18));
Pascal Thivent
fuente
1
Hay una sutileza en el uso new String(String); ver stackoverflow.com/a/390854/8946 .
Lawrence Dol
17

Cuando lo usas substring, en realidad no crea una nueva cadena. Todavía se refiere a su cadena original, con una restricción de desplazamiento y tamaño.

Por lo tanto, para permitir que se recopile su cadena original, debe crear una nueva cadena (usando new String, o lo que tiene).

Chris Jester-Young
fuente
5

Creo que this.smallpart siguió haciendo referencia a los datos, pero ¿por qué?

Como las cadenas de Java consisten en una matriz de caracteres, un desplazamiento inicial y una longitud (y un código hash en caché). Algunas operaciones de cadena comosubstring() crear un nuevo objeto de cadena que comparte la matriz de caracteres del original y simplemente tiene diferentes campos de desplazamiento y / o longitud. Esto funciona porque la matriz de caracteres de una cadena nunca se modifica una vez que se ha creado.

Esto puede ahorrar memoria cuando muchas subcadenas se refieren a la misma cadena básica sin replicar partes superpuestas. Como ha notado, en algunas situaciones, puede evitar que se recopilen datos que ya no se necesitan.

La forma "correcta" de arreglar esto es el new String(String)constructor, es decir

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

Por cierto, la mejor solución general sería evitar tener cadenas muy grandes en primer lugar y procesar cualquier entrada en fragmentos más pequeños, unos pocos KB a la vez.

Michael Borgwardt
fuente
Hay una sutileza en el uso new String(String); ver stackoverflow.com/a/390854/8946 .
Lawrence Dol
5

En Java, las cadenas son objetos inmutables y una vez que se crea una cadena, permanece en la memoria hasta que el recolector de basura la limpia (y esta limpieza no es algo que pueda dar por sentado).

Cuando llama al método de subcadena, Java no crea una cadena realmente nueva, sino que simplemente almacena un rango de caracteres dentro de la cadena original.

Entonces, cuando creaste una nueva cadena con este código:

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

en realidad creó una nueva cadena cuando concatenó el resultado con la cadena vacía. Es por eso.

Kico Lobo
fuente
3

Como documentado por jwz en 1997 :

Si tiene una cadena enorme, extraiga una subcadena () de ella, sostenga la subcadena y permita que la cadena más larga se convierta en basura (en otras palabras, la subcadena tiene una vida útil más larga), los bytes subyacentes de la cadena enorme nunca se van lejos.

Conocido
fuente
2

En resumen, si crea muchas subcadenas a partir de una pequeña cantidad de cadenas grandes, use

   String subtring = string.substring(5,23)

Como solo usa el espacio para almacenar las cadenas grandes, pero si está extrayendo un puñado de cadenas pequeñas, de las pérdidas de cadenas grandes, entonces

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

Mantendrá su uso de memoria bajo, ya que las cadenas grandes se pueden recuperar cuando ya no se necesitan.

Que llame new Stringes un recordatorio útil de que realmente está obteniendo una nueva cadena, en lugar de una referencia a la original.

mdma
fuente
Hay una sutileza en el uso new String(String); ver stackoverflow.com/a/390854/8946 .
Lawrence Dol el
2

En primer lugar, la llamada java.lang.String.substringcrea una nueva ventana en el originalString con el uso del desplazamiento y la longitud en lugar de copiar la parte significativa de la matriz subyacente.

Si echamos un vistazo más de cerca al substringmétodo, notaremos una llamada al constructor de cadenasString(int, int, char[]) y la pasamos entera char[]que representa la cadena . Eso significa que la subcadena ocupará tanta cantidad de memoria como la cadena original .

Ok, pero ¿por qué + ""resulta en demanda de menos memoria que sin ella?

Hacer un +encendido stringsse implementa a través de una StringBuilder.appendllamada al método. Mire la implementación de este método en AbstractStringBuilderclase nos dirá que finalmente lo hará arraycopycon la parte que realmente necesitamos (la substring).

¿Alguna otra solución?

this.smallpart = new String(data.substring(12,18));
this.smallpart = data.substring(12,18).intern();
laika
fuente
0

Agregar "" a una cadena a veces ahorrará memoria.

Digamos que tengo una gran cadena que contiene un libro completo, un millón de caracteres.

Luego creo 20 cadenas que contienen los capítulos del libro como subcadenas.

Luego creo 1000 cadenas que contienen todos los párrafos.

Luego creo 10.000 cadenas que contienen todas las oraciones.

Luego creo 100,000 cadenas que contienen todas las palabras.

Todavía solo uso 1,000,000 de caracteres. Si agrega "" a cada capítulo, párrafo, oración y palabra, utilizará 5,000,000 de caracteres.

Por supuesto, es completamente diferente si solo extrae una sola palabra de todo el libro, y todo el libro podría ser basura recolectada, pero no es porque esa palabra tenga una referencia a ella.

Y nuevamente es diferente si tiene una cadena de un millón de caracteres y elimina pestañas y espacios en ambos extremos, haciendo 10 llamadas para crear una subcadena. La forma en que funciona o funciona Java evita copiar un millón de caracteres cada vez. Hay compromiso, y es bueno si sabes cuáles son los compromisos.

gnasher729
fuente