Почему Java потребляет очень много ресурсов в Я.Контест?

Выполняю задачи в яндекс контест, пробую разные языки и наконец дело дошло и до Java. Так вот первая проблема возникла при использовании простейшего Set, программа заняла 75Mb памяти при ограничении на 60 (при компиляторе Oracle Java8, при OpenJDK уже были другие показатели отчет можете посмотреть в конце поста). Размер входных данных : 100000 чисел; размер Set: 63259 чисел. Тот же самый результат показал при пересечении множеств, только уже превышено время выполнения более 1 секунды. Попробовал и на примитивных программах, тот же самый результат.

Простейшая программа :

import java.util.Scanner;

public class Coditioner {
    public static void main(String[] args) {
        Integer t_room = 0, t_cond = 0;
        Scanner in = new Scanner(System.in);
        t_room = in.nextInt();
        t_cond = in.nextInt();

        String mode;
        mode = in.next();

        switch (mode) {
            case "fan":
                System.out.println(t_room);
                break;
            case "auto":
                System.out.println(t_cond);
                break;
            case "heat":
                if (t_room > t_cond)
                    System.out.println(t_room);
                else
                    System.out.println(t_cond);
                break;
            case "freeze":
                if (t_room < t_cond) {
                    System.out.println(t_room);
                }
                else
                    System.out.println(t_cond);
                break;
            default:
                break;
        }
        in.close();
    }
}

Результат программы выше

Результат простого Set (входные данные - 100000 чисел, лимит времени - 1сек, лимит памяти - 60мб):

import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

public class A_Count_Of_Diff_Nums {
    public static void main(String[] args) {
        
        Set<Integer> set = new HashSet<Integer>();
        Scanner in = new Scanner(System.in);
        while (in.hasNext()) {
            set.add(in.nextInt());
        }
        in.close();
        System.out.println(set.size());

    }
}

введите сюда описание изображения

Где ML - это превышение лимита используемой памяти.

Почему такие показатели выдает Java?


Ответы (2 шт):

Автор решения: Igor Kudryashov

Ваше Java приложение работает не самостоятельно, а внутри JVM, память потребляемая Java в операционной системе равна память JVM + память вашего приложения. Это как если бы для приложения С/С++ замерять память вместе с памятью, потребляемой операционной системой. Почитайте что-нибудь о виртуальной Java машине, как она работает и пр.

По поводу времени из вашего кода не понятно что и как вы считали.

→ Ссылка
Автор решения: Stanislav Volodarskiy

Коротко

Разница в сборке мусора. Java 15 снабжена более агрессивным сборщиком свежего мусора. Этот сборщик успевает убирать временные объекты. Сборщик Java 8 не успевает. Но ему можно помочь.

Длинно

Домашние тесты

Вот программа, которая заполняет хэш-таблицу на сто тысяч элементов, а затем в цикле помещает в неё те же самые числа снова и снова без конца. Сама таблица после первого цикла перестаёт меняться.

import java.util.HashSet;

public class Main {
    public static void main(String[] args) {
        HashSet<Integer> s = new HashSet<>();
        while (true) {
            // System.gc();
            for (int i = 0; i < 100000; ++i) {
                s.add(i);
            }
        }
    }
}

На моём железе эта программа занимает 3.5GB памяти почти сразу. А если вы уберёте комментарий перед вызовом сборщика мусора, то потребление памяти будет около 49MB.

Разница – временные объекты, которые мусорщик не успевает собрать. HashSet может содержать только Integer, в цикле перебираются int. Каждый int, кроме самых маленьких, порождает новый экземпляр Integer. Этот Integer передаётся в реализацию метода add, который его никуда не добавляет, так как такой объект уже в таблице есть. Эти экземпляры Integer ожидают сборщик мусора. Тот не успевает их собирать, они накапливаются. Расход памяти приближается к 4GB (число для моего железа), свободной памяти становится всё меньше. Наконец JVM уже не может выделить новый объект, работа программы приостанавливается (на очень короткое время), чтобы сборщик успел почистить мусор.

Тесты на сервере

Автор вопрос снабдил меня ссылкой на соревнование: https://contest.yandex.ru/contest/27663. Я загружал тестовую программу туда, меняя размер таблицы. Правильность меня не интересовала, программа валится на первом же тесте, зато сервер рапортует причину ошибки (TL – time limit, ML – memory limit) и занятую память. Я не для всех значений запускал Java 15, у неё всё и так хорошо:

n Oracle Java 8 Mb OpenJDK Java 15 Mb
100 TL 12.11 TL 9.41
200 ML 66.90 TL 27.20
1000 ML 91.26
2000 ML 91.13
10000 ML 79.40
20000 ML 76.05
100000 ML 142.82 TL 33.77
200000 ML 136.76

То что числа растут нерегулярно, вероятно, связано с несовершенством проверки объёма памяти: как я понимаю, некоторый процесс регулярно спрашивает память у операционной системы и останавливает нашу программу если число больше 64Mb.

Java 8 провалила по памяти все тесты кроме первого. А первый не провалила, потому что маленькие Integer кешированы в JVM. При n = 100 не создаются новые временные объекты.

Java 15 тоже генерирует заметное количество мусора: об этом говорит разница между памятью для n = 100 и n = 200. Дополнительные сто чисел в таблице не могут занимать 17MB, это именно мусор. Но всё же сборщик мусора старается, мусор накапливается не слишком быстро.

Теперь добавляем в цикл вызов сборщика мусора и Java 8 для n = 100000 завершается с TL 30.16Mb. Ситуация исправлена.

Рабочее решение

Чтобы Java 8 справилась, надо помочь JVM собирать мусор. Я вызываю сборщик каждые десять тысяч итераций. Этот код проходит все тесты:

import java.util.HashSet;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        HashSet<Integer> s = new HashSet<>();
        Scanner sc = new Scanner(System.in);
        int c = 0;
        while (sc.hasNextInt()) {
            if (c == 10000) {
                System.gc();
                c = 0;
            }
            ++c;
            s.add(sc.nextInt());
        }
        System.out.println(s.size());
    }
}
→ Ссылка