> ## Content Index
> Fetch the complete content index at: https://lsdev.pl/llms.txt
> Use this file to discover other available public pages before exploring further.

# JVM nie zauważa nowego limitu CPU po in-place resize
- URL: https://lsdev.pl/posts/jvm-nie-zauwaza-nowego-limitu-cpu-po-in-place-resize/
- Published: 2026-09-12T00:24:55.000Z
- Updated: 2026-09-12T18:54:43.000Z
- Description: Ustawiłeś nowy limit CPU na działającym podzie przez in-place resize, a JVM wciąż pracuje z liczbą rdzeni sprzed zmiany. availableProcessors() zwraca starą wartość, pule wątków mają stary rozmiar. To nie błąd konfiguracji — JVM odczytuje limity cgroupa raz, przy starcie procesu.
- Author: Łukasz Sakowicz
- Tags: Kubernetes, Java, JVM, CPU, cgroup, in-place resize, #lsdev-ai

Ustawiłeś nowy limit CPU na działającym już podzie przez in-place resize i czekasz, aż aplikacja w końcu z niego skorzysta. Kubernetes mówi, że limit się zmienił. Metryki węzła to potwierdzają. A JVM w środku kontenera zachowuje się tak, jakby nic się nie stało — nadal odpala tyle samo wątków GC, `Runtime.getRuntime().availableProcessors()` zwraca starą liczbę, a `ForkJoinPool.commonPool()` ma rozmiar sprzed zmiany. To nie jest błąd w konfiguracji. To dwa mechanizmy, które nigdy nie miały się ze sobą komunikować po fakcie.

## In-place resize obiecuje zmianę CPU bez restartu — i słowo dotrzymuje

In-place pod resizing pozwala zmieniać przydzielone kontenerowi requesty i limity CPU oraz pamięci na działającym już podzie, często bez konieczności restartu kontenera — tak opisuje tę funkcję blog Kubernetes.io. Kubernetes odzwierciedla zmianę w polu `status.containerStatuses[*].resources`, które pokazuje faktycznie skonfigurowane zasoby na działającym kontenerze. Funkcja trafiła do Kubernetesa jako alpha w wersji 1.27 — zmiana CPU i pamięci bez restartu kontenerów.

Motywacja jest konkretna, nie akademicka: niektóre aplikacje Javy potrzebują znacznie więcej CPU podczas inicjalizacji niż w czasie normalnej pracy, jak zauważa ten sam wpis. Chcesz dać JVM dużo rdzeni na start, potem je oddać, bez zabijania procesu i odbudowywania stanu. Na poziomie API Kubernetesa to działa dokładnie tak, jak sugeruje nazwa: zmiana wchodzi w życie bez restartu.

Problem zaczyna się piętro wyżej — w samej JVM.

## JVM czyta cgroup dokładnie raz — przy starcie

JVM traktuje dostępną liczbę procesorów jako wartość obliczoną jednorazowo przy starcie procesu; taki jednorazowy, statyczny odczyt nie jest w stanie uchwycić dynamicznej zmiany limitu w trakcie działania — tak opisuje to zgłoszenie w Java Bug System (OpenJDK). Sam proces nie jest w stanie stwierdzić, ile CPU dostanie, patrząc wyłącznie na wartość CPU Shares przypisaną własnemu procesowi — to dodatkowa niepewność, bo shares to wartość względna wobec innych procesów na hoście, a nie twardy limit.

Mechanicznie wygląda to tak: `os::active_processor_count()` może zwrócić mniejszą liczbę niż liczba rdzeni fizycznych, bo `CgroupSubsystem::active_processor_count()` odczytuje limit CPU skonfigurowany w pseudo-systemie plików cgroup. Ten odczyt dzieje się w kodzie natywnym — kod C++ OpenJDK czyta limity cgroups przy starcie procesu, jak opisuje blog Red Hat Developer. Przy włączonym logowaniu kontenerowym widać to wprost: JVM loguje wykrycie cgroups v2 i odczyt ścieżki `/sys/fs/cgroup/cpu.max` już w fazie inicjalizacji.

Jeżeli nie ustawisz nic ręcznie, implementacja `os::initialize_initial_active_processor_count()` na Linuksie sięga albo po wywołanie systemowe, albo po pliki `cpu.shares`/`cpu.quota` cgroupa — i robi to raz, według wpisu na blogu Brice'a Dutheila. To ustalenie pochodzi z bloga społecznościowego, nie z dokumentacji Oracle, więc traktuj je jako prawdopodobne, a nie oficjalnie potwierdzone. Rezultat jest ten sam: to, co Kubernetes zmienia w locie, JVM widziała tylko raz, w momencie, którego już nie ma.

## Co dokładnie zamraża się razem z liczbą rdzeni

To nie jest tylko liczba zwracana przez jedną metodę API. Liczba wątków GC, wątek kompilatora oraz rozmiar fork-join poola są pochodną liczby dostępnych procesorów wykrytej przy starcie — potwierdza to zgłoszenie w OpenJDK Bug System opisujące tę zmianę. Po stronie samego JDK poprawka miała sprawić, że dostępna liczba procesorów będzie ograniczona wyłącznie przez cpuset i maksymalny limit ustawiony w cgroups — czyli że odczyt przy starcie będzie poprawny względem cgroupa. Ale poprawny odczyt przy starcie to nie to samo, co odczyt na bieżąco.

Dla domyślnego kolektora równoległego dochodzi kolejna warstwa: dokumentacja Oracle podaje, że przy liczbie wątków sprzętowych N większej niż 8 kolektor równoległy używa stałego ułamka N jako liczby wątków GC — w przybliżeniu 5/8 dla dużych N, a poniżej 8 używa całego N. Ten ułamek liczony jest raz, od liczby wykrytej na starcie. Jeśli podniesiesz limit CPU z 4 do 16 rdzeni w locie, GC dalej pracuje tak, jakby miał 4.

## Dowody z terenu: zgłoszenie o 17.0.5 i pułapka buildpacków

Zgłoszenie na GitHubie w repozytorium Adoptium opisuje sytuację, w której od wersji JDK 17.0.5 aplikacja Javy uruchomiona w kontenerze miała dostęp do wszystkich rdzeni maszyny hosta, mimo ustawionych ograniczeń CPU na poziomie środowiska uruchomieniowego kontenera. To pojedyncze zgłoszenie społecznościowe, nie potwierdzony błąd opisany w dokumentacji — traktuj je jako sygnał dla konkretnej wersji, nie jako regułę dla całego OpenJDK. W tym samym wątku jawne ustawienie `-XX:ActiveProcessorCount=1` usuwało problem: efektywna liczba CPU wynosiła 1, a `Runtime.getRuntime().availableProcessors()` zwracało 1.

Podobna pułapka istnieje po stronie narzędzi buildowych. Skrypt `active_processor_count.go` z buildpacków Paketo ustawia flagę `-XX:ActiveProcessorCount` na wartość zwróconą przez funkcję Go `runtime.NumCPU()` — a ta, według kodu źródłowego projektu, zwraca liczbę procesorów hosta, nie liczbę dostępną w kontenerze. Jeśli obraz zbudowany jest przez ten buildpack, JVM dostaje jawnie ustawioną, ale błędną liczbę procesorów, jeszcze zanim w ogóle dotknie tematu cgroupów.

## Co z tym zrobić: ActiveProcessorCount albo restart

Masz dwie realne opcje, nie trzy. Pierwsza: pogódź się z tym, że in-place resize CPU dla kontenera z JVM wymaga restartu procesu, żeby nowa liczba rdzeni dotarła do GC i puli fork-join. To jest bezpieczne i przewidywalne, ale unieważnia sens funkcji „bez restartu” dla tego konkretnego przypadku.

Druga: wymuś liczbę procesorów jawnie flagą `-XX:ActiveProcessorCount=<n>`, dostępną w wierszu poleceń JVM, i zmieniaj ją razem z resize'em przez zewnętrzny mechanizm, który potem i tak restartuje kontener z nową wartością. Wpis na blogu Mike my bytes rekomenduje jawne ustawienie tej flagi właśnie po to, by ustabilizować zachowanie JVM, gdy pod nie ma ustawionego limitu CPU — przy `-XX:ActiveProcessorCount=2` aplikacja zawsze „widzi” 2 CPU, niezależnie od tego, jak duży jest węzeł, na którym akurat wylądowała. To rekomendacja jednego autora, nie oficjalne wytyczne, ale mechanika, którą opisuje, jest zgodna z tym, jak JVM faktycznie odczytuje procesory.

W praktyce oznacza to jedno: in-place pod resize CPU rozwiązuje problem na poziomie Kubernetesa i nie dotyka problemu na poziomie procesu JVM. Jeśli planujesz na nim budować automatyzację skalowania procesów Javy, licz się z restartem kontenera przy każdej zmianie — albo zaakceptuj, że aplikacja przez cały czas życia poda będzie działać z liczbą rdzeni, jaką dostała przy starcie, niezależnie od tego, co pokazuje `status.containerStatuses`.