Heim  >  Artikel  >  Java  >  Detaillierte Erklärung der String-Klasse in Java

Detaillierte Erklärung der String-Klasse in Java

Y2J
Y2JOriginal
2017-05-04 09:48:281841Durchsuche

Dieser Artikel stellt hauptsächlich die detaillierte Erklärung der Java-String-Klasse vor. Dieser Artikel wurde aus vielen Quellen gesammelt und schließlich in einen Artikel geschrieben Wer es braucht, kann sich darauf beziehen

Einführungsfrage

Unter allen Datentypen in der Java-Sprache ist der Der String-Typ ist ein besonderer Typ und ein häufig gestellter Wissenspunkt bei der Verwendung der Java-Speicherzuweisung. Dieser Artikel kombiniert die Java-Speicherzuweisung mit einer detaillierten Analyse vieler verwirrender Probleme im Zusammenhang mit String. Im Folgenden werden einige Probleme aufgeführt, die in diesem Artikel behandelt werden. Wenn der Leser mit diesen Problemen vertraut ist, können Sie diesen Artikel ignorieren.

1. Auf welchen Speicher bezieht sich Java-Speicher konkret? Warum sollte dieser Speicherbereich geteilt werden? Wie ist es aufgeteilt? Welche Rolle spielen die einzelnen Bereiche nach der Teilung? Wie stelle ich die Größe jedes Bereichs ein?

2. Warum ist der String-Typ beim Durchführen von Verbindungsvorgängen weniger effizient als StringBuffer oder StringBuilder? Was sind die Zusammenhänge und Unterschiede zwischen StringBuffer und StringBuilder?

3. Was bedeuten Konstanten in Java? Was ist der Unterschied zwischen String s = „s“ und String s = new String(“s“)?

Dieser Artikel wurde aus verschiedenen Quellen zusammengestellt und schließlich geschrieben. Wenn es Fehler gibt, lassen Sie es mich bitte wissen!

Java-Speicherzuweisung

1. JVMEinführung

Java Die virtuelle Maschine (Java Virtual Machine, kurz JVM) ist ein abstrakter Computer, auf dem alle Java-Programme ausgeführt werden. Sie ist eine der attraktivsten Funktionen von Java. Die Java Virtual Machine verfügt über eine eigene vollständige Hardware-Architektur, wie Prozessor, Stack, Register etc., und verfügt auch über ein entsprechendes Befehlssystem. Die JVM schirmt die Informationen ab, die sich auf die spezifische Betriebssystemplattform beziehen, sodass das Java-Programm nur den Zielcode (Bytecode) generieren muss, der auf der virtuellen Java-Maschine ausgeführt wird, und ohne Änderungen auf einer Vielzahl von Plattformen ausgeführt werden kann.

Die Hauptaufgabe einer Java Virtual Machine-Laufzeitinstanz besteht darin, für die Ausführung eines Java-Programms verantwortlich zu sein. Wenn ein Java-Programm gestartet wird, wird eine Instanz einer virtuellen Maschine geboren. Wenn das Programm geschlossen und beendet wird , stirbt auch die Instanz der virtuellen Maschine. Wenn drei Java-Programme gleichzeitig auf demselben Computer ausgeführt werden, werden drei Java Virtual Machine-Instanzen erhalten. Jedes Java-Programm wird in einer eigenen Java Virtual Machine-Instanz ausgeführt.

Wie in der folgenden Abbildung dargestellt, umfasst die JVM-Architektur mehrere wichtige Subsysteme und Speicherbereiche:

Recyceln Sie nicht verwendete Objekte im Heap-Speicher (Heap), das heißt, diese Objekte sind nicht mehr referenziert.                                                                                                                                                     Classloader-Subsystemspeicher und Hilfe beim Auflösen von Symbolverweisen.

Execution Engine (Execution Engine): Verantwortlich für die Ausführung von Anweisungen, die in den Methoden der geladenen Klasse enthalten sind.

                                                                         Laufzeitdatenbereich (Java Memory Allocation Area): Wird als Speicher der virtuellen Maschine oder Java-Speicher bezeichnet. Wenn die virtuelle Maschine ausgeführt wird, muss sie einen Speicherbereich vom gesamten Computerspeicher trennen, um viele davon zu speichern Dinge. Zum Beispiel: Bytecode, andere aus geladenen Klassendateien erhaltene Informationen, vom Programm erstellte Objekte, an Methoden übergebene Parameter, Rückgabewerte, lokale Variablen usw.

2. Java-Speicherpartition

Wie wir aus dem vorherigen Abschnitt wissen, ist der Laufzeitdatenbereich Java-Speicher, und der Im Datenbereich müssen viele Dinge gespeichert werden. Wenn dieser Speicherbereich nicht aufgeteilt und verwaltet wird, erscheint er unorganisierter. Programme mögen geordnete Dinge und hassen unorganisierte Dinge. Abhängig von den verschiedenen gespeicherten Daten ist der Java-Speicher normalerweise in fünf Bereiche unterteilt: Programmzählregister, nativer Stapel, Methodenbereich, Stapel und Heap.

Programmzähler (Program Count Register): Auch Programmregister genannt. Die JVM unterstützt mehrere Threads, die gleichzeitig ausgeführt werden. Wenn jeder neue Thread erstellt wird, erhält er ein eigenes PC-Register (Programmzähler). Wenn der Thread eine Java-Methode (nicht nativ) ausführt, zeigt der Wert des PC-Registers immer auf die nächste auszuführende Anweisung. Wenn die Methode nativ ist, wird der Wert des Programmzählerregisters nicht definiert. Das Programmzählerregister der JVM ist breit genug, um eine Rücksprungadresse oder einen nativen Zeiger aufzunehmen.                                                                                              Die JVM weist jedem neu erstellten Thread einen Stapel zu. Mit anderen Worten: Für ein Java-Programm wird seine Operation durch die Operation des Stapels abgeschlossen. Der Stapel speichert den
Status

des Threads in Frames. Die JVM führt nur zwei Vorgänge auf dem Stapel aus: Push- und Pop-Vorgänge in Frame-Einheiten. Wir wissen, dass die von einem Thread ausgeführte Methode als aktuelle Methode dieses Threads bezeichnet wird. Wir wissen möglicherweise nicht, dass der von der aktuellen Methode verwendete Frame als aktueller Frame bezeichnet wird. Wenn ein Thread eine Java-Methode aktiviert, schiebt die JVM einen neuen Frame in den Java-Stack des Threads, und dieser Frame wird natürlich zum aktuellen Frame. Während der Ausführung dieser Methode wird dieser Frame zum Speichern von Parametern, lokalen Variablen, Zwischenberechnungen und anderen Daten verwendet. Aus Sicht des Java-Zuweisungsmechanismus kann der Stapel wie folgt verstanden werden: Der Stapel ist der Speicherbereich, den das Betriebssystem für diesen Thread erstellt, wenn es einen Prozess oder Thread erstellt (Thread in einem Betriebssystem, das Multithreading unterstützt). In diesem Bereich gilt das First-In-Last-Out-Prinzip. Zugehörige Einstellungsparameter:

• XSS – der Maximalwert des Einstellungsmethodenstapels Lokaler Methodenstapel : Speichert den Aufrufstatus der lokalen Methode.

   
Methodenbereich

(Methodenbereich): Wenn die virtuelle Maschine eine Klassendatei lädt, extrahiert sie die in der Klasse enthaltenen Binärdaten Datei aus Analysieren Sie die Typinformationen und fügen Sie dann die Typinformationen (einschließlich Klasseninformationen, Konstanten,

statische -Variablen usw.) in den Methodenbereich ein. Dieser Speicherbereich wird von allen Threads gemeinsam genutzt, wie in der Abbildung gezeigt Abbildung unten. Im lokalen Methodenbereich gibt es einen speziellen Speicherbereich namens Constant Pool. Dieser Speicher steht in engem Zusammenhang mit der Analyse des String-Typs.

     
Heap

(Heap): Java Heap (Java Heap) ist der größte Speicherbereich, der von der Java Virtual Machine verwaltet wird. Der Java-Heap ist ein Speicherbereich, der von allen Threads gemeinsam genutzt wird. Der einzige Zweck dieses Bereichs besteht darin, Objektinstanzen zu speichern. Fast alle Objektinstanzen weisen hier Speicher zu, die Referenz auf dieses Objekt wird jedoch auf dem Stapel zugewiesen. Daher muss beim Ausführen von String s = new String("s") Speicher an zwei Stellen zugewiesen werden: Speicher wird für das String-Objekt im Heap zugewiesen und

ist eine Referenz im Stapel (die Speicheradresse von Dieses Heap-Objekt (d. h. der Zeiger) weist Speicher zu, wie in der folgenden Abbildung dargestellt.

Die virtuelle JAVA-Maschine verfügt über eine Anweisung zum Zuweisen neuer Objekte im Heap, aber es gibt keine Anweisung zum Freigeben von Speicher, genauso wie Sie ein Objekt in Java nicht explizit freigeben können Codebereich. Die virtuelle Maschine selbst ist dafür verantwortlich, zu entscheiden, wie und wann der Speicher freigegeben wird, der von Objekten belegt wird, auf die nicht mehr von laufenden Programmen verwiesen wird. Normalerweise überlässt die virtuelle Maschine diese Aufgabe dem Garbage Collector (Garbage Collection). Zugehörige Einstellungsparameter:

• -Xms – Legen Sie die Anfangsgröße des Heap-Speichers fest

• -Xmx – Legen Sie die maximale Größe des Heap-Speichers fest

• -XX :MaxTenuringThreshold – Legen Sie fest, wie oft ein Objekt in der neuen Generation überlebt.

• -XX:PretenureSizeThreshold – Legen Sie fest, dass große Objekte, die die angegebene Größe überschreiten, direkt in der alten Generation zugewiesen werden sollen

Der Java-Heap ist der Hauptbereich, der vom Garbage Collector verwaltet wird, daher wird er auch „GC Heap“ (Garbage Collectioned Heap) genannt. Heutige Garbage Collectors verwenden im Wesentlichen Generationssammlungsalgorithmen, sodass der Java-Heap in junge Generation und alte Generation unterteilt werden kann, wie in der folgenden Abbildung dargestellt. Die Idee des Generationssammlungsalgorithmus: Die erste Möglichkeit besteht darin, junge Objekte (junge Generation) häufiger zu scannen und zu recyceln. Dies wird als kleinere Sammlung bezeichnet, während die Häufigkeit der Überprüfung und des Recyclings alter Objekte (alte Generation) geringer ist. Eine Menge, die als große Sammlung bezeichnet wird. Auf diese Weise ist es nicht erforderlich, jedes Mal, wenn GC verwendet wird, alle Objekte im Speicher zu überprüfen, um dem Anwendungssystem mehr Systemressourcen zur Verfügung zu stellen Speicher wird GC zuerst für die neue Generation (Young GC) durchgeführt. Wenn der GC der neuen Generation die Anforderungen an die Speicherplatzzuweisung immer noch nicht erfüllen kann, wird GC (Full GC) für den gesamten Heap-Speicherplatz und den Methodenbereich durchgeführt.

Einige Leser haben hier möglicherweise Fragen: Denken Sie daran, dass es eine permanente Generation (Permanent Generation) gibt. Gehört sie nicht zum Java-Heap? Lieber, du hast es richtig verstanden! Tatsächlich handelt es sich bei der legendären permanenten Generierung um den oben erwähnten Methodenbereich, in dem einige Typinformationen (einschließlich Klasseninformationen, Konstanten, statische Variablen usw.) gespeichert werden, die vom Lader geladen werden, wenn die JVM initialisiert wird. Der Lebenszyklus dieser Informationen ist relativ lang und GC nicht. Der PermGen-Speicherplatz wird während der Ausführung des Hauptprogramms bereinigt. Wenn Ihre Anwendung also viele Klassen enthält, ist es wahrscheinlich, dass PermGen-Speicherplatzfehler auftreten. Zugehörige Einstellungsparameter:

• -XX:PermSize – Legen Sie die Anfangsgröße des Perm-Bereichs fest

• -XX:MaxPermSize – Legen Sie den Maximalwert des Perm-Bereichs fest

          🎜>Die neue Generation (Junge Generation) ist unterteilt in: Eden-Bereich und Survivor-Bereich. Der Survivor-Bereich ist in From Space und To Space unterteilt. Der Eden-Bereich ist der Ort, an dem das Objekt zunächst zugewiesen wird. Standardmäßig sind die Bereiche „From Space“ und „To Space“ gleich groß. Wenn die JVM Minor GC durchführt, kopiert sie die überlebenden Objekte in Eden in den Survivor-Bereich und kopiert auch die überlebenden Objekte im Survivor-Bereich in den Tenured-Bereich. In diesem GC-Modus unterteilt die JVM Survivor in From Space und To Space, um die GC-Effizienz zu verbessern, sodass Objektrecycling und Objektförderung getrennt werden können. Es gibt zwei verwandte Parameter für die Größeneinstellung der neuen Generation:

• -Xmn – Legt die Speichergröße der neuen Generation fest.

• -XX:SurvivorRatio – Legen Sie das Größenverhältnis von Eden zu Survivor Space fest                              Der OLD-Bereich wird nach der vollständigen Müllabfuhr einer umfassenden Sammlung unterzogen, wenn die Survivor- und OLD-Bereiche immer noch keinen Müll speichern können Objekte, die aus Eden kopiert wurden, was dazu führt, dass die JVM keinen Speicherbereich für neue Objekte im Eden-Bereich erstellen kann, tritt ein „Fehler wegen nicht genügend Speicher“ auf.

3. Eingehende Analyse des String-Typs

Beginnen wir mit Java-Datentypen! Java-Datentypen werden im Allgemeinen in zwei Kategorien unterteilt (mit verschiedenen Klassifizierungsmethoden): Variablen von Basistypen enthalten primitive Werte, und Variablen von Referenztypen stellen normalerweise Verweise auf tatsächliche Objekte dar. Ihr Wert ist normalerweise die Speicheradresse von das Objekt.

1. Die Essenz von String


Öffnen Sie den Quellcode von String, es gibt eine solche Passage in der Klasse

Kommentar " Strings sind konstant; ihre Werte können nach ihrer Erstellung nicht geändert werden. Da String-Objekte unveränderlich sind, können sie gemeinsam genutzt werden. Dieser Satz fasst eines der wichtigsten Merkmale von String zusammen: String ist eine Konstante, deren Wert unveränderlich (unveränderlich) und threadsicher (gemeinsam nutzbar) ist.

Als nächstes verwendet die String-Klasse den letzten Modifikator, der das zweite Merkmal der String-Klasse angibt: Die String-Klasse kann nicht

vererbt werden.

Das Folgende ist die Mitgliedsvariablendefinition der String-Klasse, die verdeutlicht, dass der String-Wert aus der Implementierung der Klasse unveränderlich (unveränderlich) ist.  

 private final char value[];
  private final int count;

Daher schauen wir uns die Concat-Methode der String-Klasse an. Der erste Schritt zur Implementierung dieser Methode muss darin bestehen, die Kapazität des Mitgliedsvariablenwerts zu erweitern. Die Erweiterungsmethode definiert einen Zeichenarray-Buf mit großer Kapazität neu. Der zweite Schritt besteht darin, die Zeichen im ursprünglichen Wert nach buf zu kopieren und dann den Zeichenfolgenwert, der verkettet werden muss, nach buf zu kopieren. Auf diese Weise enthält buf den Zeichenfolgenwert nach concat. Hier ist der Schlüssel zum Problem. Wenn der Wert nicht endgültig ist, verweisen Sie ihn direkt auf buf und geben Sie ihn dann zurück. Sie müssen kein neues String-Objekt zurückgeben. Aber. . . Mitleid. . . Da der Wert endgültig ist, kann er nicht auf das neu definierte Array mit großer Kapazität verweisen. Was sollen wir tun? „return new String(0, count + otherLen, buf);“ ist die letzte Anweisung der Concat-Implementierungsmethode der String-Klasse und gibt ein neues String-Objekt zurück. Jetzt kommt die Wahrheit ans Licht!

Zusammenfassung: String ist im Wesentlichen ein Zeichenarray mit zwei Merkmalen: 1. Diese Klasse kann nicht vererbt werden. 2. Unveränderlich.

2. Die Definitionsmethode von String

Bevor wir die Definitionsmethode von String besprechen, wollen wir zunächst das Konzept des konstanten Pools verstehen wird bereits früher eingeführt. Lassen Sie uns eine leicht formale Definition geben.

Der Konstantenpool bezieht sich auf einige Daten, die während der Kompilierung ermittelt und in der kompilierten .class-Datei gespeichert werden. Es umfasst Konstanten in Klassen, Methoden, Schnittstellen usw. sowie Zeichenfolgenkonstanten. Der Konstantenpool ist ebenfalls dynamisch und neue Konstanten können zur Laufzeit in den Pool eingefügt werden. Die intern()-Methode der String-Klasse ist eine typische Anwendung dieser Funktion. Verstehst du nicht? Die Praktikantenmethode wird später vorgestellt. Die virtuelle Maschine verwaltet einen Konstantenpool für jeden geladenen Typ. Der Pool ist eine geordnete Sammlung von Konstanten, die vom Typ verwendet werden, einschließlich direkter Konstanten (String-, Integer- und Float-Konstanten) und symbolischer Verweise auf andere Typen, Felder und Methoden ( Was Ist der Unterschied zwischen ihm und der Objektreferenz? Leser können es selbst herausfinden.

Die Definitionsmethode für Zeichenfolgen wird insgesamt wie folgt zusammengefasst: • Verwenden Sie neue Schlüsselwörter, z. B.: string s1 = neue Zeichenfolge ("mystring" ("mystring") ) ;

· Direkte Definition, wie zum Beispiel: String s1 = „myString“;

· Reihengenerierung, wie zum Beispiel: String s1 = „my“ + „String“; . Ich werde hier nicht auf Details eingehen.

Die erste Möglichkeit besteht darin, den Prozess über das Schlüsselwort new zu definieren: Während der Programmkompilierung überprüft der Compiler zunächst den String-Konstantenpool, um festzustellen, ob „myString“ vorhanden ist. Öffnen Sie einen Speicher im Konstantenpool. Der Speicherplatz speichert „myString“. Wenn er vorhanden ist, muss der Speicherplatz nicht erneut geöffnet werden, um sicherzustellen, dass sich nur eine „myString“-Konstante im Konstantenpool befindet, wodurch Speicherplatz gespart wird. Öffnen Sie dann einen Platz im Speicherheap, um die neue String-Instanz zu speichern, und nennen Sie ihn „s1“. Der gespeicherte Wert ist die Speicheradresse der String-Instanz im Heap Referenz s1 auf die neue String-Instanz.

Alle zusammen, der unbestimmteste Teil ist angekommen! Welche Beziehung besteht zwischen der neuen Instanz im Heap und „myString“ im Konstantenpool? Wir werden dieses Problem erneut analysieren, nachdem wir die zweite Definitionsmethode analysiert haben.

Die zweite Möglichkeit besteht darin, den Prozess direkt zu definieren: Während der Programmkompilierung prüft der Compiler zunächst, ob „myString“ vorhanden ist. Wenn er nicht vorhanden ist, wird ein Speicher zugewiesen Im Konstantenpool wird „myString“ gespeichert. Wenn es vorhanden ist, muss der Speicherplatz nicht erneut geöffnet werden. Öffnen Sie dann einen Platz im Stapel, nennen Sie ihn „s1“ und speichern Sie den Wert als Speicheradresse von „myString“ im Konstantenpool.

Was ist der Unterschied zwischen String-Konstanten im Konstantenpool und String-Objekten im Heap? Warum kann ein direkt definierter String auch verschiedene Methoden des String-Objekts aufrufen?

Bei vielen Fragen werde ich mit Ihnen die Beziehung zwischen String-Objekten im Heap und String-Konstanten im Konstantenpool besprechen. Bitte denken Sie daran, dass dies nur eine Diskussion ist, da ich auch relativ vage bin dieses Thema.

       第一种猜想:因为直接定义的字符串也可以调用String对象的各种方法,那么可以认为其实在常量池中创建的也是一个String实例(对象)。String s1 = new String("myString");先在编译期的时候在常量池创建了一个String实例,然后clone了一个String实例存储在堆中,引用s1指向堆中的这个实例。此时,池中的实例没有被引用。当接着执行String s1 = "myString";时,因为池中已经存在“myString”的实例对象,则s1直接指向池中的实例对象;否则,在池中先创建一个实例对象,s1再指向它。如下图所示: 

       这种猜想认为:常量池中的字符串常量实质上是一个String实例,与堆中的String实例是克隆关系。

       第二种猜想也是目前网上阐述的最多的,但是思路都不清晰,有些问题解释不通。下面引用《JAVA String对象和字符串常量的关系解析》一段内容。

       在解析阶段,虚拟机发现字符串常量"myString",它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对( [myString], s1 )保存到内部字符串常量列表中。如下图所示: 

            如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。
           例如,String s2 = "myString",运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。

           这个猜想有一个比较明显的问题,红色字体标示的地方就是问题的所在。证明方式很简单,下面这段代码的执行结果,javaer都应该知道。          

String s1 = new String("myString");
  String s2 = "myString";
  System.out.println(s1 == s2); //按照上面的推测逻辑,那么打印的结果为true;而实际上真实的结果是false,因为s1指向的是堆中String对象,而s2指向的是常量池中的String常量。

           虽然这段内容不那么有说服力,但是文章提到了一个东西——字符串常量列表,它可能是解释这个问题的关键。

           文中提到的三个问题,本文仅仅给出了猜想,具体请自己考证!

• 堆中new出来的实例和常量池中的“myString”是什么关系呢?

• 常量池中的字符串常量与堆中的String对象有什么区别呢?

• 为什么直接定义的字符串同样可以调用String对象的各种方法呢?  

    3、String、StringBuffer、StringBuilder的联系与区别

        上面已经分析了String的本质了,下面简单说说StringBuffer和StringBuilder。

     StringBuffer和StringBuilder都继承了抽象类AbstractStringBuilder,这个抽象类和String一样也定义了char[] value和int count,但是与String类不同的是,它们没有final修饰符。因此得出结论:String、StringBuffer和StringBuilder在本质上都是字符数组,不同的是,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以这就是为什么在进行大量字符串连接运算时,不推荐使用String,而推荐StringBuffer和StringBuilder。那么,哪种情况使用StringBuffe?哪种情况使用StringBuilder呢?        

     关于StringBuffer和StringBuilder的区别,翻开它们的源码,下面贴出append()方法的实现。    

             

Das erste Bild oben ist die Implementierung der append()-Methode in StringBuffer und das zweite Bild ist die Implementierung von append() in StringBuilder. Der Unterschied sollte auf den ersten Blick klar sein. StringBuffer fügt vor der Methode eine synchronisierte Änderung hinzu, die eine Synchronisierungsrolle spielt und in einer Multithread-Umgebung verwendet werden kann. Der dafür gezahlte Preis ist eine verringerte Ausführungseffizienz. Wenn Sie StringBuffer für Zeichenfolgenverbindungsvorgänge in einer Multithread-Umgebung verwenden können, ist es daher effizienter, StringBuilder in einer Single-Thread-Umgebung zu verwenden.

Das obige ist der detaillierte Inhalt vonDetaillierte Erklärung der String-Klasse in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn