Wie kann Java Echtzeit verarbeiten?
Ihre Skepsis ist nicht überraschend. Es kann schwer sein, sich etwas vorzustellen, das all dem widerspricht, was Sie Ihr ganzes Leben lang über Java gehört haben. Daniel Kahneman erwähnt in seinem Buch „Thinking Fast and Slow“ die Tendenz, Urteile und Entscheidungen ausschließlich auf der Grundlage von leicht verfügbaren Informationen zu treffen, während fehlende oder unvollständige Informationen ignoriert oder unterschätzt werden. Er nannte dies „What You See Is All There Is“ (WYSIATI, „Was Sie sehen, ist alles, was es gibt“).
Lassen Sie uns diese Vorurteile mit einigen Fakten widerlegen:
- Die Spezifikationen von Java schließen eine Echtzeitausführung nicht aus. Weder die Java-Sprachspezifikation noch die Spezifikation der Java Virtual Machine enthalten irgendetwas, was eine Echtzeitausführung ausschließt.
- Herkömmliche JVMs haben große Fortschritte bei der Reduzierung von Nichtdeterminismus erzielt. Beispielsweise haben Oracle und OpenJDK JVMs (bezeichnen wir sie als „trad-JVMs“) jahrelang daran gearbeitet, die Garbage Collection (GC) zu verbessern, eine der Hauptursachen für Nichtdeterminismus. Java 17 bietet vier GC-Algorithmen, die jeweils Kompromisse zwischen Leistung und Latenzzeit eingehen. Azul Systems bietet in seiner Zing JVM der Enterprise-Klasse sogar eine „pauseless“ GC an.
- Die eigentliche Herausforderung liegt in der Unterstützung durch Betriebssysteme. Viele Betriebssysteme unterstützen keine Echtzeitausführung, und um JVMs vollständig echtzeitfähig zu machen, wären architektonische Änderungen erforderlich, die Kunden abschrecken könnten. Aus diesem Grund bieten herkömmliche JVMs mehrere GC-Algorithmen an – einige Benutzer legen Wert auf reine Leistung, andere hingegen auf kürzere Pausen.
- Perc ist auf deterministisches Verhalten ausgelegt. Der GC von Perc kann jederzeit durch Java-Threads mit hoher Priorität unterbrochen werden und wird nahtlos fortgesetzt. Er verwendet parallele Worker-Threads, die gleichzeitig mit Java-Threads ausgeführt werden, und nutzt Multi-Core-Prozessoren, um die Leistung aufrechtzuerhalten, ohne den Determinismus zu beeinträchtigen.
Die Unterstützung von Echtzeitanwendungen umfasst jedoch weit mehr als nur den Garbage Collector.
- JIT-Compiler-Unterbrechungen:
Traditionelle JVMs haben unvorhersehbare Just-in-Time (JIT)-Compiler-Unterbrechungen. Sofern Sie den JIT-Compiler nicht deaktivieren, unterbrechen sie einen Thread willkürlich, um interpretierten Bytecode in nativen Code zu kompilieren oder zuvor kompilierten Code zu optimieren. Perc bietet Ihnen die Möglichkeit, den JIT-Compiler zu umgehen, indem es eine Ahead-of-Time (AOT)-Kompilierung anbietet. Mit Perc kann Bytecode zum Zeitpunkt der Erstellung statt zur Laufzeit kompiliert werden. - Thread-Planung:
Herkömmliche JVMs übergeben Threads einfach an das Betriebssystem und überlassen ihm die Entscheidung, wann sie ausgeführt werden. Obwohl Java-Threads eine Priorität zugewiesen wird, lässt sich nicht vorhersagen, wann oder in welcher Reihenfolge sie ausgeführt werden. Das Betriebssystem verfügt über eigene Scheduling-Algorithmen und versucht beispielsweise, die CPU-Zeit „fair“ auf der Grundlage der bisherigen CPU-Auslastung der Threads zu verteilen. Dies verstößt gegen Echtzeit-Designprinzipien, nach denen die Thread-Priorität oberste Priorität haben muss. Perc verfügt über einen eigenen Thread-Dispatcher, der Linux-Echtzeit-Scheduling-Richtlinien und CPU-Kernaffinität verwendet, um sicherzustellen, dass die N Threads mit der höchsten Priorität auf den N verfügbaren Kernen ausgeführt werden. Immer. Der Perc-Dispatcher implementiert außerdem das Priority Inheritance Protocol, um Prioritätsumkehrungen zwischen Java-Threads zu verhindern. Trad-JVMs tun nichts davon.
Wenn Ihnen das nächste Mal jemand erzählt, dass Java nicht in Echtzeit funktioniert, schicken Sie ihn zu uns. Wir freuen uns immer, das Verständnis der Menschen über WYSIATI hinaus zu erweitern.
Warum nicht C/C++ oder Rust?
Im Vergleich zu C/C++ verwendet Perc die Java-Sprache, die folgende Eigenschaften aufweist:
- Automatische Speicherbereinigung, wodurch der Programmierer von der Speicherverwaltung entlastet wird
- Speichersicherheit
- Typsicherheit wird bei der Kompilierung und Ausführung durchgesetzt (Cast-Prüfung)
- Ausnahmebehandlung
- Integrierte Thread-Verwaltung
- Thread-Sicherheit auf Sprachebene (synchronisierte Blöcke)
- Höhere Entwicklerproduktivität durch:
- All das oben Genannte (d. h. weniger Zeitaufwand für die Fehlersuche)
- Ein Ökosystem aus integrierten und Drittanbieter-Bibliotheken
Warum Java (und Perc) Rust übertrifft:
Rust bietet Speichersicherheit, hat jedoch seine eigenen Herausforderungen:
- Kein Garbage Collector: Rust verfügt über die Speichersicherheit, die C fehlt, hat jedoch seine eigenen Nachteile beim Aufbau großer Systeme. Es gibt keinen Garbage Collector und die Freigabe von nicht mehr benötigtem Speicher basiert auf der Objektzugehörigkeit. Ein Objekt kann immer nur einen Eigentümer haben. Wenn es den Gültigkeitsbereich verlässt, wird es freigegeben, aber der Speicherheap wird niemals defragmentiert. Bei einem System, das für einen längeren Betrieb ausgelegt ist, kann es vorkommen, dass der Speicher so stark fragmentiert ist, dass keine großen Zuweisungen mehr möglich sind. Rust-Entwickler empfehlen die Verwendung von Speicherpools oder die Zuweisung großer Objekte zu Beginn und deren Beibehaltung während der gesamten Laufzeit des Programms. Der Garbage Collector von Perc komprimiert Objekte im Heap-Speicher automatisch, ohne die Anwendung anzuhalten.
- Komplexe Besitzregeln: Um einen einzigen Besitzer durchzusetzen, hat der Rust-Compiler recht komplizierte Regeln, die für Entwickler frustrierend sein können. Er erlaubt das „Ausleihen“ von Objekten (mit dem Zeichen &, analog zu C++-Referenzen), aber eine ausgeliehene Referenz erlaubt es dem Entleiher nicht, das Objekt zu ändern, es sei denn, es wird „mut[able]“ gemacht. Und eine einzelne mutable Referenz kann nicht mit anderen (auch nicht mutablen) Referenzen koexistieren.
- Kleineres Ökosystem: Obwohl mit einem Wachstum im Laufe der Zeit zu rechnen ist, ist das Ökosystem von Rust-Bibliotheken („Crates“) derzeit noch klein und stammt größtenteils von Drittanbietern (was ein Risiko für Malware mit sich bringt). Das Ökosystem von Java ist riesig, da es auf 25 Jahre Entwicklung zurückblickt, und die Bibliotheken, die Teil der Java-Distribution sind, wurden gründlich auf Sicherheitslücken überprüft.
Für eine kleine Anwendung, die voraussichtlich nicht weit verbreitet sein wird, Objekte über lange Zeiträume hinweg zuweisen/freigeben oder in Zukunft stark verändern wird, sind diese Nachteile möglicherweise nicht von Bedeutung. Bei einem größeren System oder einem System, das langfristig erweitert werden soll, können sie jedoch zu einer unhaltbaren Situation führen.
Darüber hinaus enthält Perc Funktionen zum Schutz Ihrer Anwendung vor Hackern, die für den Schutz geistigen Eigentums oder in sicherheitssensiblen Umgebungen wichtig sein können.
Muss ich speziellen Code schreiben, um Perc auszuführen?
Nein. Perc ist eine Java Standard Edition JVM. Sie führt Standard-Java-SE-Code wie herkömmliche JVMs aus. Allerdings gibt es einige Coding-Praktiken, die das deterministische Verhalten in Perc verbessern. In einer technischen Hinweis werden diese näher beschrieben:
- Legen Sie die Thread-Prioritäten basierend auf der relativen Kritikalität jedes Threads fest. Belassen Sie sie nicht einfach auf der Standardpriorität Thread.NORMAL (5). Höhere Prioritäten sollten wirklich zeitkritischen Funktionen zugewiesen werden. Verwenden Sie Prioritäten über der GC-Priorität (Standard 6) für Threads, die den GC unterbrechen müssen.
- Vermeiden Sie „Jitter“ in der Standardklasse java.util.Timer, indem Sie eine Perc-spezifische Timer-API verwenden, die Ablaufzeiten in absoluter statt in relativer Zeit unterstützt.
- Vermeiden Sie Schleifen: Ersetzen Sie Thread.yield() durch Object.wait/notify oder einen Semaphore.
- Versuchen Sie, Java-Objekte wiederzuverwenden, anstatt sie in übermäßiger Zahl zu erstellen und zu verwerfen. Ein Beispiel hierfür ist die Verwendung eines StringBuilder zum Verknüpfen von Strings anstelle der „+“-Syntax. Verwenden Sie Pools aus vorab zugewiesenen Objekten, um Datenströme zwischen Anwendungsthreads zu verarbeiten. Verwenden Sie Thread-Pools, um Arbeitsabläufe gleichzeitig zu verarbeiten, anstatt Threads zu erstellen und zu verwerfen.
Muss ich alle meine Java-Klassen und Ressourcen ROMisieren?
Nein. Sie können Ihre Java-Anwendung mit einer Standardkonfiguration der Perc VM mit dynamischem Klassenladen und aktivierter JIT-Kompilierung wie eine herkömmliche JVM ausführen. Wir empfehlen Ihnen sogar, die Migration von einer herkömmlichen JVM zu Perc mit diesem Ansatz zu beginnen.
- Sie können Tools wie die Perc-Shell verwenden, um die CPU- und Speicherressourcennutzung zu überwachen.
- Um bessere Echtzeit-Latenzen zu erzielen, können Sie dann unseren technischen Hinweis befolgen, um die Perc VM-Optionen für Timing und Echtzeitrichtlinien und -prioritäten festzulegen.
- Erst dann empfehlen wir die Verwendung von ROMizer zum Paketieren und AOT-Kompilieren von Klassen und Ressourcen in eine angepasste Perc VM-Binärdatei.
Dazu haben wir auch einen technischen Hinweis.
Wie packe ich Dateien in eine Perc VM-Binärdatei?
Zusätzlich zur Verpackung von Java-Klassen und -Ressourcen in eine Perc VM-Binärdatei kann der ROMizer virtuelle Dateisysteme (VFS) mit Verzeichnissen und statischen Dateien in eine PVM-Binärdatei verpacken. Die Dateien werden als Objekte im Heap-Speicher extrahiert und sind zur Laufzeit über die Standardbibliotheken java.io und java.nio zugänglich. VFS-Dateien werden beim Start aus einem ROMized-Zip-Archiv initialisiert. Das Schreiben ist zwar zulässig, jedoch bleiben die von der Anwendung geschriebenen Daten nach einem Neustart der Perc VM nicht erhalten.
Weitere Informationen finden Sie in unserem technischen Hinweis zur Verwendung virtueller Dateisysteme mit Perc.
PTC Perc Echtzeit-Java
Echtzeit-Java für das Internet der Dinge (IoT), Edge-Geräte und eingebettete Systeme.
Mehr erfahren