Czy tworzenie plików klas Java jest deterministyczne?

94

Czy w przypadku korzystania z tego samego JDK (tj. Tego samego javacpliku wykonywalnego) wygenerowane pliki klas są zawsze identyczne? Czy może istnieć różnica w zależności od systemu operacyjnego lub sprzętu ? Czy poza wersją JDK mogą istnieć inne czynniki powodujące różnice? Czy są jakieś opcje kompilatora, aby uniknąć różnic? Czy różnica jest możliwa tylko w teorii, czy też Oracle javacfaktycznie tworzy różne pliki klas dla tych samych danych wejściowych i opcji kompilatora?

Update 1 Interesuje mnie generacja , czyli wyjście kompilatora, a nie to, czy plik klasy można uruchomić na różnych platformach.

Aktualizacja 2 Przez „ten sam JDK” mam na myśli również ten sam javacplik wykonywalny.

Aktualizacja 3 Rozróżnienie między teoretyczną różnicą a praktyczną różnicą w kompilatorach Oracle.

[EDYTUJ, dodając sparafrazowane pytanie]
„W jakich okolicznościach ten sam plik wykonywalny javac, uruchomiony na innej platformie, wygeneruje inny kod bajtowy?”

mstrap
źródło
5
@Gamb CORA nie oznacza, że ​​kod bajtowy będzie dokładnie taki sam, jeśli zostanie skompilowany na różnych platformach; wszystko to oznacza, że ​​wygenerowany kod bajtowy zrobi dokładnie to samo.
Sergey Kalinichenko
11
Dlaczego się przejmujesz? To pachnie jak problem XY .
Joachim Sauer
4
@JoachimSauer Zastanów się, czy kontrolujesz wersje swoich plików binarnych - możesz chcieć wykryć zmiany tylko wtedy, gdy zmienił się kod źródłowy, ale wiedziałbyś, że nie był to rozsądny pomysł, gdyby JDK mógł dowolnie zmieniać wyjściowe pliki binarne.
RB.
7
@RB .: kompilator może tworzyć dowolny zgodny kod bajtowy, który reprezentuje skompilowany kod. W rzeczywistości niektóre aktualizacje kompilatora naprawiają błędy, które generują nieco inny kod (zwykle z tym samym zachowaniem w czasie wykonywania). Innymi słowy: jeśli chcesz wykryć zmiany źródła, sprawdź zmiany źródła.
Joachim Sauer,
3
@dasblinkenlight: zakładasz, że odpowiedź, którą twierdzą, jest w rzeczywistości poprawna i aktualna (wątpliwe, biorąc pod uwagę, że pytanie pochodzi z 2003 r.).
Joachim Sauer

Odpowiedzi:

68

Ujmijmy to w ten sposób:

Mogę z łatwością stworzyć całkowicie zgodny kompilator Java, który nigdy nie tworzy .classdwukrotnie tego samego pliku, mając ten sam .javaplik.

Mógłbym to zrobić, poprawiając wszystkie rodzaje konstrukcji kodu bajtowego lub po prostu dodając zbędne atrybuty do mojej metody (co jest dozwolone).

Biorąc pod uwagę, że specyfikacja nie wymaga, aby kompilator tworzył pliki klas identyczne bajt po bajcie, unikałbym uzależnienia od takiego wyniku.

Jednak kilka razy, które sprawdzałem, kompilowanie tego samego pliku źródłowego za pomocą tego samego kompilatora z tymi samymi przełącznikami (i tymi samymi bibliotekami!) Dało w wyniku te same .classpliki.

Aktualizacja: Niedawno natknąłem się na ten interesujący wpis na blogu dotyczący implementacji switchon Stringw Javie 7 . W tym poście na blogu jest kilka istotnych części, które zacytuję tutaj (moje wyróżnienie):

Aby dane wyjściowe kompilatora były przewidywalne i powtarzalne, mapy i zbiory używane w tych strukturach danych to LinkedHashMaps i LinkedHashSets, a nie tylko HashMapsi HashSets. Pod względem funkcjonalnej poprawności kodu generowanego podczas danej kompilacji, użycie HashMapi HashSetbyłoby w porządku ; kolejność iteracji nie ma znaczenia. Jednak uważamy, że korzystne jest, aby javacwyniki nie różniły się w zależności od szczegółów implementacji klas systemu .

To dość wyraźnie ilustruje problem: kompilator nie musi działać w sposób deterministyczny, o ile pasuje do specyfikacji. Twórcy kompilatorów zdają sobie jednak sprawę, że generalnie dobrym pomysłem jest wypróbowanie (pod warunkiem, że prawdopodobnie nie jest to zbyt drogie).

Joachim Sauer
źródło
@GaborSch czego brakuje? „Jakie są okoliczności, w których ten sam plik wykonywalny javac, uruchomiony na innej platformie, wygeneruje inny kod bajtowy?” w zasadzie w zależności od kaprysu grupy, która wyprodukowała kompilator
emory
3
Cóż, dla mnie byłby to wystarczający powód, aby nie polegać na tym: zaktualizowany JDK mógłby zepsuć mój system kompilacji / archiwizacji, gdybym polegał na tym, że kompilator zawsze tworzy ten sam kod.
Joachim Sauer,
3
@GaborSch: masz już doskonały przykład takiej sytuacji, więc potrzebne było dodatkowe spojrzenie na problem. Nie ma sensu powielać swojej pracy.
Joachim Sauer
1
@GaborSch Głównym problemem jest to, że chcemy zaimplementować skuteczną "aktualizację online" naszej aplikacji, dla której użytkownicy będą pobierać tylko zmodyfikowane pliki JAR ze strony internetowej. Mogę tworzyć identyczne pliki JAR z identycznymi plikami klas jako dane wejściowe. Ale pytanie brzmi, czy pliki klas są zawsze identyczne, gdy są kompilowane z tych samych plików źródłowych. Cała nasza koncepcja stoi na przeszkodzie temu faktowi.
mstrap
2
@mstrap: więc jest to w końcu problem XY. Cóż, możesz zajrzeć do różnicowych aktualizacji słoików (więc nawet jednobajtowe różnice nie spowodowałyby ponownego pobrania całego pliku jar) i i tak powinieneś podać wyraźne numery wersji do swoich wydań, więc moim zdaniem cały punkt jest dyskusyjny .
Joachim Sauer
39

Kompilatory nie mają obowiązku tworzenia tego samego kodu bajtowego na każdej platformie. javacAby uzyskać konkretną odpowiedź, należy skonsultować się z narzędziami różnych dostawców .


Pokażę praktyczny przykład tego z porządkiem plików.

Powiedzmy, że mamy 2 pliki jar: my1.jari My2.jar. Są umieszczane w libkatalogu obok siebie. Kompilator odczytuje je w porządku alfabetycznym (od tego jest lib), ale kolejność jest my1.jar, My2.jargdy system plików jest sprawa niewrażliwe i My2.jar, my1.jarjeśli to jest wielkość liter.

my1.jarMa klasę A.classz metodą

public class A {
     public static void a(String s) {}
}

My2.jarMa takie samo A.class, ale z inną metodą podpis (przyjmuje Object)

public class A {
     public static void a(Object o) {}
}

Oczywiste jest, że jeśli masz telefon

String s = "x"; 
A.a(s); 

skompiluje wywołanie metody z różnym podpisem w różnych przypadkach. Tak więc, w zależności od wrażliwości systemu plików na wielkość liter, w rezultacie otrzymasz inną klasę.

gaborsch
źródło
1
+1 Istnieją niezliczone różnice między kompilatorem Eclipse i javac, na przykład sposób generowania konstruktorów syntetycznych .
Paul Bellora,
2
@GaborSch Interesuje mnie, czy kod bajtowy jest identyczny dla tego samego JDK, czyli tego samego javaca. Wyjaśnię to jaśniej.
mstrap
2
@mstrap Zrozumiałem twoje pytanie, ale odpowiedź jest nadal taka sama: zależy od dostawcy. To javacnie to samo, ponieważ masz różne pliki binarne na każdej platformie (np. Win7, Linux, Solaris, Mac). Dla dostawcy nie ma sensu mieć różnych implementacji, ale każdy problem związany z platformą może wpłynąć na wynik (np. Zamawianie plików w katalogu (pomyśl o swoim libkatalogu), endianness itp.).
gaborsch
1
Zwykle większość z nich javacjest zaimplementowana w Javie (i javacjest to tylko prosty natywny program uruchamiający), więc większość różnic między platformami nie powinna mieć wpływu.
Joachim Sauer,
2
@mstrap - chodzi o to, że żaden dostawca nie wymaga, aby ich kompilator produkował dokładnie ten sam kod bajtowy na różnych platformach, tylko że wynikowy kod bajtowy daje takie same wyniki. Biorąc pod uwagę, że nie ma standardu / specyfikacji / wymagań, odpowiedź na Twoje pytanie brzmi: „To zależy od konkretnego dostawcy, kompilatora i platformy”.
Brian Roach,
6

Krótka odpowiedź - NIE


Długa odpowiedź

Nie bytecodemuszą być takie same dla różnych platform. To JRE (Java Runtime Environment) wie, jak dokładnie wykonać kod bajtowy.

Jeśli przejrzysz specyfikację maszyny wirtualnej Java , dowiesz się, że nie musi to być prawdą, że kod bajtowy jest taki sam dla różnych platform.

Przechodząc przez format pliku klasy , pokazuje strukturę pliku klasy jako

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

Sprawdzanie wersji pomocniczej i głównej

minor_version, major_version

Wartości elementów minor_version i major_version to podrzędne i główne numery wersji tego pliku klasy. Razem, główny i pomocniczy numer wersji określają wersję formatu pliku klasy. Jeśli plik klasy ma główny numer wersji M i podrzędny numer wersji m, oznaczamy wersję jego formatu pliku klasy jako Mm. W ten sposób wersje formatu plików klas mogą być uporządkowane leksykograficznie, na przykład 1,5 <2,0 <2,1. Implementacja maszyny wirtualnej Java może obsługiwać format pliku klasy w wersji v wtedy i tylko wtedy, gdy v leży w pewnym ciągłym zakresie Mi.0 v Mj.m. Tylko firma Sun może określić, jaki zakres wersji może obsługiwać implementacja wirtualnej maszyny języka Java zgodna z określonym poziomem wydania platformy Java

Przeczytaj więcej w przypisach

1 Implementacja maszyny wirtualnej Java JDK w wersji 1.0.2 firmy Sun obsługuje wersje formatu plików klas od 45.0 do 45.3 włącznie. Wydania JDK 1.1.X firmy Sun mogą obsługiwać formaty plików klas w wersjach z zakresu od 45.0 do 45.65535 włącznie. Implementacje wersji 1.2 platformy Java 2 mogą obsługiwać formaty plików klas wersji z zakresu od 45.0 do 46.0 włącznie.

Zatem zbadanie tego wszystkiego pokazuje, że pliki klas generowane na różnych platformach nie muszą być identyczne.

mtk
źródło
Czy możesz podać bardziej szczegółowy link?
mstrap
Myślę, że przez „platformę” odnoszą się do platformy Java, a nie do systemu operacyjnego. Oczywiście podczas instruowania javaca 1.7, aby utworzył pliki klas zgodne z wersją 1.6, będzie różnica.
mstrap
@mtk +1, aby pokazać, ile właściwości jest generowanych dla pojedynczej klasy podczas kompilacji.
gaborsch,
3

Po pierwsze, w specyfikacji nie ma absolutnie takiej gwarancji. Zgodny kompilator mógłby oznaczyć czas kompilacji w wygenerowanym pliku klasy jako dodatkowy (niestandardowy) atrybut, a plik klasy nadal byłby poprawny. Stworzyłoby to jednak inny plik na poziomie bajtów dla każdej pojedynczej kompilacji, i to w trywialny sposób.

Po drugie, nawet bez takich paskudnych sztuczek, nie ma powodu, aby oczekiwać, że kompilator zrobi dokładnie to samo dwa razy z rzędu, chyba że zarówno jego konfiguracja, jak i dane wejściowe są identyczne w obu przypadkach. Spec robi opisać nazwę pliku źródłowego jako jeden ze standardowych atrybutów i dodawanie pustych wierszy w pliku źródłowym mogłaby zmienić tabelę numer linii.

Po trzecie, nigdy nie spotkałem się z żadną różnicą w kompilacji ze względu na platformę hosta (poza tą, która wynikała z różnic w ścieżce klas). Kod, który byłby różny w zależności od platformy (tj. Natywnych bibliotek kodu) nie jest częścią pliku klasy, a faktyczne generowanie kodu natywnego z kodu bajtowego ma miejsce po załadowaniu klasy.

Po czwarte (i najważniejsze), to cuchnie nieprzyjemnym zapachem procesu (jak zapach kodu, ale z powodu tego, jak zachowujesz się na kodzie), aby chcieć to wiedzieć. Wersja źródła, jeśli to możliwe, a nie kompilacji, a jeśli musisz wersjonować kompilację, wersję na poziomie całego komponentu, a nie na poszczególnych plikach klas. W pierwszej kolejności użyj serwera CI (takiego jak Jenkins), aby zarządzać procesem przekształcania źródła w kod, który można uruchomić.

Donal Fellows
źródło
2

Uważam, że jeśli użyjesz tego samego JDK, wygenerowany kod bajtowy zawsze będzie taki sam, bez związku z używanym oprogramowaniem i systemem operacyjnym. Produkcja kodu bajtowego jest wykonywana przez kompilator java, który używa deterministycznego algorytmu do „transformacji” kodu źródłowego na kod bajtowy. Tak więc wynik zawsze będzie taki sam. W tych warunkach tylko aktualizacja kodu źródłowego wpłynie na dane wyjściowe.

viniciusjssouza
źródło
3
Czy masz jednak do tego odniesienie? Jak powiedziałem w komentarzach do pytania, zdecydowanie nie dotyczy to języka C # , więc chciałbym zobaczyć odniesienie stwierdzające, że tak jest w przypadku Javy. Myślę szczególnie, że kompilator wielowątkowy może przypisywać różne nazwy identyfikatorów w różnych przebiegach.
RB.
1
To jest odpowiedź na moje pytanie i czego bym się spodziewał, jednak zgadzam się z RB, że odniesienie do tego byłoby ważne.
mstrap
Wierzę w to samo. Myślę, że nie znajdziesz ostatecznego odniesienia. Jeśli jest to dla Ciebie ważne, możesz przeprowadzić badanie. Zbierz kilka wiodących i wypróbuj je na różnych platformach, kompilując jakiś otwarty kod źródłowy. Porównaj pliki bajtów. Opublikuj wynik. Pamiętaj, aby umieścić tutaj link.
emory
1

Ogólnie rzecz biorąc, muszę powiedzieć, że nie ma gwarancji, że to samo źródło wyprodukuje ten sam kod bajtowy, gdy zostanie skompilowane przez ten sam kompilator, ale na innej platformie.

Przyjrzałbym się scenariuszom obejmującym różne języki (strony kodowe), na przykład Windows z obsługą języka japońskiego. Pomyśl o znakach wielobajtowych; chyba że kompilator zawsze zakłada, że ​​musi obsługiwać wszystkie języki, które może zoptymalizować pod kątem 8-bitowego ASCII.

W specyfikacji języka Java znajduje się sekcja dotycząca zgodności binarnej .

W ramach kompatybilności binarnej z wydania do wydania w SOM (Forman, Conner, Danforth i Raper, Proceedings of OOPSLA '95), pliki binarne języka programowania Java są kompatybilne binarnie we wszystkich istotnych transformacjach, które autorzy identyfikują (z pewnymi zastrzeżeniami z w odniesieniu do dodawania zmiennych instancji). Korzystając z ich schematu, oto lista niektórych ważnych zmian kompatybilnych binarnie, które obsługuje język programowania Java:

• Ponowne zaimplementowanie istniejących metod, konstruktorów i inicjatorów w celu poprawy wydajności.

• Zmiana metod lub konstruktorów tak, aby zwracały wartości na danych wejściowych, dla których poprzednio generowały wyjątki, które normalnie nie powinny występować, lub kończyły się niepowodzeniem, przechodząc w nieskończoną pętlę lub powodując zakleszczenie.

• Dodawanie nowych pól, metod lub konstruktorów do istniejącej klasy lub interfejsu.

• Usuwanie prywatnych pól, metod lub konstruktorów klasy.

• Gdy cały pakiet jest aktualizowany, usuwanie domyślnych (tylko dla pakietu) pól dostępu, metod lub konstruktorów klas i interfejsów w pakiecie.

• Zmiana kolejności pól, metod lub konstruktorów w istniejącej deklaracji typu.

• Przenoszenie metody w górę w hierarchii klas.

• Zmiana kolejności na liście bezpośrednich superinterfejsów klasy lub interfejsu.

• Wstawianie nowych klas lub typów interfejsów do hierarchii typów.

Ten rozdział określa minimalne standardy kompatybilności binarnej gwarantowanej przez wszystkie implementacje. Język programowania Java gwarantuje zgodność w przypadku mieszania plików binarnych klas i interfejsów, o których nie wiadomo, że pochodzą z kompatybilnych źródeł, ale których źródła zostały zmodyfikowane w zgodny sposób opisany tutaj. Zauważ, że omawiamy kompatybilność między wersjami aplikacji. Omówienie kompatybilności między wersjami platformy Java SE wykracza poza zakres tego rozdziału.

Kelly S. French
źródło
W tym artykule omówiono, co może się stać, gdy zmienimy wersję Javy. Pytanie OP dotyczyło tego, co się stanie, jeśli zmienimy platformę w ramach tej samej wersji Javy. W przeciwnym razie to dobry chwyt.
gaborsch,
1
Jest tak blisko, jak mogłem znaleźć. Istnieje dziwna luka między specyfikacją języka a specyfikacją JVM. Do tej pory musiałbym odpowiedzieć na OP „nie ma gwarancji, że ten sam kompilator java wygeneruje ten sam kod bajtowy, gdy zostanie uruchomiony na innej platformie”.
Kelly S. French,
1

Java allows you write/compile code on one platform and run on different platform. AFAIK ; będzie to możliwe tylko wtedy, gdy plik klasy wygenerowany na innej platformie będzie taki sam lub taki sam, tj. identyczny.

Edytować

Co mam na myśli przez technicznie to samo komentarz to. Nie muszą być dokładnie takie same, jeśli porównujesz bajt po bajcie.

Tak więc zgodnie ze specyfikacją plik .class klasy na różnych platformach nie musi być dopasowywany bajt po bajcie.

rai.skumar
źródło
Pytanie PO za to , czy pliki klasowe były takie same lub „technicznie takie same”.
bdesham
Interesuje mnie, czy są identyczne .
mstrap
a odpowiedź brzmi: tak. mam na myśli to, że mogą nie być takie same, jeśli porównasz bajt po bajcie, dlatego użyłem słowa technicznie to samo.
rai.skumar
@bdesham chciał wiedzieć, czy są identyczne. nie jestem pewien, co rozumiesz przez „technicznie to samo”… czy to jest powód do odrzucenia?
rai.skumar
@ rai.skumar Twoja odpowiedź zasadniczo brzmi: „Dwa kompilatory zawsze będą generować dane wyjściowe zachowujące się tak samo”. Oczywiście to prawda; to cała motywacja platformy Java. Operator chciał wiedzieć, czy emitowany kod był identyczny w bajtach po bajcie , czego nie uwzględniłeś w swojej odpowiedzi.
bdesham,
1

Na pytanie:

„Jakie są okoliczności, w których ten sam plik wykonywalny javac, uruchomiony na innej platformie, wygeneruje inny kod bajtowy?”

Przykład Cross-Compilation pokazuje, jak możemy użyć opcji Javac: -target version

Ta flaga generuje pliki klas, które są zgodne z wersją Java, którą określamy podczas wywoływania tego polecenia. W związku z tym pliki klas będą się różnić w zależności od atrybutów dostarczonych podczas obliczania przy użyciu tej opcji.

PhilipJoseParampettu
źródło
0

Najprawdopodobniej odpowiedź brzmi „tak”, ale aby uzyskać precyzyjną odpowiedź, podczas kompilacji trzeba szukać kluczy lub generować guid.

Nie pamiętam sytuacji, w której to się dzieje. Na przykład, aby mieć identyfikator do celów serializacji, jest on zakodowany na stałe, tj. Generowany przez programistę lub IDE.

PS Również JNI może mieć znaczenie.

PPS znalazłem, że javacjest napisany w Javie. Oznacza to, że jest identyczny na różnych platformach. Dlatego nie wygenerowałby innego kodu bez powodu. Tak więc może to zrobić tylko w przypadku połączeń natywnych.

Suzan Cioc
źródło
Pamiętaj, że Java nie chroni Cię przed wszystkimi różnicami między platformami. Kolejność plików zwracanych podczas wyświetlania zawartości katalogu nie jest zdefiniowana, co może mieć pewien wpływ na kompilator.
Joachim Sauer,
0

Są dwa pytania.

Can there be a difference depending on the operating system or hardware? 

To jest pytanie teoretyczne, a odpowiedź brzmi: tak, może być. Jak powiedzieli inni, specyfikacja nie wymaga od kompilatora tworzenia plików klas identycznych bajt po bajcie.

Nawet jeśli każdy obecnie istniejący kompilator wyprodukował ten sam kod bajtowy we wszystkich okolicznościach (inny sprzęt itp.), Jutro odpowiedź może być inna. Jeśli nigdy nie planujesz aktualizacji javac lub systemu operacyjnego, możesz przetestować zachowanie tej wersji w określonych okolicznościach, ale wyniki mogą być inne, jeśli przejdziesz na przykład z Java 7 Update 11 na Java 7 Update 15.

What are the circumstances where the same javac executable, when run on a different platform, will produce different bytecode?

To niepoznawalne.

Nie wiem, czy zarządzanie konfiguracją jest powodem zadawania tego pytania, ale jest to zrozumiały powód, aby się tym przejmować. Porównywanie kodów bajtów jest uzasadnioną kontrolą IT, ale tylko w celu ustalenia, czy pliki klas uległy zmianie, a nie w celu ustalenia, czy zmieniły się pliki źródłowe.

Pomiń Addison
źródło
0

Ujmę to inaczej.

Po pierwsze, myślę, że nie chodzi o bycie deterministycznym:

Oczywiście jest to deterministyczne: losowość jest trudna do osiągnięcia w informatyce i nie ma powodu, aby kompilator wprowadzał ją tutaj z jakiegokolwiek powodu.

Po drugie, jeśli przeformułujesz go przez „jak podobne są pliki z kodem bajtowym dla tego samego pliku kodu źródłowego?”, To nie , nie możesz polegać na tym, że będą podobne .

Dobrym sposobem, aby się tego upewnić, jest pozostawienie .class (lub .pyc w moim przypadku) na etapie git. Zrozumiesz, że na różnych komputerach w Twoim zespole git zauważa zmiany między plikami .pyc, gdy żadne zmiany nie zostały wprowadzone do pliku .py (i mimo to .pyc ponownie skompilowano).

Tak przynajmniej zauważyłem. Więc umieść * .pyc i * .class w swoim .gitignore!

Augustin Riedinger
źródło