メモリの原則に関する詳細な説明: 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 の読み取り/書き込み分離期限切れに対する一般的な解決策

推薦する

MySQLの基本的な共通コマンドの概要

目次MySQL の基本的な共通コマンド1. SQL文2. テーブルを作成する3. フィールドのプロパ...

HTML+CSS+JavaScript でガールフレンド版のスクラッチ カードを作成します (一度見ればすぐに覚えられます)

誰もがスクラッチ チケットで遊んだことがあると思います。子供の頃、ポケットにお金が入るとすぐに友達に...

Windows 10にWSL2 Ubuntu20.04をインストールしてdocker環境を構築する方法

WSLを有効にするシステムがWindows 10 2004以降であることを確認してください 「メニュ...

MySQL のインデックス障害の一般的なシナリオと回避方法

序文これまでにも、一部の SQL ステートメントを不適切に使用すると MySQL インデックスが失敗...

MySQL 5.7.17 winx64 無料インストールバージョン設定方法グラフィックチュートリアル

mysql5.7.17無料インストールバージョンのインストールに関する最近の経験1.ダウンロードして...

HTML チュートリアル: 画像のサイズ、配置、間隔、境界線の属性を変更する方法

画像タグ: <img> ページに画像を挿入するには、「src」属性を持つ「img」タグを...

MySQL 8.0.24 バージョンのインストールと設定方法のグラフィックチュートリアル

この記事ではMySQL 8.0.24バージョンのインストールと設定方法を記録し、皆さんと共有しますM...

MySQLデータ復旧のさまざまな方法の概要

目次1. はじめに2. 直接回復2.1 mysqldumpバックアップの完全リカバリ2.2 xtra...

MySQLデータベースを使い始めるための最初のステップはテーブルを作成することです

データベースを作成する右クリック - 新しいデータベースを作成ライブラリ名を入力し、文字セットと並べ...

HTMLデータ送信投稿_PowerNode Java Academy

HTTP/1.1 プロトコルで指定されている HTTP リクエスト メソッドには、OPTIONS、...

MySQL 完全崩壊: クエリフィルタ条件の詳細な説明

概要実際のビジネス シナリオ アプリケーションでは、ビジネス条件に基づいて対象データを取得およびフィ...

Dockerコンテナがホストポートにアクセスできない場合の解決策

最近、仕事中に問題が発生しました。Docker コンテナがホストの redis にアクセスできず、t...

Vueのv-onパラメータの問題についてお話しましょう

Vue での v-on:clock の使用現在、vue.js フレームワークを学習しています。後で参...

Docker で Jenkins-2.249.3-1.1 をインストールする詳細な手順

目次1. Dockerをインストールする2. Jenkinsイメージファイルを取得する3. マウント...

MySQL 5.7 でルートパスワードを忘れた後に変更する方法の詳細なチュートリアル

序文長い間、MySQL のアプリケーションおよび学習環境は MySQL 5.6 以前のバージョンであ...