blog

3.実行時データ領域[PCレジスタ、仮想マシンスタック、ローカルメソッドスタック]。

Javaプログラムの実行中、Java仮想マシンはすべてのメモリをいくつかの異なるデータ領域に分割します。これらのデータ領域は、仮想マシンの起動時やユーザースレッドの起動時、終了時など、それぞれの目的や...

Nov 14, 2020 · 17 min. read
シェア

概要

Javaプログラムの実行中、Java仮想マシンはすべてのメモリをいくつかの異なるデータ領域に分割します。これらの領域は、それぞれの目的と、その作成と破棄の時間を持っており、ある領域は仮想マシンの開始とともに作成され、ある領域はユーザーのスレッドの開始と終了とともに作成と破棄が行われます。Java仮想マシン仕様の定義によると、以下の6つの領域が定義されています。

PCレジスタまたはプログラム・カウンタ

説明

プログラム・カウンタは、現在のスレッドで実行されているバイトコードの行番号インジケータと考えることができる、メモリ空間の小さな部分です。分岐、ループ、ジャンプ、例外処理、スレッド回復などの基本的な機能は、このカウンタに依存して完了します。

  • 現在のスレッドが実行したバイトコードの行番号インジケータが格納されます。
  • 各スレッドは個別のプログラム・カウンタを持っており、スレッド同士が影響し合うことはありません。
  • スレッドが Java メソッドを実行している場合、カウンタには実行中の仮想マシン・バイトコードの命令のアドレスが記録されます。
  • スレッドがネイティブ・メソッドを実行している場合、カウンターの値は NULL です。
  • ランタイム・データ領域の中で唯一OOMの対象とならず、ゴミも収集されない領域。

コードデモ

CPU

CPUタイムスライスとは、CPUが個々のプログラムに割り当てる時間のことで、各スレッドにタイムスロットが割り当てられています。タイムスライスと呼ばれます。

マクロレベルでは、複数のアプリケーションを同時に開くことができ、各プログラムは同時に並行して実行されます。

しかし、ミクロレベルでは、CPUは1つしかないため、一度に処理できるのはプログラムの要求の一部だけであり、公平性にどう対処するかですが、1つの方法として、タイムスライスを導入し、各プログラムが順番に実行されるようにします。

並列性と並行性

並列性:複数のスレッドが同時に実行されること;

並行性:1つのコアが複数のスレッドを順番に実行するように素早く切り替えること。

仮想マシンスタック

説明

Java 仮想マシン・スタックはスレッド・プライベートで、スレッドと同じライフサイクルを持ちます。仮想マシン・スタックは、Java メソッド実行のメモリ・モデルを記述します。各メソッドは、ローカル変数テーブル、操作スタック、ダイナミック・リンク、およびメソッド・エクスポートを格納するスタック・フレームを作成することによって実行されます。メソッドが呼び出されてから実行が完了するまでのプロセスは、スタック・フレームを仮想マシン・スタックのイン・スタックからアウト・スタックに移動するプロセスに相当します。

  • java仮想マシン・スタックは、初期にはJavaスタックとも呼ばれていました。 各スレッドは、仮想マシン・スタック、スタック・フレームの内部ストレージの作成時に作成され、この1回のJavaメソッド呼び出しに対応します。それはスレッドプライベートです。
  • Javaプログラムの実行を監督し、メソッドのローカル変数、結果の一部を保持し、メソッドの呼び出しとリターンに参加します。
  • ライフサイクルはスレッドと同じです。

メモリ内のヒープとスタック

  • スタックはプログラムを実行する、つまりプログラムがどのように実行される か、あるいはどのようにデータを扱うかという問題を解決します。ヒープはデータ*の保存、つまりデータをどこにどのように置くかという問題を解決します。
  • 一般的に言って、オブジェクトは主にヒープ空間に置かれます。
  • 基本的なデータ型のローカル変数、および参照データ型のオブジェクトへの参照を格納するためのスタック空間。

スタック操作

  • スタックは、PC レジスタに次いで高速で効率的なストレージ割り当て方法です。
  • JVMがJavaスタックで直接実行する操作は2つだけです。
    • 各メソッドの実行は、スタック・エントリーを伴います。
    • すべてのメソッド実行はスタック・エントリーを伴います。
  • スタックにはゴミ収集の問題はありません。

スタックの OOM

Java VM仕様では、Javaスタックのサイズを動的または固定にすることができます。

  • 固定サイズの Java VM スタックが使用される場合、各スレッドの Java VM スタック容量は、スレッド生成時に独立して選択できます。スレッドが最大許容スタック・サイズ以上を要求すると、Java仮想マシンはStackOverFlowError例外をスローします。
  • Java 仮想マシン・スタックが動的に拡張可能で、拡張しようとしたときに十分なメモリーを要求できないか、新しいスレッドを作成するときに対応する仮想マシン・スタックを作成するのに十分なメモリーがない場合、Java 仮想マシンは OutOfMemoryError 例外をスローします。
/**
* デモスタックの例外
*/
public class StackErrorTest {
 public static void main(String[] args) {
 main(args);
 }
}

スタック・サイズを設定するパラメータ

スタックのサイズは、関数呼び出しの到達可能な最大深度を直接決定します。

/**
 * デモスタックの例外
 *
 * デフォルト:count 10818
 * スタックのサイズを設定する: -Xss256k count 1872
 */
public class StackErrorTest {
 private static int count = 1;
 public static void main(String[] args) {
 System.out.println(count);
 count++;
 main(args);
 }
}

スタック内部と実行

スタックの説明

  • 各スレッドは独自のスタックを持ち、スタックフレームの形でデータを格納します。
  • スレッド内で実行される各メソッドは、それ自身のスタックフレームを持っています。
  • スタックフレームはメモリのブロックであり、メソッドの実行プロセスに関する情報を保持するデータの集合です。
  • JVMがJavaスタック上で直接行う操作は2つだけで、それはスタックフレーム上でのスタックプレスとスタックエグジットで、先入れ先出し/後入れ先出しと原則に従います。
  • アクティブなスレッドでは、ある時点で、アクティブなスタックフレームは1つだけです。つまり、現在実行中のメソッドのスタックフレームだけが有効で、このスタックフレームは現在のスタックフレームと呼ばれ、現在のスタックフレームに対応するメソッドが現在のメソッドです。
  • 実行エンジンによって実行されるすべてのバイトコード命令は、現在のスタックフレーム上でのみ動作します。
  • このメソッド内で別のメソッドが呼び出されると、新しいスタックフレームが作成されてスタックの先頭に置かれ、新しいカレントスタックフレームになります。
  • 異なるスレッドに含まれるスタックフレームは互いに参照することができません。つまり、別のスタックフレーム内で別のスレッドのスタックフレームを参照することはできません。
  • 現在のメソッドが別のメソッドを呼び出す場合、メソッドが戻ると、現在のスタックフレームはこのメソッドの結果を前のスタックフレームに戻し、仮想マシンは現在のスタックフレームを破棄して、前のスタックフレームを再び現在のスタックフレームにします。
  • Javaメソッドには、関数に戻る2つの方法があります。1つはreturn命令を使った通常の関数リターン、もう1つは例外をスローする方法です。どちらの方法を使っても、スタック・フレームは排出されます。
/**
 *  
 */
public class StackFrameTest {
 public static void main(String[] args) {
 StackFrameTest test = new StackFrameTest();
 test.method1();
 //出力メソッド1は現在のスタックフレームとして2回表示され、メソッド3()は1回表示される。
// method1()実装を開始する。
// method2()実装を開始する。
// method3()実装を開始する。
// method3()実装を終了する。
// method2()実装を終了する。
// method1()実装を終了する。
 }
 public void method1(){
 System.out.println("method1()実装を開始する");
 method2();
 System.out.println("method1()実装終了");
 }
 public int method2(){
 System.out.println("method2()実装を開始する");
 int i = 10;
 int m = (int) method3();
 System.out.println("method2()実装終了");
 return i+m;
 }
 public double method3(){
 System.out.println("method3()実装を開始する");
 double j = 20.0;
 System.out.println("method3()実装を終了する");
 return j;
 }
}

スタックフレームの内部構造

各スタックフレームに格納されているもの
  • ローカル変数の表
  • オペランドスタック
  • 動的リンク
  • メソッドのリターンアドレス
  • いくつかの追加情報

ローカル変数の表

説明

  • ローカル変数テーブル (ローカル変数配列またはローカル変数テーブルとも呼ばれます) は、メソッド内で定義されたメソッド・パラメータとローカル変数を格納するために使用される数値の配列として定義されます。
  • これらのデータ型には、さまざまな基本データ型、オブジェクト参照、および returnAddressleixing が含まれます。
  • ローカル変数テーブルはスレッドのスタック上に構築されるため、スレッドプライベートデータであり、データセキュリティ上の問題はありません。
  • ローカル変数テーブルの必要なサイズはコンパイル時に決定され、メソッドの Code プロパティの最大ローカル変数データ項目に格納されます。ローカル変数テーブルのサイズは、メソッドの実行中に変更されることはありません。
  • ネストされたメソッド呼び出しの数は、スタックのサイズによって決まります。一般に、スタックが大きいほど、ネストされたメソッド呼び出しが多くなります。関数の場合、引数やローカル変数が多ければ多いほど、ローカル変数テーブルが拡張され、メソッド呼び出しに渡す必要がある情報が増えるため、スタックフレームが大きくなります。その結果、関数呼び出しのスタック領域が大きくなり、ネストした呼び出しが少なくなります。
  • ローカル変数テーブルの変数は、現在のメソッド呼び出しに対してのみ有効です。メソッドの実行中、仮想マシンはローカル変数テーブルを使用してパラメータ変数リストにパラメータ値を渡す処理を完了します。メソッド呼び出しが終了すると、ローカル変数テーブルはメソッド・スタック・フレームとともに破棄されます。

変数スロット・スロットの理解とデモンストレーション

  • パラメータ値は常に、index0 から始まり配列長 1 のインデックスで終わるローカル変数配列に格納されます。
  • ローカル変数テーブル、最も基本的な記憶単位はスロットです。
  • ローカル変数テーブルには、コンパイル時に既知のさまざまな基本データ型、参照型、returnAddress型の変数が格納されます。

    Byte、short、char、floatは格納前にintに変換され、booleanもintに変換されます(0はfalse、0以外はtrue);

    longとdoubleは2つのスロットを占有します。
  • Longとdoubleは2つのスロットを占有します。ローカル変数テーブルの64ビットのローカル変数の値にアクセスする必要がある場合は、符号の1つのインデックスを使用するだけです。
  • コンストラクタまたはインスタンスメソッドによってカレントフレームが作成される場合、オブジェクト参照 this はインデックス 0 のスロットに格納され、残りのパラメータはパラメータリストの順番に並べられます。残りのパラメータは、パラメータリストの順にリストされます。
public class LocalVariablesTest {
 private int count = 1;
 //静的メソッドは thisを使用できない。
 public static void testStatic(){
 //this "変数が現在のメソッドのローカル変数テーブルに存在しないため、コンパイルエラーとなる!
 System.out.println(this.count);
 }
}

slotメソッドの再利用は

スタックフレーム内のローカル変数のテーブルのスロットは再利用することができます。 ローカル変数がスコープ外にある場合、そのスコープの後に宣言された新しいローカル変数は、スコープ外のローカル変数のスロットを再利用する可能性が高く、リソースを節約することができます。

private void test2() {
 int a = 0;
 {
 int b = 0;
 b = a+1;
 }
 //変数cの使用と、破壊された変数bが占めるスロット位置
 int c = a+1;
 }

スタティック変数とローカル変数の比較とまとめ

  • データ型によると
    • 基本データ型;
    • データ型を参照します;
  • クラス内で宣言される場所による分類:
    • メンバ変数: 使用前にデフォルトの初期化代入が行われます。
      • static修飾子:クラス変数:クラス変数デフォルト代入への準備段階をリンクするクラスロード→静的コードブロック代入であるクラス変数明示的代入への初期化段階;
      • インスタンス変数:オブジェクトの生成に伴い、インスタンス変数空間がヒープ空間に割り当てられ、デフォルトで値が割り当てられます。
    • ローカル変数:使用する前に明示的に代入する必要がある変数!そうしないとコンパイルに失敗します。
    • スタックフレームの中でパフォーマンスチューニングに最も密接に関係する部分は、ローカル変数テーブルです。メソッドの実行中、仮想マシンはローカル変数テーブルを使用してメソッドのパスを完了します。
    • ローカル変数テーブルで直接または間接的に参照されるオブジェクトがリサイクルされない限り、ローカル変数テーブルの変数も重要なゴミ収集ルートノードです。

オペランドスタック

  • 個々のスタックフレームには、ローカル変数テーブルに加えて、式スタックとも呼ばれる LIFO オペランドスタックがあります。
  • オペランドスタックは、メソッドの実行中に、バイトコード命令に従ってスタックにデータを書き込んだり、スタックからデータを取り出したりします。

概要

  • Java 仮想マシンの解釈エンジンはスタック・ベースの実行エンジンで、スタックはオペランド・スタックです。
  • スタックは、主に計算処理の中間結果を格納するために使用され、計算処理中の変数の一時的な格納領域としても機能します。
  • メソッドの実行が開始されると、新しいスタック・フレームが作成され、このメソッドのオペランド・スタックは空になります。
  • 各オペランド・スタックには、値を格納するための明示的なスタック深度があり、最大深度はコンパイル時に定義されます。
  • スタックは、32 ビット型では 1 スタック・ユニット深さ、64 ビット型では 2 スタック・ユニット深さを占めます。
  • オペランド・スタックは、データ・アクセスのためのインデックス付けはされませんが、標準的なスタック・インおよびスタック・アウト操作によって一度だけアクセスすることができます。
  • メソッドが戻り値で呼び出された場合、戻り値は現在のスタックフレームのオペランドスタックに押し込まれ、PC レジスタは次に実行されるバイトコード命令で更新されます。
  • 上の図と次の図を組み合わせると、メソッドの実行過程がわかります。

    (1) 15 がスタックに追加され、(2) 15 が格納され、15 がローカル変数テーブルに入力されます。
スタックに 8 を入れます;
ローカル変数テーブルからインデックス 1 と 2 のデータを取り出してオペランドスタックに置く ⑥ iadd 和演算でスタックから 8 と 15 を取り出す
(iadd 演算の結果、23 がスタックに入ります。

i++ ++i

i++ : i はまず使用されなくなり、次に +1 されます;

++i : 最初に i+1 を取り出し、次に取り出して使用します;

トップ・オブ・スタック・キャッシング技法 ToS

  • スタック・ベースのVMで使用されるゼロ・アドレス命令はよりコンパクトですが、演算を完了させるためにはより多くの入出力命令が必要となり、これは命令ディスパッチとメモリの読み書きが増えることを意味します。
  • オペランドはメモリに格納されるため、頻繁なメモリリード/ライトは実行速度に影響します。この問題を解決するため、HotSpot JVMの設計者は、スタック最上位要素をすべてハウス内のCPUレジスタにキャッシュするトップオブスタックキャッシング技術を提案し、メモリへのリード/ライトの回数を減らし、エピデミックの実行効率を向上させました。

動的リンク

概要

  • 各スタック・フレームは内部的に、実行時定数のプールまたはスタック・フレームが属するメソッドへの参照を含んでいます。この参照は、現在のメソッドをサポートするコードを動的にリンクできるように含まれています。例えば、invokedynamic ディレクティブは次のようになります。
  • Java ソース・ファイルがバイトコード・ファイルにコンパイルされるとき、すべての変数とメソッドの参照は、クラス・ファイルの定数プールにシンボリック参照として格納されます。例えば、別のメソッドを呼び出すメソッドを記述する場合、定数プール内のメソッドへのシンボリック参照によって表現され、動的リンクの役割は、これらのシンボリック参照を呼び出しメソッドへの直接参照に変換することです。

メソッド呼び出し

  • メソッド呼び出しの唯一のタスクは、呼び出されるメソッドのバージョンを決定することであり、メソッドの内部動作 の詳細にはまだ関与しません。
  • クラス・ファイルのコンパイル・プロセスには、従来のコンパイルのリンク・ステップは含まれず、すべてのメソッド呼び出しは、メソッドの実際の実行時メモリ・レイアウトのエントリ・アドレスとしてではなく、シンボリック参照としてクラス・ファイルに格納されます。これにより、Javaは強力な動的スケーラビリティを持つことができますが、Javaのメソッド呼び出しのプロセスが比較的複雑になり、クラス・ロード時や実行時にもターゲット・メソッドへの直接参照を決定する必要があります。

JVMでは、呼び出しメソッドへの直接参照へのシンボリック参照の変換は、メソッドのバインディング・メカニズムに関連しています。

  • 静的リンク:呼び出しのターゲット・メソッドがコンパイル時にわかっていて、実行時に変更されない場合。
  • 動的リンク:プログラムの実行時にのみ、直接参照のメソッドへのシンボリック参照を呼び出すことができるようになり、この参照変換プロセスの動的な性質のため、それはまた、動的リンクとして知られています。

対応するメソッドバインディングのメカニズムは、アーリーバインディングとレイトバインディングです。

  • 事前バインディング:早期バインディングとは、呼び出されるターゲット・メソッドがコンパイル時にわかっていて、実行時に変更されない場合、そのメソッドをそのメソッドが属する型にバインドできることを意味します。 この方法では、呼び出されるターゲット・メソッドが明確なので、静的リンクを使用してシンボリック参照を直接参照に変換できます。
  • 後期バインディング:呼び出されるメソッドをコンパイル時に決定できず、メソッドを実行時に実際の型にバインドすることしかできない場合、このバインディング方法も後期バインディングと呼ばれます。

Voidメソッドと非仮想メソッド

  • 非仮想メソッド:
    • 非仮想メソッド:メソッドがコンパイラによって決定された特定の呼び出しバージョンを持つ場合、このバージョンは実行時に不変です。このようなメソッドは非仮想メソッドと呼ばれます。
    • スタティック・メソッド、プライベート・メソッド、ファイナル・メソッド、インスタンス・コンストラクタ、親メソッドは非仮想メソッドです。
  • その他のメソッドは仮想メソッドと呼ばれます。

Java仮想マシンによって呼び出されるバイトコード命令とは?

  • invokestatic静的メソッドの呼び出し
  • invokespecial: インスタンスのコンストラクタ・メソッド、プライベート・メソッド、親メソッドを呼び出します。
  • invokevirtual:すべての仮想メソッドを呼び出します。
  • invokeinterfaceインターフェイスメソッドの呼び出し
  • invokedynamic : 呼び出されるメソッドを動的に解決してから実行します。
  • invokestatic ディレクティブと invokespecial ディレクティブによって呼び出される メソッドを非仮想メソッド、それ以外を仮想メソッドと呼びます。

Javaでinvokedynamicディレクティブの生成が簡単になったのは、Java 8でLambda式が登場してからです。

/**
 * 非仮想メソッドのテスト、構文解析呼び出しの仮想メソッド
 */
class Father {
 public Father(){
 System.out.println("Fatherデフォルトコンストラクタ");
 }
 public static void showStatic(String s){
 System.out.println("Father show static"+s);
 }
 public final void showFinal(){
 System.out.println("Father show final");
 }
 public void showCommon(){
 System.out.println("Father show common");
 }
}
public class Son extends Father{
 public Son(){
 super();
 }
 public Son(int age){
 this();
 }
 public static void main(String[] args) {
 Son son = new Son();
 son.show();
 }
 //静的メソッドはオーバーライドできないので、オーバーライドされない親クラスのメソッド。
 public static void showStatic(String s){
 System.out.println("Son show static"+s);
 }
 private void showPrivate(String s){
 System.out.println("Son show private"+s);
 }
 public void show(){
 //invokestatic
 showStatic(" bighead");
 //invokestatic
 super.showStatic(" bighead");
 //invokespecial
 showPrivate(" hello!");
 //invokespecial
 super.showCommon();
 //invokevirtual このメソッドはfinalと宣言され、サブクラスでオーバーライドできないため、非仮想メソッドとみなされる。
 showFinal();
 //仮想メソッドは以下の通りである。
 //invokevirtual
 showCommon();//サブクラスがshowCommonをオーバーライドする可能性があるため、仮想メソッドとみなされるsuperを明示的に追加しない。
 info();
 MethodInterface in = null;
 //invokeinterface どのインターフェイス実装クラスかわからない 書き換えが必要
 in.methodA();
 }
 public void info(){
 }
}
interface MethodInterface {
 void methodA();
}

invokedynamic ディレクティブについて

  • JVMバイトコード命令セットは、java7が動的型言語のサポートを実現するためにJavaであるinvokedynamic命令を追加し、一種の改良を行うまで、比較的安定していました。
  • しかし、java7はinvokedynamic命令を生成する直接的な方法を提供しておらず、invokedynamic命令を生成するにはASMの基礎となるバイトコード・ツールを使用する必要があります。Java8のラムダ式の出現まで、javaのinvokedynamic命令の生成は、直接生成メソッドを持っています。
  • 動的な言語型のサポートの追加でJava7は、Java仮想マシンの仕様変更の本質ではなく、Java言語のルールに変更され、この作品は比較的、より複雑な仮想マシン内の増加メソッドコールを言えば、最も直接的な受益者は、コンパイラの動的言語のJava資格情報で実行されています。

動的型付け言語と静的型付け言語

  • 動的型付け言語と静的型付け言語の違いは、型チェックがコンパイル時に行われるか、実行時に行われるかにあります。
  • Javaは静的型付け言語、jsやpythonは動的型付け言語です。

メソッド書き換えの性質

  • 1.オペランドスタックの最初の要素が実行されるオブジェクトの実際の型を求めます。
  • 2.単純な名前で定数の記述と一致するC型のメソッドが見つかれば、アクセス権チェックが行われ、パスすればメソッドへの直接参照が返され、検索処理は完了します。パスしなければ、java.lang.IllegalAccessError例外が返されます。
  • 3.そうでなければ、cの各親クラスの下から上への継承関係に従って、順番に検索と検証のプロセスの第二ステップのために。
  • 4.適切なメソッドが見つからなかった場合、java.lang.AbstractMethodError例外がスローされます。IllegalAccessError の紹介 プログラムのビューで、アクセス権限のないプロパティやメソッドにアクセスしたり、変更したり、メソッド を呼び出したりします。通常、これはコンパイラ例外を発生させます。このエラーが実行時に発生する場合は、クラスに対する互換性のない変更を示しています。

仮想メソッドの表

  • オブジェクト指向プログラミングは、動的割り当ての非常に頻繁な使用になります、各動的割り当てプロセスは、適切なターゲット内のクラスメソッドメタデータで再検索する必要がある場合、それは実行の効率に影響を与える可能性がありますので、パフォーマンスを向上させるために、JVMは、ルックアップの代わりにインデックステーブルを使用して、仮想メソッドテーブルを作成するクラスのメソッド領域を使用します。
  • 各クラスには仮想メソッド・テーブルがあり、各メソッドの実際のエントリーを保持しています。
  • では、仮想メソッド・テーブルはいつ作成されるのでしょうか? クラス変数の初期値が準備された後、JVMはクラスのメソッドテーブルも初期化します。

メソッドのリターンアドレス

  • メソッドが呼び出された pc レジスタの値を格納します。
  • メソッドの終了: 1 通常の実行が完了 2 未処理の例外、異常終了
  • どの終了方法であっても、メソッドの終了はメソッドが呼び出された場所を返します。通常のメソッド終了の場合、呼び出し元の PC カウンタの値がリターン・アドレス、つまりメソッドを呼び出した命令の次の命令のアドレスとして使用されます。
  • 異常終了の場合、リターンアドレスは例外テーブルから決定され、通常はスタックフレームには格納されません。
  • 基本的に、メソッドの終了は、現在のスタックフレームをスタックから取り出す処理です。この時点で、上位メソッドのローカル変数テーブル、オペランドスタックを復元し、戻り値も呼び出し元のスタックフレームのオペランドスタックとして設定し、PCレジスタ値を設定するなどして、呼び出し元のメソッドが実行を継続するようにする必要があります。
  • 通常の完了終了と例外完了終了の違いは、例外完了終了による終了は、上位の呼び出し元に対して戻り値を生成しないことです。
  • 実行エンジンは、任意のメソッドによって返されたバイトコード命令に遭遇し、正常終了と呼ばれる戻り値を上位レベルのメソッド呼び出し元に渡します。
    • ireturn戻り値は、boolean、byte、char、short、int型で
    • areturn 参照型
    • void として宣言されたメソッド、インスタンス初期化メソッド、クラスおよびインターフェイス初期化メソッドにも戻り値参照があります。

いくつかの追加情報

プログラムのデバッグをサポートする情報など、Java仮想マシン実装に関連するいくつかの追加情報を持つことが許可されています。持つかどうかは不明、オプションの場合

Read next

webpackを書く

問題があれば答えを見つけなければなりませんし、問題がなければ答えを見つける必要はありません。 ご覧のように、bable変換の後にはexports["default"]があります。これにより、レイヤーごとに再帰を実行し続けることができます。

Nov 14, 2020 · 1 min read