>  기사  >  Java  >  Java 클래스 파일에 대한 지식 포인트는 무엇입니까?

Java 클래스 파일에 대한 지식 포인트는 무엇입니까?

王林
王林앞으로
2023-05-05 12:22:061071검색

클래스 파일 형식은 C 언어 구조와 유사한 의사 구조를 사용하여 데이터를 저장합니다. 이 의사 구조에는 "부호 없는 숫자"와 "테이블"이라는 두 가지 데이터 유형만 있습니다.

·부호 없는 숫자는 기본 데이터 유형으로 U1, u2, u4, u8은 각각 1바이트, 2바이트, 4바이트, 8바이트의 부호 없는 숫자를 나타냅니다. 숫자, 인덱스 참조, 정량적 값을 설명하는 데 사용할 수 있습니다. 또는 UTF-8로 인코딩된 문자열 값입니다.

·테이블은 여러 개의 부호 없는 숫자 또는 기타 테이블을 데이터 항목으로 구성한 복합 데이터 유형으로, 구별을 용이하게 하기 위해 모든 테이블 이름은 습관적으로 "_info"로 끝납니다. 테이블은 계층적 복합 구조 데이터를 설명하는 데 사용됩니다. 전체 클래스 파일은 본질적으로 테이블로 간주될 수 있습니다

Java 클래스 파일에 대한 지식 포인트는 무엇입니까?

각 클래스 파일의 처음 4바이트를 매직 넘버라고 하며, 유일한 기능은 이 파일이 파일인지 여부를 결정하는 것입니다. 가상 머신에서 허용할 수 있는 클래스 파일입니다. 클래스 파일뿐만 아니라 많은 파일 형식 표준에는 식별을 위해 매직 넘버를 사용하는 습관이 있습니다. 예를 들어 GIF 또는 JPEG와 같은 이미지 형식의 경우 파일 헤더에 매직 넘버가 있습니다.

클래스 파일의 매직 넘버는 매우 "로맨틱"하며 값은 0xCAFEBABE(커피 베이비?)입니다.

매직 넘버의 다음 4바이트는 클래스 파일의 버전 번호를 저장합니다: 5번째와 6번째 문자 섹션은 마이너 버전 번호(MinorVersion)이고, 7번째와 8번째 바이트는 메이저 버전 번호(Major Version)입니다. Java의 버전 번호는 45부터 시작됩니다. JDK 1.1 이후 출시된 JDK 주요 버전의 기본 버전 번호는 1씩 증가합니다. (JDK 1.0~1.1은 버전 번호 45.0~45.3을 사용합니다.) JDK의 상위 버전은 이전 버전과 호환될 수 있습니다. . 버전의 클래스 파일이지만 이후 버전의 클래스 파일을 실행할 수 없습니다.

Constant pool

메이저 및 마이너 버전 번호 바로 뒤에는 상수 풀 항목이 리소스 웨어하우스와 비교될 수 ​​있습니다. Class 파일 구조에서 다른 항목과 가장 많이 연관되는 데이터는 일반적으로 Class 파일에서 가장 큰 공간을 차지하는 데이터 항목 중 하나이며, 또한 첫 번째 테이블 유형이기도 합니다. 클래스 파일에 나타나는 데이터 항목입니다.

상수 풀에 저장되는 상수에는 리터럴과 기호 참조라는 두 가지 주요 유형이 있습니다. 리터럴은 텍스트 문자열, 최종으로 선언된 상수 값 등 Java 언어 수준의 상수 개념에 더 가깝습니다. 기호 참조는 컴파일 원칙의 개념에 속하며 주로 다음 유형의 상수를 포함합니다.

· 모듈에서 내보내거나 여는 패키지

· 클래스 및 인터페이스의 정규화된 이름

· 필드 이름 및 설명자(Descriptor)

·메소드 이름 및 설명자

·메서드 핸들 및 메소드 유형(메소드 핸들, 메소드 유형, 동적 호출)

·동적 계산 호출 사이트, 동적 계산 상수)

Java 코드가 Javac로 컴파일될 때 C 및 C++와 같은 "연결" 단계가 있습니다. 대신 가상 머신이 클래스 파일을 로드할 때 동적 연결을 수행합니다(자세한 내용은 7장 참조). 즉, 메모리에 있는 각 메소드와 필드의 최종 레이아웃 정보는 클래스 파일에 저장되지 않습니다. 이러한 필드와 메소드의 기호 참조가 런타임 시 가상 머신에 의해 변환되지 않으면 실제 메모리 항목 주소를 변경할 수 없습니다. 즉, 가상 머신에서 직접 사용할 수 없습니다. 가상 머신이 클래스를 로드할 때 상수 풀에서 해당 기호 참조를 얻은 다음 클래스가 생성되거나 런타임될 때 이를 특정 메모리 주소로 구문 분석하고 변환합니다.

상수 풀의 각 상수는 테이블입니다. 처음에는 상수 테이블에 구조가 다른 11가지의 테이블 구조 데이터가 있었지만 나중에 동적 언어 호출을 더 잘 지원하기 위해 4개의 동적 언어 관련 상수가 추가되었습니다. [1] Java 모듈러 시스템(Jigsaw)을 지원하기 위해 CONSTANT_Module_info와 CONSTANT_Package_info라는 두 가지 상수가 추가되었습니다. 따라서 JDK13 기준으로 상수 테이블에는 17가지 유형의 상수가 있습니다.

Java 클래스 파일에 대한 지식 포인트는 무엇입니까?

그런데 클래스 파일의 메소드와 필드는 이름을 설명하기 위해 CONSTANT_Utf8_info 상수를 참조해야 하므로 CONSTANT_Utf8_info 상수의 최대 길이는 Java의 메소드 및 필드 이름의 최대 길이이기도 합니다. 여기서 최대 길이는 length의 최대값으로, u2 타입이 표현할 수 있는 최대값은 65535이다. 따라서 Java 프로그램에서 영문 64KB를 초과하는 변수나 메소드 이름을 정의하면 해당 규칙과 모든 문자가 적법하더라도 컴파일되지 않습니다.

Classfile /D:/BaiduYunDownload/geekbang-lessons/thinking-in-spring/validation/target/classes/org/geekbang/thinking/in/spring/validation/TestClass.class

최종 수정일 2020-6-25; 크기 439바이트

MD5 체크섬 18760ee8065f9fb68d4dab7bd7450c4c

"TestClass.java"

public class org.geekbang.thinking.in.spring.validation.TestClass

에서 컴파일됨 마이너 버전: 0

메이저 버전: 52

플래그: ACC_PUBLIC, ACC_SUPER

상수 풀:

   #1 = Methodref          #4.#18         // java/lang/Object."":()V

   #2 = Fieldref          #3.#19         // org/geekbang/thinking/in/spring /validation/TestClass.m:I

   #3 = 클래스              #20           // org/geekbang/thinking/in/spring/validation/TestClass

   #4 = 클래스             #21           // 언어/객체

   #5 = Utf8              m

   #6 = Utf8              I

   #7 = Utf8             

   #8 = Utf8              ()V

   #9 = Utf8               Code

  #10 = Utf8              LineNumberTable

  #11 = Utf8               LocalVariableTable

  #12 = Utf8              this

  #13 = Utf8              Lorg/geekbang/thinking/in/spring/validation/TestClass;

  #14 = Utf8             inc

  #15 = Utf8               ()I

  #16 = Utf8              SourceFile

  #17 = Utf8              TestClass.java

  #18 = NameAndType        #7:#8         // "":()V

  #19 = NameAndType        #5: #6          // m:I

#20 = Utf8              org/geekbang/thinking/in/spring/validation/TestClass

  #21 = Utf8              java/lang/Object

{

  public org.geekbang.thinking.in.spring.validation.TestClass();

    설명자: ()V

    플래그: ACC_PUBLIC

    코드:

      stack=1, locals=1, args_size=1

         0: aload_0

         1: Invokespecial #1                  // 메소드 java/lang/Object. "":()V

         4: return

      LineNumberTable:

        3행: 0

      LocalVariableTable:

        시작  길이  슬롯 이름   서명

            0       5     0  이것   Lorg/geekbang/thinking/in/ spring/validation/TestClass;

  public int inc();

    설명자: ()I

    플래그: ACC_PUBLIC

    코드:

      stack=2, locals=1

         0: aload_0

1 : getfield #2 // 필드 M : I

4 : ICONST_1

5 : IADD

6 : IRETURN

linEnebertable :

줄 7 : 0

LocalVariableTable :

시작 길이 슬롯 이름 서명 0   7 0 this Lorg/geekbang/thinking/in/spring/validation/TestClass;

}

SourceFile: "TestClass.java"

상수 풀이 끝난 후 다음 2바이트는 액세스 플래그(access_flags)를 나타냅니다. ), 이 플래그는 다음을 포함하여 일부 클래스 또는 인터페이스 수준 액세스 정보를 식별하는 데 사용됩니다. class, final로 선언되었는지 여부

클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스 세트는 모두 액세스 플래그 뒤에 순서대로 정렬됩니다. 클래스 인덱스와 부모 클래스 인덱스는 각각 두 개의 u2 유형 인덱스 값으로 표시됩니다. CONSTANT_Class_info를 통해 CONSTANT_Class_info 유형의 클래스 설명자 상수를 가리킵니다. 유형의 상수에 있는 인덱스 값은 CONSTANT_Utf8_info 유형의 상수에 정의된 정규화된 이름 문자열에서 찾을 수 있습니다.

JDK 8에서 Lambda 표현식과 인터페이스 기본 메서드가 등장한 후에야 InvokeDynamic 명령어가 Java 언어로 생성된 클래스 파일에서 작동하게 되었습니다.

그래서 JDK 8에 추가된 이 속성은 컴파일러가

(컴파일 시 -parameters 매개변수 추가) 메소드 이름도 Class 파일에 기록되며, MethodParameters는 메소드 테이블의 속성으로 Code 속성과 동일한 수준이며 에서 리플렉션 API를 통해 얻을 수 있습니다. 실행 시간.

·피연산자 스택에 지역 변수 로드: iload

·피연산자 스택의 값을 지역 변수 테이블에 저장: istore

·피연산자 스택에 상수 로드: bipush

iload_, It iload_0, iload_1, iload_2 및 iload_3 명령어를 나타냅니다.

·덧셈 명령어: iadd, ladd, fadd, Dadd

·뺄셈 명령어: isub, lsub, fsub, dsub

·곱셈 명령어: imul, lmul, fmul, dmul

·나눗셈 명령어: idiv, ldiv, fdiv, ddiv

·나머지 명령어: irem, lrem, frem, drem

·부정 명령어: ineg, lneg, fneg, dneg

·배치 명령어: ishl, ishr, iushr, lshl, lshr, lushr

·비트 OR 명령어: ior, lor

·비트 AND 명령어: iand, land

·비트 XOR 명령어: ixor, lxor

·로컬 변수 자동 증가 명령어: iinc

·비교 지침: dcmpg, dcmpl, fcmpg, fcmpl, lcmp

JDK 1.0.2에서는 Invokespecial 명령의 의미가 변경되었습니다. JDK 7에서는 Invokedynamic 명령이 추가되고 ret 및 jsr 명령이 금지되었습니다.

클래스 수명주기

로딩-> 연결(확인, 준비, 구문 분석)->초기화->사용->제거. 로드, 확인, 준비, 초기화 및 언로드의 5단계 순서가 결정됩니다. 유형의 로드 프로세스는 이 순서대로 단계별로 시작해야 하지만 구문 분석 단계는 반드시 그런 것은 아닙니다. 일부에서는 초기화될 수 있습니다. 이는 Java 언어의 런타임 바인딩 기능(동적 바인딩 또는 후기 바인딩이라고도 함)을 지원하기 위한 것입니다.

public static final int value = 123;

컴파일 시 Javac은 준비 단계에서 값에 대한 ConstantValue 속성을 생성하며 가상 머신은 Con-stantValue 설정에 따라 값을 123에 할당합니다.

상위 위임 모델의 작동 프로세스는 다음과 같습니다. 클래스 로더가 클래스 로딩 요청을 받으면 먼저 클래스 자체를 로드하려고 시도하지 않고 각 수준에서 완료되도록 요청을 상위 클래스 로더에 위임합니다. 모든 클래스 로더에 대해 true이므로 모든 로드 요청은 결국 최상위 시작 클래스 로더로 전송되어야 합니다. 상위 로더가 로드 요청을 완료할 수 없다고 피드백하는 경우에만(필요한 파일이 해당 검색 범위에서 찾을 수 없음) 클래스) , 서브로더는 스스로 로딩을 완료하려고 시도합니다

우선 확장 클래스 로더(Extension Class Loader)가 플랫폼 클래스 로더(Platform Class Loader)로 대체됩니다. 이는 실제로 매우 논리적인 변화입니다. 전체 JDK가 모듈성을 기반으로 구축되었으므로(원본 rt.jar 및 tools.jar은 수십 개의 JMOD 파일로 분할됨) 확장 가능한 요구 사항에는 당연히 Java 클래스 라이브러리가 충분합니다. libext 디렉토리를 유지할 필요가 없습니다. JDK 기능을 확장하기 위해 이 디렉토리 또는 java.ext.dirs 시스템 변수를 사용하는 이전 메커니즘은 더 이상 값이 없으며 이 클래스 라이브러리를 로드하는 데 사용됩니다. 또한 역사적 임무를 완수했습니다.

메서드의 실행 버전을 결정하기 위해 정적 유형에 의존하는 모든 디스패치 작업을 정적 디스패치라고 합니다. 정적 디스패치의 가장 일반적인 적용은 메서드 오버로딩입니다. 정적 디스패치는 컴파일 단계에서 발생하므로 정적 디스패치를 ​​결정하는 작업은 가상 머신에서 실제로 수행되지 않습니다. 이것이 바로 일부 자료가 이를 "디스패치"가 아닌 "파싱"으로 분류하는 이유입니다.

Java Virtual Machine은 바이트코드 명령어를 호출하기 위해 다음과 같은 5가지 메서드를 지원합니다.

·invokestatic. 정적 메서드를 호출하는 데 사용됩니다.

·invokespecial. 인스턴스 생성자 () 메서드, 전용 메서드 및 상위 클래스의 메서드를 호출하는 데 사용됩니다.

·invokevirtual. 모든 가상 메서드를 호출하는 데 사용됩니다.

·invoke인터페이스. 인터페이스 메소드를 호출하는 데 사용되며 인터페이스를 구현하는 객체는 런타임에 결정됩니다.

·invokedynamic. 먼저 호출 사이트 한정자가 참조하는 메서드가 런타임에 동적으로 확인된 다음 해당 메서드가 실행됩니다. 처음 4개 호출 명령어의 디스패치 로직은 JVM(Java Virtual Machine) 내부에 고정되어 있는 반면, Invokedynamic 명령어의 디스패치 로직은 사용자가 설정한 부팅 방법에 따라 결정됩니다.

invokestatic 및 informspecial 명령으로 메소드를 호출할 수 있는 한, 고유한 호출 버전은 구문 분석 단계에서 결정될 수 있습니다. Java 언어에는 이 조건을 충족하는 네 가지 메소드(정적 메소드, 개인 메소드, 인스턴스 생성자)가 있습니다. 및 부모 클래스 메서드 final로 수정된 메서드(invokevirtual 명령어를 사용하여 호출됨)와 결합하면 이러한 5개 메서드 호출은 클래스가 로드될 때 메서드에 대한 직접 참조에 대한 기호 참조를 확인합니다. 이들 방법을 통칭하여 "Non-Virtual Method"(Non-Virtual Method)라고 하고, 반대로 다른 방법을 "Virtual Method"(Virtual Method)라고 합니다.

파싱 호출은 컴파일 중에 완전히 결정되는 정적 프로세스여야 합니다. 클래스 로딩의 파싱 단계 중에 관련된 모든 기호 참조는 명확한 직접 참조로 변환되며 런타임까지 지연할 필요가 없습니다. 다른 주요 메서드 호출 형식: 디스패치(Dispatch) 호출은 훨씬 더 복잡합니다. 디스패치에 따른 경우의 수에 따라 단일 디스패치와 다중 디스패치로 나눌 수 있습니다[1]. 이 두 가지 유형의 디스패치 방법의 조합은 정적 단일 디스패치, 정적 다중 디스패치, 동적 단일 디스패치 및 동적 다중 디스패치의 네 가지 디스패치 조합을 구성합니다. 가상 머신에서 메소드 디스패치가 어떻게 수행되는지 살펴보겠습니다.

코드는 의도적으로 정적 유형은 같지만 실제 유형이 다른 두 개의 변수를 정의하지만, 가상 머신(또는 정확히 말하면 컴파일러)은

판단 기준을 오버로드할 때 실제 유형 대신

정적 유형 매개변수를 로 전달합니다. 정적 유형은 컴파일 타임에 알려지기 때문에 컴파일 단계에서 Javac 컴파일러는 매개변수의 정적 유형을 기반으로 사용할 오버로드된 버전을 결정하고 호출 대상으로 sayHello(Human)를 선택하고 기호를 작성합니다. 이 메서드의 참조를 main() 메서드에 있는 두 개의 Invokevirtual 명령어 매개변수에 추가합니다.

정적 유형을 사용하여 메서드의 실행 버전을 결정하는 모든 디스패치 작업을 정적 디스패치라고 합니다.

정적 디스패치의 가장 일반적인 응용 프로그램은 메서드 오버로딩입니다. 정적 디스패치는 컴파일 단계에서 발생하므로 정적 디스패치를 ​​결정하는 작업은 가상 머신에서 실제로 수행되지 않습니다. 이것이 바로 일부 자료가 이를 "디스패치"가 아닌 "파싱"으로 분류하는 이유입니다. 가변 길이 매개변수의 오버로드 우선순위가 가장 낮다는 것을 알 수 있습니다. 필드는 다형성에 절대 참여하지 않습니다. 클래스의 메서드가 특정 이름을 가진 필드에 액세스하면 해당 이름은 클래스가 볼 수 있는 필드를 나타냅니다.

핵심 포인트

invokevirtual 명령어 실행의 첫 번째 단계는 런타임에 수신자의 실제 유형을 결정하는 것이므로 정확하게 두 호출의 Invokevirtual 명령어는 기호 참조를 확인하지 않습니다. 메소드 버전은 메소드 수신자의 실제 유형을 기반으로 선택됩니다. 이 프로세스는 Java 언어의 메소드 재작성에서 핵심입니다. 동적 디스패치라고 하는 런타임 시 실제 유형을 기반으로 메서드 실행 버전을 결정하는 이 디스패치 프로세스를 호출합니다. 다형성의 근원은 가상 메서드 호출 명령인 Invokevirtual의 실행 논리에 있습니다. 당연히 우리가 도출하는 결론은 필드가 아닌 메서드에만 유효합니다. 왜냐하면 필드는 이 명령을 사용하지 않기 때문입니다.

Java 언어는 정적 다중 디스패치 언어와 동적 단일 디스패치 언어입니다.

프로그램 구현의 편의를 위해 동일한 시그니처를 갖는 메소드는 상위 클래스와 하위 클래스의 가상 메소드 테이블에서 동일한 인덱스 번호를 가져야 합니다. 이렇게 유형이 변경되면 가상 메소드 테이블만 보입니다. up을 변경해야 하며, 필요한 항목 주소는 다른 가상 메서드 테이블의 인덱스에 따라 변환됩니다. 가상 메소드 테이블은 일반적으로 클래스 로딩의 연결 단계에서 초기화되며, 가상 머신은 클래스 변수의 초기값을 준비한 후 클래스의 가상 메소드 테이블도 초기화합니다.

동적 유형 언어 지원

Java 가상 머신의 바이트코드 명령어 세트 수 Sun의 첫 번째 Java 가상 머신 출시 이후 20여년 동안 단 하나의 새로운 명령어만 등장했습니다. 이는 JDK 7 출시와 함께 나온 최초의 바이트코드 명령어 세트입니다. 새 멤버 - 동적 명령을 호출합니다. 새로 추가된 이 지침은 JDK 7의 프로젝트 목표인 동적 유형 언어(Dynamicically Typed Language) 지원을 달성하기 위해 개선된 사항 중 하나입니다. 또한 JDK 8에서 Lambda 표현식을 원활하게 구현하기 위한 기술적 예비 사항이기도 합니다.

동적 유형 언어란 무엇입니까 [1]? 동적 유형 언어의 주요 특징은 유형 검사의 주요 프로세스가 컴파일 타임이 아닌 런타임에 수행된다는 것입니다. 일반적으로 사용되는 언어에는 APL, Clojure, Erlang, Groovy, javaScript가 있습니다. , Lisp, Lua, PHP, Prolog, Python, Ruby, Smalltalk, Tcl 등. 이에 비해 C++, Java 등 컴파일 중에 유형 검사를 수행하는 언어는 가장 일반적으로 사용되는 정적인 유형 언어입니다. 변수에는 유형이 없지만 변수 값에만 유형이 있습니다

Java 가상 머신 수준에서 동적 유형에 대한 직접 지원을 제공하는 것이 Java 플랫폼 개발을 위해 해결해야 할 문제가 되었습니다. 이것이 바로 Invokedynamic 명령어와 Java입니다. JDK 7의 JSR-292 제안에 포함된 .lang .invoke 패키지 출현에 대한 기술적 배경.

JDK 7에 새로 추가된 java.lang.invoke 패키지 [1]는 JSR 292의 중요한 부분입니다. 이 패키지의 주요 목적은 대상 메소드를 결정하기 위해 단순히 기호 참조에 의존하는 이전 경로를 대체하는 것입니다. 또한 "메소드 핸들"이라는 대상 메서드를 동적으로 결정하는 새로운 메커니즘이 제공됩니다.

·Reflection 및 MethodHandle 메커니즘은 기본적으로 메서드 호출을 시뮬레이션하지만 Reflection은 Java 코드 수준에서 메서드 호출을 시뮬레이션하는 반면 MethodHandle은 바이트코드 수준에서 메서드 호출을 시뮬레이션합니다.

Tomcat 디렉터리 구조에서는 3개의 디렉터리 그룹(/common/*, /server/* 및 /shared/*)을 설정할 수 있지만 기본적으로 반드시 열릴 필요는 없으며 /lib/* 디렉터리만 열 수 있습니다. 존재) Java 클래스 라이브러리를 저장하기 위한 웹 애플리케이션 자체의 "/WEB-INF/*" 디렉토리 외에 총 4개의 그룹이 있습니다. Java 클래스 라이브러리를 이 네 가지 디렉토리 그룹에 배치합니다. 각 그룹은 다음과 같은 독립적인 의미를 갖습니다.

·/common 디렉토리에 배치합니다. 클래스 라이브러리는 Tomcat 및 모든 웹 애플리케이션에서 사용할 수 있습니다.

·/server 디렉토리에 넣으세요. 클래스 라이브러리는 Tomcat에서 사용할 수 있으며 모든 웹 애플리케이션에는 표시되지 않습니다.

·/shared 디렉토리에 넣으세요. 클래스 라이브러리는 모든 웹 애플리케이션에서 사용할 수 있지만 Tomcat 자체에는 표시되지 않습니다.

·/WebApp/WEB-INF 디렉토리에 배치하세요. 클래스 라이브러리는 웹 애플리케이션에서만 사용할 수 있으며 Tomcat이나 다른 웹 애플리케이션에는 표시되지 않습니다.

이 디렉토리 구조를 지원하고 디렉토리의 클래스 라이브러리를 로드 및 격리하기 위해 Tomcat은 고전적인 상위 위임 모델에 따라 구현되는 여러 클래스 로더를 사용자 정의했습니다.

Java 클래스 파일에 대한 지식 포인트는 무엇입니까?

Common 클래스 로더, Catalina 클래스 로더( 서버 클래스 로더라고도 함), 공유 클래스 로더 및 Webapp 클래스 로더는 각각 shared/* 및 /WebApp/WEB-INF/에 /common/*, /server/*, /를 로드하는 Tomcat 고유의 클래스 로더입니다. *. 일반적으로 WebApp 클래스 로더와 JSP 클래스 로더의 인스턴스는 여러 개 있습니다. 각 웹 애플리케이션은 WebApp 클래스 로더에 해당하고 각 JSP 파일은 JasperLoader 클래스 로더에 해당합니다.

JasperLoader의 로딩 범위는 이 JSP 파일로 컴파일된 클래스 파일일 뿐입니다. 존재 목적은 폐기되는 것입니다. 서버가 JSP 파일이 수정되었음을 감지하면 현재 JasperLoader 인스턴스를 교체하고 HotSwap을 구현합니다. 새로운 JSP 클래스 로더를 생성하여 JSP 파일의 기능을 수행합니다.

위 내용은 Java 클래스 파일에 대한 지식 포인트는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제