Потребление памяти при загрузке файла сервлета - PullRequest
2 голосов
/ 10 июня 2010

Я использую сервлет для загрузки нескольких файлов (используя apache commons fileupload). Часть моего кода размещена ниже. Моя проблема в том, что если я загружаю несколько файлов одновременно, потребление памяти сервером приложений резко возрастает. Это, вероятно, нормально, если бы это было только до завершения загрузки файла, но сервер приложений, кажется, зависает в памяти и никогда не возвращает его в ОС. Я беспокоюсь, что когда я запустлю это в производство, я получу исключение нехватки памяти на сервере. Любые идеи о том, почему это происходит? Я думаю, что сервер, возможно, начал сеанс и вернет память после его истечения, но я не уверен на 100%.

    if (ServletFileUpload.isMultipartContent(request)) {
        ServletFileUpload upload = new ServletFileUpload();
        FileItemIterator iter = upload.getItemIterator(request);
        while (iter.hasNext()) {
            FileItemStream license = iter.next();
            if (license.getFieldName().equals("upload_button") || license.getName().equals("")) {
                continue;
            }
            // DataInputStream stream = new DataInputStream(license.openStream());
            InputStream stream = license.openStream();
            List<Integer> byteArray = new ArrayList<Integer>();
            int tempByte;
            do {
                tempByte = stream.read();
                byteArray.add(tempByte);
            } while (tempByte != -1);
            stream.close();
            byteArray.remove(byteArray.size() - 1);
            byte[] bytes = new byte[byteArray.size()];
            int i = 0;
            for (Integer tByte : byteArray) {
                bytes[i++] = tByte.byteValue();
            }
        }
    }

Заранее спасибо !!

Ответы [ 3 ]

2 голосов
/ 10 июня 2010

При создании ServletFileUpload вы должны передать объект FileItemFactory (в частности, DiskFileItemFactory), который вы настраиваете самостоятельно, а не полагаться на значения по умолчанию. Значения по умолчанию могут не соответствовать вашим требованиям, особенно в производственных средах с большим объемом.

2 голосов
/ 11 июня 2010

Здесь

ArrayList<Integer> byteArray = new ArrayList<Integer>();
int tempByte;
do {
 tempByte = stream.read();
 byteArray.add(tempByte);

Вы записываете каждый байт прямо в память в виде массива целых чисел! Каждое целое число занимает 4 байта памяти, в то время как вам нужен только один байт на каждый прочитанный байт. Фактически вы должны использовать ArrayList<Byte> или лучше byte[] вместо этого, поскольку каждый byte стоит только один байт памяти, но при этом каждый раз выделяется столько же памяти, сколько размер файла.

А здесь

byte[] bytes = new byte[byteArray.size()];

впоследствии вы выделяете столько памяти, сколько занимает файл. С другой стороны, вы и ArrayList<Integer>, и byte[] выделяете в 5 раз больше памяти, чем размер файла.

Это пустая трата времени.

Вы должны записать это OutputStream немедленно , например FileOutputStream.

InputStream input = null;
OutputStream output = null;
try {
    input = license.openStream();
    output = new FileOutputStream("/file.ext");
    byte[] buffer = new byte[1024];
    for (int length; (length = input.read(buffer)) > 0;) {
        output.write(buffer, 0, length);
    }
} finally {
    if (output != null) try { output.close(); } catch (IOException logOrIgnore) {}
    if (input != null) try { input.close(); } catch (IOException logOrIgnore) {}
}

Это эффективно стоит всего 1 КБ памяти для буфера вместо всей длины файла в байтах (или в 4 раза больше при использовании целых чисел).

Или, если вы действительно хотите иметь его в byte[], просто пропустите весь шаг ArrayList<Integer>. Это не имеет никакого смысла. Используйте ByteArrayOutputStream как OutputStream.

InputStream input = null;
ByteArrayOutputStream output = null;
try {
    input = license.openStream();
    output = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    for (int length; (length = input.read(buffer)) > 0;) {
        output.write(buffer, 0, length);
    }
} finally {
    if (output != null) try { output.close(); } catch (IOException logOrIgnore) {}
    if (input != null) try { input.close(); } catch (IOException logOrIgnore) {}
}

byte[] bytes = output.toByteArray();

Это, однако, все еще стоит столько же памяти, сколько размер файла, но теперь он больше не в 5 раз больше размера файла, чем вы изначально делали с ArrayList<Integer> и byte[] впоследствии.


Обновление: Согласно вашему комментарию, вы хотите сохранить это в базе данных. Вы также можете сделать это, не сохраняя весь файл в памяти Java. Просто запишите полученный InputStream немедленно в БД, используя PreparedStatement#setBinaryStream().

final String SQL = "INSERT INTO file (filename, contentType, content) VALUES (?, ?, ?)";
String filename = FilenameUtils.getName(license.getName());
InputStream input = license.openStream();

Connection connection = null;
PreparedStatement statement = null;
try {
    connection = database.getConnection();
    statement = connection.prepareStatement(SQL);
    statement.setString(1, filename);
    statement.setString(2, getServletContext().getMimeType(filename));
    statement.setBinaryStream(3, input);
    statement.executeUpdate();
} catch (SQLException e) {
    throw new ServletException("Saving file in DB failed", e);
} finally {
    if (statement != null) try { statement.close(); } catch (SQLException logOrIgnore) {}
    if (connection != null) try { connection .close(); } catch (SQLException logOrIgnore) {}
}
0 голосов
/ 10 июня 2010

Правильный способ обработки потоков в Java (по крайней мере, до Java 7):

InputStream is;
try {
   is = ...
} catch (IOEXception ex) {
   // report exception - print, or throw a wrapper
} finally {
   try {
      is.close();
   } catch (IOException ex) {}
}

(возможно, регистрация и исключения во втором улове)

Если вы нене закрывайте свои потоки, сборщик мусора .

не освободит память
...