1. Profilinqə giriş
Profilinq — proqramınız üçün tibbi müayinə kimidir: biz sadəcə “temperatur”a (monitorinq) baxmırıq, tətbiqin harasının “ağrıdığını”, nəyin yavaş işlədiyini, yaddaşın və resursların harada həddən artıq sərf olunduğunu axtarırıq.
Profilinq — proqramın işi barədə məlumatın toplanması və analizi prosesidir; məqsəd dar boğazların (bottlenecks) və qeyri-səmərəli kod hissələrinin aşkarlanmasıdır. Monitorinqdən fərqli olaraq, hansı ki, adətən ümumi göstəriciləri izləyir (prosessor yükü, yaddaş, axınların sayı), profilinq daxilə baxmağa imkan verir: hansı metodların ən çox çağırıldığını, nə qədər vaxt apardığını, nə qədər obyekt yaradıldığını və yaddaş sızmasının dəqiq harada baş verdiyini öyrənməyə kömək edir.
Profilinq nə zaman həqiqətən lazımdır?
- Tətbiq “yavaşıyır”, amma səbəb aydın deyil.
- Yaddaş istehlakı qəfil artıb.
- Kodu yenilədikdən sonra nəsə daha uzun işləməyə başlayıb.
- Serverdə niyə resurs çatışmadığını anlamaq lazımdır.
Yeri gəlmişkən, demək olar ki, hər bir inkişafçı həyatda ən azı bir dəfə düzgün olmayan kod hissəsini optimallaşdırıb. Niyə? Çünki dar boğazı “gözə” müəyyənləşdirmək demək olar ki, mümkün deyil — bunun üçün də profayler lazımdır.
Profilinqin əsas metrikləri
- Metodların icra vaxtı (CPU profilinqi): Hansı metodlar ən çox vaxt aparır? Proqram prosessoru harada “xərcləyir”?
- Yaddaşdan istifadə (Memory profilinqi): Hansı obyektlər ən tez-tez yaradılır? Harada lazım olduğundan artıq yaddaşda qalırlar?
- Obyektlərin sayı: Eyni tipli obyektləri çox yaratmırıq ki?
- Axınlar: Axınların sayı çox deyil ki? Bloklanmalar (deadlock, contention) yoxdur ki?
- Metod çağırışları: Stack dərinliyi nə qədərdir? Çıxışsız rekursiya baş vermir ki?
2. Profilinq alətləri
Java dünyasında profilinq aparmağa imkan verən bir neçə klassik (və pulsuz!) alət var. Əsaslarını nəzərdən keçirək.
VisualVM
VisualVM — JDK-nin (JDK 6-dan başlayaraq) tərkibinə daxil olan pulsuz alətdir. O, imkan verir:
- Yerli və uzaq JVM-lərə qoşulmağa.
- Yaddaşa, axınlara, CPU-ya, zibil toplanmasına baxmağa.
- heap dump etməyə və onu analiz etməyə.
- Tətbiqi CPU və yaddaşa görə profilləşdirməyə.
VisualVM-i necə işə salmalı?
Adətən o, JDK qovluğundadır: <path_to_JDK>/bin/jvisualvm
İşə salırsınız, Java-tətbiqinin prosesini seçirsiniz — və onun həyatını akvariumdakı balıqlar kimi izləyə bilərsiniz (sadəcə burada balıqlar obyektlər və axınlardır).
JProfiler, YourKit
Bunlar kommersiya, amma çox güclü alətlərdir. Onlar imkan verir:
- Yaddaşı, CPU-nu, axınları profilləşdirməyə.
- Yaddaşın “şəkillərini” (heap dump) analiz etməyə.
- Sızmaları, uzun bloklanmaları, yavaş metodları tapmağa.
- IDE və CI/CD ilə inteqrasiya etməyə.
Başlanğıc üçün VisualVM kifayətdir, amma böyük layihələrə “böyüdükdə” bu alətlərə də baxın.
Java Flight Recorder (JFR)
JFR — JVM-in işi ilə bağlı hadisələri toplamaq üçün JDK-ya daxil edilmiş alətdir. Çox yüngüldür, demək olar ki, performansa təsir etmir və aşağıdakı məlumatları toplamağa imkan verir:
- Metodların iş vaxtı.
- Zibil toplanması.
- Axınlar, bloklanmalar, xətalar.
JFR tətbiqi yavaşlatmaq olmazkən, production mühitində iş üçün əladır.
3. Təcrübə: sadə bir tətbiqi profil edirik
Gəlin uzun hesablamalar apara və əməliyyat tarixçəsini saxlayan mini kalkulyator yaradaq (döngələr, kolleksiyalar və yaddaşla işimiz olsun deyə).
Kod nümunəsi: “Yavaş kalkulyator”
import java.util.ArrayList;
import java.util.List;
public class SlowCalculator {
private final List<String> history = new ArrayList<>();
public int add(int a, int b) {
simulateHeavyOperation();
int result = a + b;
history.add(a + " + " + b + " = " + result);
return result;
}
public int multiply(int a, int b) {
simulateHeavyOperation();
int result = a * b;
history.add(a + " * " + b + " = " + result);
return result;
}
public List<String> getHistory() {
return history;
}
// “Ağır” əməliyyatın simulyasiyası
private void simulateHeavyOperation() {
for (int i = 0; i < 5_000_000; i++) {
Math.sqrt(i);
}
}
}
İndi isə — əsas sinif:
public class Main {
public static void main(String[] args) {
SlowCalculator calc = new SlowCalculator();
for (int i = 0; i < 10; i++) {
calc.add(i, i * 2);
calc.multiply(i, i + 5);
}
System.out.println("Əməliyyatların tarixi:");
for (String entry : calc.getHistory()) {
System.out.println(entry);
}
}
}
Bu tətbiqi necə profilləmək olar?
- Tətbiqi tərtib edib işə salın.
- VisualVM-i (jvisualvm) açın.
- Prosesinizi tapın (adətən Main sinfinin adına görə).
- CPU Profiler bölməsinə keçin və Start düyməsini basın.
- Proqrama bir qədər işləməyə imkan verin (və ya yavaş düyməni bir daha basın).
- Hansı metodların ən çox vaxt apardığına baxın.
Sual: Sizin fikrinizcə, ən “ağır” metod hansıdır?
Cavab: Təbii ki, simulateHeavyOperation() — çünki o, 5_000_000 iterasiyalı nəhəng döngə fırladır və Math.sqrt çağırır.
4. Performansla bağlı tipik problemlər
Yavaş alqoritmlər
Ən çox rast gəlinən səbəb: alqoritmin və ya verilənlər strukturunun uğursuz seçimi. Məsələn, siyahıda axtarış etmək əvəzinə HashMap istifadə etmək, yaxud “köpük” çeşidləməsi əvəzinə sürətli çeşidləmə.
Nümunə:
// Yavaş axtarış
for (String s : list) {
if (s.equals("target")) {
// tapıldı
}
}
Daha yaxşısı, sürətli axtarış üçün Set və ya Map istifadə edin.
Yaddaş sızmaları
Yaddaş sızması — obyektlər “canlı” qalanda (onlara istinadlar olduqda), baxmayaraq ki, artıq lazım deyillər. Bu, yaddaş istifadəsinin artmasına və nəticədə OutOfMemoryError-a gətirib çıxarır.
public class MemoryLeakDemo {
private static List<byte[]> leakyList = new ArrayList<>();
public static void main(String[] args) {
while (true) {
leakyList.add(new byte[1_000_000]); // 1 MB
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
}
}
}
Sızmaları necə tapmalı?
heap dump edin, VisualVM-də baxın və hansı obyektlərin ən çox yaddaş tutduğunu və niyə onlara istinadlar qaldığını araşdırın.
Artıq obyekt yaradılması
Əgər döngədə çox sayda eyni tipli obyekt yaradırsınızsa, bu, təkcə zibil toplayıcısını yükləmir, həm də tətbiqi ləngidə bilər.
for (int i = 0; i < 1_000_000; i++) {
String s = new String("hello"); // pisdir!
}
Daha yaxşısı, sabitlərdən və ya sətir hovuzundan (String pool) istifadə etməkdir.
Axınların bloklanması
Bir neçə axın eyni resurs uğrunda “mübarizə” aparırsa (məsələn, sinxronlaşdırılmış metod), bu, bloklanmalara və performansın düşməsinə səbəb ola bilər.
public synchronized void doWork() {
// ...
}
Necə tapmalı?
VisualVM-də Threads bölməsində hansı axınların “asılı qaldığını” və səbəbini görə bilərsiniz.
5. Optimallaşdırma yanaşmaları
Əvvəl ölç, sonra optimallaşdır
Optimallaşdırmanın əsas qaydası: Yavaşlamayan şeyi optimallaşdırma.
Əvvəlcə profilləşdirin, “isti nöqtələri” tapın, sonra kodu dəyişin. Bəzən ən “aşkar” kod hissəsi cəmi 1 % vaxt tutur, əsl “canavar” isə haradasa kitabxanada və ya gözlənilməz yerdə olur.
İsti nöqtələri tapmaq üçün profaylerdən istifadə
İsti nöqtə — tətbiqin iş vaxtının ən böyük payını tutan metod və ya kod hissəsidir.
VisualVM-də bu, CPU Profiler bölməsində görünür:
- Metodları icra vaxtına görə sıralayın.
- Stack trace-a baxın: kim kimi çağırır.
- Unutmayın ki, bəzən “təqsirkar” sizin kodunuz deyil, kitabxana və ya hətta JDK olur.
Optimallaşdırma nümunələri
Nümunə 1: Alqoritmin əvəzlənməsi
Ən çox vaxtın siyahıda axtarışa sərf edildiyini tapmısınızsa, List-i HashSet ilə əvəz edin.
Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
// tez!
}
Nümunə 2: Allokasiyaların sayını azaltmaq
Döngədə yeni obyektlər yaratmaq əvəzinə, təkrar istifadə və ya StringBuilder-dən istifadə edin.
// Pis:
for (int i = 0; i < 10000; i++) {
String s = "Nəticə: " + i;
}
// Daha yaxşı:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.setLength(0);
sb.append("Nəticə: ").append(i);
String s = sb.toString();
}
Nümunə 3: Keşləmə
Əgər görürsünüzsə ki, ağır metod eyni parametrlərlə çoxlu dəfə çağırılır, keşdən istifadə edin.
Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
return sqrtCache.computeIfAbsent(x, Math::sqrt);
}
6. Demonstrasiya: kalkulyatorumuzu sürətləndiririk
Problem: simulateHeavyOperation() çox vaxt aparır
Addım 1. Profilinq
VisualVM-də görünür ki, vaxtın demək olar ki, hamısı 5_000_000 iterasiyalı döngənin içindəki Math.sqrt(i)-ə gedir.
Addım 2. Optimallaşdırma
Əgər bu, sadəcə yük simulyasiyasıdırsa — onu çıxarın və ya iterasiya sayını azaldın.
Əgər bu real biznes məntiqidirsə — düşünün, görəsən belə etmək olarmı:
- Nəticəni keşləmək.
- Daha sürətli alqoritmdən istifadə etmək.
- Hesablamaları ayrıca axına keçirmək (əgər istifadəçi üçün kritik deyilsə).
Optimallaşdırma nümunəsi:
private void simulateHeavyOperation() {
// Əvvəl 5_000_000 idi, indi 100_000
for (int i = 0; i < 100_000; i++) {
Math.sqrt(i);
}
}
Addım 3. Nəticəni yoxlamaq
Profilinqi yenidən başladın — proqram daha sürətli işləyir, CPU yükü azalıb.
7. Vizualizasiya: optimallaşdırma prosesi
flowchart TD
A[Tətbiqin işə salınması]
B["Profilinq (VisualVM)"]
C[Dar boğazların müəyyən edilməsi]
D[Kodun optimallaşdırılması]
E[Təkrar profilinq]
F[Performansın yaxşılaşması]
A --> B --> C --> D --> E --> F
E --> C
8. Profilinq və optimallaşdırmada tipik səhvlər
Səhv №1: “Gözə” əsasən optimallaşdırma. Çox vaxt inkişafçılar problemin əslində harada olduğunu ölçmədən kodu dəyişməyə başlayırlar. Nəticədə — çox iş, minimal nəticə.
Səhv №2: “Reallığa uyğun olmayan” şəraitdə profilinq. Profilinqi, yüklənmə və verilənlər production-a yaxın olduqda aparmaq lazımdır. “Boş” şəraitdə profilinq həqiqi problemləri üzə çıxarmaya bilər.
Səhv №3: Yaddaş sızmalarını görməzdən gəlmək. Əgər heap dump-a baxmır və istinadları analiz etmirsinizsə, proqramın “şişdiyini” və tezliklə yıxılacağını uzun müddət görməyə bilərsiniz.
Səhv №4: Mikroskopik optimallaşdırmanın dalınca qaçmaq. Tətbiqin iş vaxtının 0.1 % hissəsini tutan kodu sürətləndirməyə günlər sərf etməyə dəyməz. Əvvəlcə — əsas dar boğazlar.
Səhv №5: Axınları və sinxronizasiyanı nəzərə almamaq. Çoxaxınlı tətbiqlərdə performans problemləri çox vaxt alqoritmlərdən deyil, bloklanma və gözləmələrdən (synchronized, contention) qaynaqlanır.
Səhv №6: Dəyişikliklərdən sonra profilinqi unutmaq. Optimallaşdırmadan sonra nəticəni mütləq yenidən yoxlayın: bəzən “optimallaşdırma” hətta işi ləngidə də bilər!
GO TO FULL VERSION