メモリの原則に関する詳細な説明: JS では変数はヒープに保存されるのか、スタックに保存されるのか?

メモリの原則に関する詳細な説明: JS では変数はヒープに保存されるのか、スタックに保存されるのか?

JavaScriptではプリミティブ型はヒープに保存されるのでしょうか、それともスタックに保存されるのでしょうか?

---- 非基本型の基本型

この質問を見ると、この質問はあまりにも基本的なものであり、これ以上基本的なことはできないと誰もが感じると思います。 Baidu で検索すると、基本型はスタックに格納され、参照型はヒープに格納されると言っている人がたくさんいることがわかります。

本当にそんなに簡単なのでしょうか?

1. 冷蔵庫に入りきらない象

次のコードを見てみましょう:

ここでは 67MiB の文字列を宣言していますが、文字列が実際にスタック上に存在していた場合、これを説明するのは困難です。結局のところ、v8 のデフォルトのスタック サイズは 984KiB です。絶対に救えない。

注: V8 では、オペレーティング システムによって、また時期によって、文字列サイズの制限が異なります。おおよその範囲は256MiB~1GiBです

node --v8-options | grep -B0 -A1 スタックサイズ


この時点で、疑問に思い始めていますか? Baidu からの回答が間違っていて、Google で検索する必要がある可能性はありますか?

何が起こっているのか見てみましょう。

2. シャドウクローン文字列

const BasicVarGen = 関数(){
    this.s1 = 'IAmString'
    this.s2 = 'IAmString'
}


a = 新しい BasicVarGen() とする
b = 新しい BasicVarGen() とする

ここでは、それぞれ 2 つの同一の文字列を含む 2 つの同一のオブジェクトを宣言します。

開発者ツールを使用すると、4 つの文字列を宣言したにもかかわらず、それらのメモリが同じアドレスを指していることがわかります。

注: chrome実際のアドレスを表示できません。ここでは抽象的なアドレスを示します。

これはどういう意味ですか? 4 つの文字列に参照アドレスが含まれていることがわかります。

なので、上記の記事の冷蔵庫に入りきらない象は簡単に説明できます。文字列はスタックに格納されるのではなく、別の場所に格納され、その場所のアドレスがスタックに格納されます。

そこで、文字列の 1 つの内容を変更してみましょう。

const BasicVarGen = 関数(){
    this.s0 = 'IAmString'
    this.s1 = 'IAmString'
}


a = 新しい BasicVarGen() とする
b = 新しい BasicVarGen() とする
デバッガ
a.s0 = '異なる文字列'
a.s2 = 'IAmString'

debugger前のメモリスナップショット

debugger後のメモリスナップショット

a.s0 の初期コンテンツは ' IAmString'であり、そのコンテンツを変更するとアドレスが変わることがわかります。

新しく追加された a.s2 の内容は ' IAmString'であり、そのアドレスは値が ' IAmString'である他の変数と一致しています。

文字列を宣言する場合:

  1. v8 には、すべての文字列をキャッシュするstringTableというhashmapがあります。V8 がコードを読み取って抽象構文木を変換する際、文字列に遭遇するたびに、その特性に基づいてhash値に変換し、 hashmapに挿入します。後で同じハッシュ値を持つ文字列に遭遇した場合、最初に比較のために取り出されます。一貫性がある場合は、新しい文字列クラスは生成されません。
  2. 文字列をキャッシュする場合、文字列に応じて異なるhash方法が使用されます。

では、整理してみましょう。文字列を作成すると、V8 はまずメモリ (ハッシュ テーブル) を検索して、すでに作成されている同一の文字列があるかどうかを確認します。存在する場合は、直接再利用されます。存在しない場合は、文字列を格納するための新しいメモリ領域が開かれ、そのアドレスが変数に割り当てられます。これが、添え字を使用して文字列を直接変更できない理由です。V8 の文字列は不変です。

jsの基本型コピーを例に、v8の実装ロジックと誰もが理解している従来のロジックを説明します(Yawen)

例:

var a = "刘潇洒"; // V8 は文字列を読み込んだ後、stringTable にアクセスして文字列が存在するかどうかを確認します。存在しない場合は、'刘潇洒' を hashTable に挿入し、'刘潇洒' の参照を
var b = a; // '刘潇洒' の参照を直接コピーします b = "谭雅文"; // stringTable にエントリがないか検索します


質問:

const BasicVarGen = 関数(){
    this.s0 = 'IAmString'
    this.s1 = 'IAmString'
}


a = 新しい BasicVarGen() とする
b = 新しい BasicVarGen() とする
デバッガ
a.s0 = '異なる文字列'
a.s2 = 'IAmString'


a.s3 = a.s2+a.s0; // 質問: 文字列の連結ではどのような操作が実行されますか?
a.s4 = a.s2+as

同じ内容の連結された文字列を 2 つ同時に適用します。

ご覧の通り内容は同じです。しかし、住所は同じではありません。さらに、住所の前の地図の説明も変更されました。

文字列が従来の方法 ( SeqStringなど) で連結される場合、連結操作の時間計算量は O(n) です。 Rope Structure (つまり、 ConsStringで使用されるデータ構造) を使用すると、連結にかかる時間を短縮できます。

これが文字列に当てはまる場合、他のプリミティブ型にも当てはまるでしょうか?

3. 実際に見た「奇妙なボール」

文字列について説明した後、V8 のもう 1 つの典型的な「基本型」であるoddBallについて見てみましょう。

oddBall typeから拡張

もう一つの小さな実験をしてみましょう:

上図に基本的なタイプがリストされており、アドレスは同じであることがわかります。値を割り当てると、その値もその場で再利用されます。 (そして、 oddBallから拡張されたこれらの基本型のアドレスは固定されています。つまり、V8 が初めて実行されるとき、これらの基本型を宣言したかどうかに関係なく、それらは作成されています。オブジェクトを宣言すると、その参照が割り当てられます。これは、基本型がスタックに割り当てられると言われる理由も説明しています。V8 では、@73 に格納される値は常に空の文字列であるため、v8 はこれらのアドレスを値自体として扱うことができます。)

確認するためにソースコードを見てみましょう。

さまざまなoddBallタイプのメソッドを生成すると、戻り値がアドレスであることがわかります

undefined変数に割り当てられると、実際にはアドレスが割り当てられます

getRootメソッド

オフセットが定義される場所

4. 紛らわしい数字

紛らわしい番号と言われる理由は、割り当て時と変更時のメモリ割り当ての仕組みがまだ解明されていないためです。 (メモリは動的です)

V8 では、数値はsmiheapNumberに分割されます。

smi -2³¹から2³¹-1(2³¹≈2*10⁹)の範囲の整数を直接保存します。

heapNumberは文字列に似ており、不変です。その範囲は、すべての非SMI数値です。

最下位ビットは、それがポインターであるかどうかを示すために使用されます。最下位ビットが 1 の場合、それはポインターです。

定数o = {
  x: 42, // スミ
  y: 4.2, // ヒープ番号
};


ox の 42 は Smi として扱われ、オブジェクト自体に直接格納されますが、oy の 4.2 は追加のメモリ エンティティに格納する必要があり、oy のオブジェクト ポインターはメモリ エンティティを指します。

32 ビット オペレーティング システムの場合は、32 ビットを使用して smi を表すのは理解できますが、64 ビット オペレーティング システムでは、smi の範囲も -2³¹ から 2³¹-1 (2³¹≈2*10⁹) になるのはなぜでしょうか。

ECMAScript標準では、 numberは 64 ビットの倍精度浮動小数点数として扱う必要があると規定されていますが、実際には、任意の数値を格納するために常に 64 ビットを使用するのは非常に非効率的です (スペースの非効率性、計算時間の非効率性smiが多くのビット操作を使用する)。そのため、 JavaScriptエンジンは、数値を格納するために常に 64 ビットを使用するわけではありません。監視できる数値のすべての外部機能が 64 ビット表現と一致している限り、エンジンは内部的に他のメモリ表現 (32 ビットなど) を使用できます。

定数サイクル制限 = 50000
console.time('ヒープ番号')
定数 foo = { x: 1.1 };
(i = 0; i < cycleLimit; ++i とします) {
// 追加の heapNumber インスタンスを作成します foo.x += 1; 
}
console.timeEnd('heapNumber') // 遅い   


コンソール時間('smi')
定数バー = { x: 1.0 };
(i = 0; i < cycleLimit; ++i とします) {
  バー.x += 1;
}
console.timeEnd('smi') // 高速

質問:

const BasicVarGen = 関数(){


    smi1 = 1 です
    .smi2 = 2 です
    this.heapNumber1 = 1.1
    this.heapNumber2 = 2.1
}
    foo = 新しい BasicVarGen() を作成します。
    bar = 新しい BasicVarGen() を作成します。
    
    デバッガ
    
    baz.obj1.heapNumber1++

数字では、単一の数字の値は変更されませんが、他の数字のアドレスが変更されます。

5. まとめ: 基本型はどこに存在するのか?

文字列:ヒープ内に存在し、スタック内の参照アドレスです。同じ文字列が存在する場合、参照アドレスは同じです。

数値:小さな整数はスタックに格納され、その他の型はヒープに格納されます。

その他のタイプ:エンジンが初期化されるときに一意のアドレスが割り当てられ、スタック内の変数には一意の参照が格納されます。

ここでは、基本的なタイプがどこに存在するかを大まかに説明するだけです。

これで、メモリの原理の詳細と、JS の変数がヒープまたはスタックに格納されるかどうかに関するこの記事は終了です。JS の変数がヒープまたはスタックに格納されるかどうかの詳細については、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • JS変数ストレージのディープコピーとシャローコピーの詳しい説明
  • JS 変数の昇格と関数の昇格の例の分析
  • js 変数、スコープ、メモリの詳細な説明
  • js変数とそのスコープの詳細な説明
  • JavaScript 変数の昇格についての簡単な説明
  • JavaScript 変数 Dom オブジェクトのすべてのプロパティ
  • JavaScript変数の基本的な使い方
  • JavaScript 変数宣言例の分析
  • Javascript 変数スコープとスコープチェーンの詳細な説明

<<:  Linuxコマンドをバックグラウンドで実行する方法

>>:  Mysql の読み取り/書き込み分離期限切れに対する一般的な解決策

推薦する

ローカルの Windows リモート デスクトップから Alibaba Cloud Ubuntu 16.04 サーバーに接続する方法

ローカル Windows リモート デスクトップが Alibaba Cloud Ubuntu 16....

Centos7にGitLabサーバーをインストールして展開する方法

私はここでCentOS 7 64ビットシステムを使用しています。CentOS 64ビットシステムを試...

Vue+EChartsは、中国の地図の描画と省の自動回転と強調表示を実現します。

目次成果を達成する完全なコード + 詳細なコメントまとめ成果を達成する完全なコード + 詳細なコメン...

MySQL トリガー: 複数のトリガー操作の作成例の分析

この記事では、例を使用して、MySQL で複数のトリガー操作を作成する方法について説明します。ご参考...

Ant Design Pro ログイン機能にグラフィック検証コード コンポーネントを統合する方法

序文:この記事では、Ant Design Proログイン機能にグラフィック検証コードコンポーネントを...

Fabric.js は DIY ポストカード機能を実装します

この記事では、DIYポストカード機能を実現するためのfabricjsの具体的なコードを参考までに共有...

フィールドの文字セットの違いによる MySQL のインデックス失敗の解決策

インデックスとは何ですか?なぜインデックスを作成するのですか?インデックスは、列に特定の値を持つ行を...

CSS と HTML とフロントエンド テクノロジーのレイヤー図

フロントエンドテクノロジー層 (写真は少し極端ですが、参考までに) Javascript と DOM...

さまざまな Tomcat ログと catalina.out ファイルのセグメンテーションの関係についての簡単な分析

Tomcatログの関係一枚の写真は千の言葉に値する! localhost.{yyyy-MM-dd}....

CSS3 疑似クラスセレクターの簡単なレビュー

序文CSS がフロントエンド開発の基本的なスキルであるならば、「セレクター」は基礎中の基礎です。これ...

CSSでプロセスナビゲーション効果を実現する(3つの方法)

CSS によりプロセスナビゲーション効果を実現します。具体的な内容は以下のとおりです。 ::tip...

Bツリーの削除プロセスの紹介

前回の記事 https://www.jb51.net/article/154157.htm では、B...

JavaScript の手ぶれ補正とスロットリングの説明

目次安定スロットリング要約する安定自動ドアは人を感知してドアを開け、5 秒間のカウントダウンを開始し...

React Native環境のインストールプロセス

react-native インストールプロセス1.npx react-native init Awe...

Linux自動ログイン例の説明

インターネット上には、expect を使用して自動ログインを実現するスクリプトが多数存在しますが、明...