web-dev-qa-db-fra.com

Java: ByteArrayOutputStream efficace en mémoire

J'ai un fichier de 40 Mo sur le disque et je dois le "mapper" en mémoire à l'aide d'un tableau d'octets.

Au début, je pensais que l'écriture du fichier sur un ByteArrayOutputStream serait la meilleure solution, mais je trouve qu'il faut environ 160 Mo d'espace disque à un moment donné pendant l'opération de copie.

Est-ce que quelqu'un connaît un meilleur moyen de faire cela sans utiliser trois fois la taille du fichier de RAM?

Mise à jour: / Merci pour vos réponses. J'ai remarqué que je pouvais réduire un peu la consommation de mémoire en indiquant à la taille initiale de ByteArrayOutputStream d'être un peu supérieure à la taille du fichier d'origine (utiliser la taille exacte avec la réallocation forcée de mon code, je dois vérifier pourquoi).

Il existe un autre point de mémoire importante: lorsque je récupère byte [] avec ByteArrayOutputStream.toByteArray. En regardant son code source, je peux voir qu’il clone le tableau:

public synchronized byte toByteArray()[] {
    return Arrays.copyOf(buf, count);
}

Je pense que je pourrais simplement étendre ByteArrayOutputStream et réécrire cette méthode afin de renvoyer directement le tableau d'origine. Y a-t-il un danger potentiel ici, étant donné que le flux et le tableau d'octets ne seront pas utilisés plus d'une fois?

15
user683887

MappedByteBuffer pourrait être ce que vous cherchez. 

Je suis surpris qu'il soit si difficile de lire un fichier en mémoire RAM. Avez-vous construit la ByteArrayOutputStream avec une capacité appropriée? Sinon, le flux pourrait allouer un nouveau tableau d'octets vers la fin des 40 Mo, ce qui signifie que vous disposeriez par exemple d'un tampon complet de 39 Mo et d'un nouveau tampon deux fois plus volumineux. Alors que si le flux a la capacité appropriée, il n’y aura pas de réallocation (plus rapide) ni de mémoire perdue.

13
JB Nizet

ByteArrayOutputStream devrait suffire tant que vous spécifiez une taille appropriée dans le constructeur. Il en créera quand même une copie lorsque vous appelez toByteArray, mais ce n'est que temporaire . Souhaitez-vous vraiment que la mémoire brièvement monte beaucoup?

Si vous connaissez déjà la taille de départ, vous pouvez simplement créer un tableau d'octets et lire de manière répétée une FileInputStream dans ce tampon jusqu'à ce que vous ayez toutes les données.

10
Jon Skeet

Si vous voulez vraiment mapper le fichier en mémoire, alors FileChannel est le mécanisme approprié.

Si tout ce que vous voulez faire, c'est lire le fichier dans un simple byte[] (sans avoir besoin que les modifications apportées à ce tableau soient reflétées dans le fichier), puis simplement dans un byte[] de taille appropriée à partir d'un FileInputStream normal devrait suffire.

Guava a Files.toByteArray() qui fait tout cela pour vous.

5
Joachim Sauer

Pour une explication du comportement de croissance du tampon de ByteArrayOutputStream, veuillez lire cette réponse .

En réponse à votre question, il est / pour étendre ByteArrayOutputStream. Dans votre cas, il est probablement préférable de remplacer les méthodes d'écriture de telle sorte que l'allocation supplémentaire maximale soit limitée, par exemple, à 16 Mo. Vous ne devez pas remplacer la toByteArray pour exposer le membre protégé buf []. En effet, un flux n'est pas un tampon; Un flux est un tampon doté d'un pointeur de position et d'une protection de limite. Il est donc dangereux d’accéder au tampon et de le manipuler de l’extérieur.

3
Derek Bennett

... mais je trouve qu'il faut environ 160 Mo d'espace disque à un moment donné pendant l'opération de copie

Je trouve cela extrêmement surprenant ... dans la mesure où j'ai des doutes sur le fait que vous mesurez correctement l'utilisation du tas.

Supposons que votre code ressemble à ceci:

BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("somefile"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();  /* no hint !! */

int b;
while ((b = bis.read()) != -1) {
    baos.write((byte) b);
}
byte[] stuff = baos.toByteArray();

Maintenant, la manière dont un ByteArrayOutputStream gère son tampon consiste à allouer une taille initiale et à (au moins) doubler le tampon quand il le remplit. Ainsi, dans le pire des cas, baos pourrait utiliser jusqu'à 80 Mo de mémoire tampon pour contenir un fichier de 40 Mo.

La dernière étape alloue un nouveau tableau d'octets exactement baos.size() pour contenir le contenu du tampon. C'est 40Mb. Ainsi, la quantité maximale de mémoire réellement utilisée devrait être de 120 Mo.

Alors, où sont ces 40 Mo supplémentaires utilisés? Je suppose qu'ils ne le sont pas et que vous signalez en réalité la taille totale du segment de mémoire, et non la quantité de mémoire occupée par des objets accessibles.


Donc, quelle est la solution?

  1. Vous pouvez utiliser un tampon mappé en mémoire.

  2. Vous pouvez donner un indice de taille lorsque vous affectez la variable ByteArrayOutputStream; par exemple.

     ByteArrayOutputStream baos = ByteArrayOutputStream(file.size());
    
  3. Vous pouvez vous passer entièrement de ByteArrayOutputStream et lire directement dans un tableau d'octets.

     byte[] buffer = new byte[file.size()];
     FileInputStream fis = new FileInputStream(file);
     int nosRead = fis.read(buffer);
     /* check that nosRead == buffer.length and repeat if necessary */
    

Les deux options 1 et 2 devraient avoir une utilisation de mémoire maximale de 40 Mo lors de la lecture d'un fichier de 40 Mo; c'est-à-dire pas d'espace perdu.


Il serait utile que vous publiiez votre code et décriviez votre méthodologie pour mesurer l'utilisation de la mémoire.


Je pense que je pourrais simplement étendre ByteArrayOutputStream et réécrire cette méthode afin de renvoyer directement le tableau d'origine. Y a-t-il un danger potentiel ici, étant donné que le flux et le tableau d'octets ne seront pas utilisés plus d'une fois?

Le danger potentiel est que vos hypothèses soient incorrectes, ou deviennent incorrectes du fait que quelqu'un d'autre modifie votre code sans le vouloir ...

2
Stephen C

Je pense que je pourrais simplement étendre ByteArrayOutputStream et réécrire cette méthode afin de renvoyer directement le tableau d'origine. Y a-t-il un danger potentiel ici, étant donné que le flux et le tableau d'octets ne seront pas utilisés plus d'une fois?

Vous ne devriez pas changer le comportement spécifié de la méthode existante, mais il est parfaitement correct d'ajouter une nouvelle méthode. Voici une implémentation:

/** Subclasses ByteArrayOutputStream to give access to the internal raw buffer. */
public class ByteArrayOutputStream2 extends Java.io.ByteArrayOutputStream {
    public ByteArrayOutputStream2() { super(); }
    public ByteArrayOutputStream2(int size) { super(size); }

    /** Returns the internal buffer of this ByteArrayOutputStream, without copying. */
    public synchronized byte[] buf() {
        return this.buf;
    }
}

Une autre façon, mais astucieuse, d'obtenir le tampon de any ByteArrayOutputStream consiste à utiliser le fait que sa méthode writeTo(OutputStream) passe le tampon directement au OutputStream fourni:

/**
 * Returns the internal raw buffer of a ByteArrayOutputStream, without copying.
 */
public static byte[] getBuffer(ByteArrayOutputStream bout) {
    final byte[][] result = new byte[1][];
    try {
        bout.writeTo(new OutputStream() {
            @Override
            public void write(byte[] buf, int offset, int length) {
                result[0] = buf;
            }

            @Override
            public void write(int b) {}
        });
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    return result[0];
}

(Cela fonctionne, mais je ne suis pas sûr que ce soit utile, étant donné que ByteArrayOutputStream est un sous-classement plus simple.)

Cependant, dans le reste de votre question, il semble que tout ce que vous voulez est un byte[] complet du contenu complet du fichier. A partir de Java 7, le moyen le plus simple et le plus rapide de procéder consiste à appeler Files.readAllBytes . En Java 6 et inférieur, vous pouvez utiliser DataInputStream.readFully, comme dans Réponse de Peter Lawrey . Dans les deux cas, vous obtiendrez un tableau alloué une fois à la taille correcte, sans la réallocation répétée de ByteArrayOutputStream.

2
Boann

Google Guava ByteSource semble être un bon choix pour la mise en mémoire tampon. Contrairement aux implémentations telles que ByteArrayOutputStream ou ByteArrayList (de Colt Library), elle ne fusionne pas les données dans un immense tableau d'octets, mais stocke chaque bloc séparément. Un exemple:

List<ByteSource> result = new ArrayList<>();
try (InputStream source = httpRequest.getInputStream()) {
    byte[] cbuf = new byte[CHUNK_SIZE];
    while (true) {
        int read = source.read(cbuf);
        if (read == -1) {
            break;
        } else {
            result.add(ByteSource.wrap(Arrays.copyOf(cbuf, read)));
        }
    }
}
ByteSource body = ByteSource.concat(result);

La ByteSource peut être lue comme une InputStream à tout moment par la suite:

InputStream data = body.openBufferedStream();
2
30thh

Si vous avez 40 Mo de données, je ne vois pas pourquoi il faudrait plus de 40 Mo pour créer un octet []. Je suppose que vous utilisez un ByteArrayOutputStream en pleine croissance qui crée une copie byte [] lorsque vous avez terminé.

Vous pouvez essayer l'ancien lire le fichier à la fois approche.

File file = 
DataInputStream is = new DataInputStream(FileInputStream(file));
byte[] bytes = new byte[(int) file.length()];
is.readFully(bytes);
is.close();

L'utilisation d'un MappedByteBuffer est plus efficace et évite une copie des données (ou l'utilisation du tas) à condition que vous puissiez utiliser le ByteBuffer directement. Toutefois, si vous devez utiliser un octet [], cela ne vous aidera probablement pas beaucoup.

2
Peter Lawrey

... est venu ici avec la même observation lors de la lecture d'un fichier de 1 Go: ByteArrayOutputStream d'Oracle a une gestion de mémoire paresseuse. Un tableau d'octets est indexé par un int et est limité à 2 Go. Cela peut être utile sans dépendance vis-à-vis de tiers:

static public byte[] getBinFileContent(String aFile) 
{
    try
    {
        final int bufLen = 32768;
        final long fs = new File(aFile).length();
        final long maxInt = ((long) 1 << 31) - 1;
        if (fs > maxInt)
        {
            System.err.println("file size out of range");
            return null;
        }
        final byte[] res = new byte[(int) fs];
        final byte[] buffer = new byte[bufLen];
        final InputStream is = new FileInputStream(aFile);
        int n;
        int pos = 0;
        while ((n = is.read(buffer)) > 0)
        {
            System.arraycopy(buffer, 0, res, pos, n);
            pos += n;
        }
        is.close();
        return res;
    }
    catch (final IOException e)
    {
        e.printStackTrace();
        return null;
    }
    catch (final OutOfMemoryError e)
    {
        e.printStackTrace();
        return null;
    }
}
0
Sam Ginrich