Strings compactos

0
3219

Java 6 introdujo un mecanismo para almacenar caracteres ASCII en byte[] en lugar de en char[]. Esta característica se eliminó de nuevo en Java 7. Aún así, volverá en Java 9, pero en esta ocasión, por defecto está habilitada la compresión y se usa siempre byte[].

[box style=»1″]

Éste artículo es una traducción al castellano de la entrada original publicada, en inglés, por Dr. Heinz Kabutz en su número 237 del JavaSpecialists newsletter. Puedes consultar el texto original en Javaspecialists’ Newsletter #237: String compaction

Este artículo se publica en Adictos al Trabajo, con permiso del autor, traducido por David Gómez García, (@dgomezg) consultor tecnológico en Autentia, colaborador de Javaspecialists e instructor certificado para impartir los cursos de Javaspecialists en Español.
[/box]

Strings compactos

Estuve una semana entera instruyendo a un grupo de buenos desarrolladores Java de Provinzial Rheinland Versicherung AG sobre las sutilezas de los patrones de diseño en Java. Os sorprendería saber que mi Patterns Course es, de todos, el más popular. Ni el Advanced concurrency and performance for Java 8 (aunque también está bastante solicitado), ni el curso de introdución a Java (no he impartido ninguno en los últimos años), ni siquiera el Advanced Topics in Java. No, mi humilde curso de patrones de diseño que escribí ya en 2001 es todavía el más demandando en la actualidad.

Normalmente, cuando imparto mi curso de patrones, vemos todo tipo de temas relacionados en Java. De esta forma, los asistentes aprenden mucho más de lo que encontrarían en cualquier libro: pueden ver dónde se utilizan los patrones en la propia JDK; aprenden buenos principios de diseño; conocen las últimas mejoras de Java 8, aunque aún estén anclados en JDK 6 o 7; incluso tocamos un poquito de concurrencia. Este es el curso que, una vez que una empresa ha inscrito a sus programadores, habitualmente siguen enviando a más y más de ellos. Esta es la razón por la que, 15 años después de escribir la primera versión, aún es popular entre las empresas.

En una de esas jornadas, estábamos revisando el patrón Flyweight, que tiene una estructura de clases bastante extraña. No es realmente un patrón de diseño. Sin embargo, como el Facade, es un mal necesario en los patrones de diseño. Me explico. Un buen diseño orientado a objetos produce sistemas altamente configurables y reducen la duplicidad de código. Y esto es bueno, pero también implica a veces bastante trabajo para utilizar el sistema. El patrón Facade hace que un subsistema complejo sea más fácil de utilizar. ¿Por qué es complejo?. Porque normalmente tenemos muchas formas de usarlo, gracias a un uso generoso de los propios patrones de diseño. Flyweight tiene una razón de ser parecida. Normalmente, los buenos diseños en orientación a objetos tienen muchos más objetos (e instancias) que los diseños monolíticos, donde todo es un Singleton. Flyweight trata de reducir el número de objetos, compartiendo las instancias de aquellos que son iguales; lo que es posible si desde fuera no dependemos (o modificamos) el estado interno de los mismos.

Estábamos viendo la fusión de Strings en clase, y cómo el char[] interno en String se reemplaza con un char[] compartido cuando tenemos varios String que contienen el mismo valor. Para ello hay que utilizar G1 como Garbage Collector (-XX:+UseG1GC) y también activar la característica de String deduplication (-XX:+UseStringDeduplication). Funciona muy bien en Java 8. Pero quería comprobar si estaba activo por defecto en Java 9, dado que G1 es su Garbage Collector por defecto. Me sorprendió que mi código ahora produjese un ClassCastException al intentar convertir el campo value de String a char[].

En algún momento con Java 6, aparecieron los String comprimidos. Dicho comportamiento estaba desactivado por defecto, aunque se podía activar con -XX:+UseCompressedStrings. Al activarlo, los Strings que contienen únicamente caracteres ASCII (codificables en 7-bits) pasan automáticamente a tener un byte[] como estructura interna. Con sólo un carácter en el String que necesite más de 7 bits para codificarlo, el String utilizaba de nuevo un char[]. Más curioso es cuando el String contiene caracteres UTF-16, como los del alfabeto Hindi Devanagari, porque en ese caso se crean objetos adicionales y al final tenemos una tasa de creación de objetos aún mayor que sin la compresión de Strings. Pero para el caso de los caracteres US ASCII, es perfecto. Por alguna razón, esta característica de Java 6 se descartó en Java 7 y la opción para activarlo se eliminó definitivamente en Java 8.

Pero con Java 9, aparece la nueva opción -XX:+CompactStrings, activa por defecto. Si examinas internamente la clase String, verás que siempre almacena los caracteres de la cadena en un byte[]. También hay un nuevo campo de tipo byte que almacena el encoding, que de momento puede ser Latin1 (0) o UTF-16 (1). En el futuro podrían añadirse otros valores. Así que, si tus caracteres son Latin1, tu String ocupará menos memoria.

Para probarlo, he escrito un pequeño programa en Java que puede ejecutarse con Java 6, Java 7 y Java 9 para ver las diferencias:

	import java.lang.reflect.*;

	public class StringCompactionTest {
	  private static Field valueField;

	  static {
	    try {
	      valueField = String.class.getDeclaredField("value");
	      valueField.setAccessible(true);
	    } catch (NoSuchFieldException e) {
	      throw new ExceptionInInitializerError(e);
	    }
	  }

	  public static void main(String... args)
	      throws IllegalAccessException {
	    showGoryDetails("hello world");
	    showGoryDetails("hello w\u00f8rld"); // Scandinavian o
	    showGoryDetails("he\u03bb\u03bbo wor\u03bbd"); // Greek l
	  }

	  private static void showGoryDetails(String s)
	      throws IllegalAccessException {
	    s = "" + s;
	    System.out.printf("Details of String \"%s\"\n", s);
	    System.out.printf("Identity Hash of String: 0x%x%n",
	        System.identityHashCode(s));
	    Object value = valueField.get(s);
	    System.out.println("Type of value field: " +
	        value.getClass().getSimpleName());
	    System.out.println("Length of value field: " +
	        Array.getLength(value));
	    System.out.printf("Identity Hash of value: 0x%x%n",
	        System.identityHashCode(value));
	    System.out.println();
	  }
	}

Esta es la primera ejecución, con Java 6 y el flag -XX:-UseCompressedStrings (por defecto). Observa como cada uno de los Strings contiene un char[].

	Java6 no compaction
	java version "1.6.0_65"

	Details of String "hello world"
	Identity Hash of String: 0x7b1ddcde
	Type of value field: char[]
	Length of value field: 11
	Identity Hash of value: 0x6c6e70c7

	Details of String "hello wørld"
	Identity Hash of String: 0x46ae506e
	Type of value field: char[]
	Length of value field: 11
	Identity Hash of value: 0x5e228a02

	Details of String "heλλo worλd"
	Identity Hash of String: 0x2d92b996
	Type of value field: char[]
	Length of value field: 11
	Identity Hash of value: 0x7bd63e39

La segunda vez, lo ejecutamos con Java 6 y -XX:+UseCompressedStrings. La cadena «hello world» contiene un byte[] y las otras dos un char[]. Sólo se comprimen los caracteres US ASCII (de 7 bits).

Java6 compaction
java version "1.6.0_65"

Details of String "hello world"
Identity Hash of String: 0x46ae506e
Type of value field: byte[]
Length of value field: 11
Identity Hash of value: 0x7bd63e39

Details of String "hello wørld"
Identity Hash of String: 0x42b988a6
Type of value field: char[]
Length of value field: 11
Identity Hash of value: 0x22ba6c83

Details of String "heλλo worλd"
Identity Hash of String: 0x7d2a1e44
Type of value field: char[]
Length of value field: 11
Identity Hash of value: 0x5829428e

En Java 7 el flag se ignora. En Java 8 se eliminó, por tanto, la JVM con -XX:+UseCompressedStrings no arrancará. Por supuesto, todos los Strings contienen un char[].

Java7 compaction
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option
    UseCompressedStrings; support was removed in 7.0
java version "1.7.0_80"

Details of String "hello world"
Identity Hash of String: 0xa89848d
Type of value field: char[]
Length of value field: 11
Identity Hash of value: 0x57fd54c4

Details of String "hello wørld"
Identity Hash of String: 0x38c83cfd
Type of value field: char[]
Length of value field: 11
Identity Hash of value: 0x621c232a

Details of String "heλλo worλd"
Identity Hash of String: 0x2548ccb8
Type of value field: char[]
Length of value field: 11
Identity Hash of value: 0x4e785727

En Java 9 tenemos el nuevo flag -XX:+CompactStrings, activo por defecto. Los Strings ahora almacenan siempre su carga en un byte[], independientenmente del encoding. Por ejemplo, vemos que para Latin1, todos los bytes están comprimidos.

	Java9 compaction
	java version "9-ea"

	Details of String "hello world"
	Identity Hash of String: 0x77f03bb1
	Type of value field: byte[]
	Length of value field: 11
	Identity Hash of value: 0x7a92922

	Details of String "hello wørld"
	Identity Hash of String: 0x71f2a7d5
	Type of value field: byte[]
	Length of value field: 11
	Identity Hash of value: 0x2cfb4a64

	Details of String "heλλo worλd"
	Identity Hash of String: 0x5474c6c
	Type of value field: byte[]
	Length of value field: 22
	Identity Hash of value: 0x4b6995df

Naturalmente, podemos desactivar esta característica de Java 9 con -XX:-CompactStrings. Sin embargo, la estructura de String ha cambiado así que, independientemente de lo que hagas, el campo value sigue siendo un byte[].

	Java9 no compaction
	java version "9-ea"

	Details of String "hello world"
	Identity Hash of String: 0x21a06946
	Type of value field: byte[]
	Length of value field: 22
	Identity Hash of value: 0x25618e91

	Details of String "hello wørld"
	Identity Hash of String: 0x7a92922
	Type of value field: byte[]
	Length of value field: 22
	Identity Hash of value: 0x71f2a7d5

	Details of String "heλλo worλd"
	Identity Hash of String: 0x2cfb4a64
	Type of value field: byte[]
	Length of value field: 22
	Identity Hash of value: 0x5474c6c

Cualquiera que utilice introspección para acceder a las tripas de String, podría obtener ahora un ClassCastException. Esperemos que la cantidad de esos programadores sea infinitamente pequeño.

Más preocupante es el rendimiento. Métodos como String.charAt(int) solían ser rápidos como la pólvora. Puedo notar una ralentización en Java 9. Si recorres habitualmente las cadenas con charAt(), será mejor que evalues algunas alternativas, ¡aunque estoy seguro de cuáles!. O quizás lo arreglen en la release final de Java 9, al fin y al cabo estoy trabajando con una Early Release (EA).

Me contaron un truquito de Peter Lawrey en una de las JCrete Unconferences: String tiene un constructor que recibe como parámetros un char[] y un boolean. El parámetro boolean no se utiliza nunca y se supone que debes pasarlo como true, indicando que el char[] se utilizará directamente como value y no se copiará. Este es el código:

String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

El truco de Lawrey consiste en crear Strings de forma muy rápida desde un char[] utilizando directamente este constructor. No estoy seguro de los detalles, pero lo más probable es que utilice JavaLangAccess que nos proporciona la clase SharedSecrets. Antes de Java 9, esta clase estaba en el paquete sun.misc.package. Desde Java 9, está en jdk.internal.misc. Espero que no estés utilizando éste constructor directamente, porque tendrás que cambiar tu código cuando actualices a Java 9.

Aquí tienes el código. Tendrás que cambiar los imports en función de la versión de Java que uses

	//import sun.misc.*; // prior to Java 9, use this
import jdk.internal.misc.*; // since Java 9, use this instead

public class StringUnsafeTest {
  private static String s;

  public static void main(String... args) {
    char[] chars = "hello world".toCharArray();
    JavaLangAccess javaLang = SharedSecrets.getJavaLangAccess();
    long time = System.currentTimeMillis();
    for (int i = 0; i < 100 * 1000 * 1000; i++) {
      s = javaLang.newStringUnsafe(chars);
    }
    time = System.currentTimeMillis() - time;
    System.out.println("time = " + time);
  }
}

En resumen, si hablas inglés, alemán, francés o español, tus Strings son ahora mucho más ligeros. Para los griegos y los chinos, siguen siendo lo mismo. Para todos, seguramente resultarán un poco más lentos.

Saludos desde el aeropuerto de Tesalónica.

Heinz.

Heinz es el creador de "The Java Specialists' Newsletter".
Doctor en Informática, Heinz ha participado en el desarrollo de grandes aplicaciones en Java, ha formado a miles de desarrolladores profesionales y es ponente habitual en las principales conferencias sobre Java.
Reconocido como Java Champion por Sun Microsystems -los creadores de Java- por su trabajo en mejorar Java, Heinz imparte cursos de JavaSpecialists.eu por todo el mundo, de forma remota y presencial. Es autor de dichos cursos, incluidos 'Java Specialists Master', 'DesignPatterns and Concurrency Specialists' y 'Performance and Concurrency for Java 8'.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad