우리는 프로그래머로서 매일 코드를 작성하지만 라이프사이클을 제대로 이해하고 계신가요? 오늘은 Java 코드의 수명에 대해 간단히 이야기해보겠습니다. Java 코드는 탄생부터 게임 종료까지의 단계, 즉 컴파일, 클래스 로딩, 실행, GC로 크게 나눌 수 있습니다.
Java 언어의 컴파일 기간은 프론트엔드일 수도 있기 때문에 실제로는 "불확실한" 과정입니다. 컴파일러 .java 파일을 .class 파일로 변환하는 프로세스는 JVM의 백엔드 런타임 컴파일러(JIT 컴파일러)에 의해 바이트코드를 기계어 코드로 변환하는 프로세스를 의미할 수도 있습니다. static advance 컴파일러(AOT 컴파일러)는 .java 파일을 로컬 기계어 코드로 직접 컴파일합니다. 그러나 여기서는 첫 번째 범주에 대해 이야기하고 있습니다. 이는 또한 편집에 대한 대중의 이해와도 일치합니다. 이 기간 동안 편집은 어떤 과정을 거쳤나요?
어휘 분석은 소스 코드의 문자 스트림을 토큰 세트로 변환하는 것이고, 구문 분석은 이를 기반으로 구문 트리(ATS)를 추상적으로 구성하는 프로세스입니다. 토큰 시퀀스는 프로그램 코드의 구문 구조를 설명하는 데 사용되는 트리 표현입니다. 구문 트리의 각 노드는 패키지, 유형, 수정자, 연산자와 같은 프로그램 코드의 구문 구조를 나타냅니다. 인터페이스, 반환 값, 심지어 코드설명까지 구문 구조가 될 수 있습니다.
구문 및 어휘 분석이 완료되면 다음 단계는 기호 테이블에 등록된 정보가 다양한 단계에서 사용됩니다. 편집. 여기서 기호 테이블의 개념을 확장해 보겠습니다. 심볼 테이블이란 무엇입니까? 심볼 주소와 심볼 정보의 집합으로 구성된 테이블입니다. 가장 간단한 것은 해시 테이블의 K-V 값 쌍의 형태로 이해하면 됩니다. 심볼 테이블을 사용하는 이유는 무엇입니까? 기호 테이블의 초기 응용 중 하나는 프로그램 코드에 대한 정보를 구성하는 것이었습니다. 처음에 컴퓨터 프로그램은 단순한 숫자열에 불과했지만 프로그래머들은 곧 기호를 사용하여 연산과 메모리 주소(변수 이름)를 나타내는 것이 훨씬 더 편리하다는 것을 깨달았습니다. 이름과 숫자를 연결하려면 기호 테이블이 필요합니다. 프로그램이 성장함에 따라 기호 테이블 연산의 성능은 점차 프로그램 개발 효율성에 병목 현상이 됩니다. 이러한 이유로 시퀀스 번호 테이블의 효율성을 향상시키기 위해 많은 데이터 구조와 알고리즘이 탄생했습니다. 소위 데이터 구조와 알고리즘은 무엇입니까? 일반적으로 말하면: 순서가 지정되지 않은 연결 목록의 순차 검색, 순서 배열의 이진 검색, 이진 검색 트리, 균형 검색 트리(여기서는 주로 레드-블랙 트리와 접촉합니다), 해시 테이블(지퍼 방법 기반 해시) 목록, 해시 테이블 선형 프로빙 기반). Java의 java.util.TreeMap 및 java.util.HashMap과 마찬가지로 각각 레드-블랙 트리의 기호 테이블과 지퍼 해시 테이블을 기반으로 구현됩니다. 여기서 언급하는 심볼테이블의 개념은 자세히 설명하지 않겠습니다. 관심 있는 분들은 관련 정보를 찾아보시면 됩니다.
이전 두 단계 후에 우리는 프로그램 코드의 추상 구문 트리 표현을 얻었습니다. 구문 트리는 올바른 소스 코드 추상화를 나타낼 수 있지만, 예, 이때 의미론적 분석이 등장합니다. 주요 작업은 구조적으로 올바른 소스 프로그램의 상황에 맞는 특성을 검토하는 것입니다. 주석 확인, 데이터 및 제어 흐름 분석, 구문 설탕 디코딩은 의미 분석 단계의 여러 단계로, 여기서는 구문 설탕의 개념을 자세히 논의합니다. 구문 설탕은 컴퓨터 언어에 추가된 특정 구문을 의미합니다. 이 구문은 언어의 기능에 영향을 미치지 않지만 프로그래머가 사용하기 더 편리합니다. Java에서 가장 일반적으로 사용되는 구문 설탕은 제네릭, 가변 길이 매개변수, 셀프 박싱/언박싱 및 순회루프입니다. JVM은 런타임에 이러한 구문을 지원하지 않으며 컴파일 중에 간단한 기본으로 돌아갑니다. 단계. 문법 구조, 이 과정은 구문 설탕을 해결하는 것입니다. 일반 삭제의 예를 들면 List
바이트코드 생성은 Javac 컴파일 프로세스의 마지막 단계입니다. 이 단계에서는 이전 단계에서 생성된 정보가 바이트코드로 변환되어 디스크에 기록됩니다. 소량의 코드 추가 및 변환 작업이 완료되었습니다. 인스턴스 생성자
컴파일 프로그램을 바이트코드로 컴파일한 후 다음 단계는 클래스를 메모리에 로딩하는 과정입니다.
클래스 로딩 과정은 가상 머신 메모리를 포함하는 가상 머신 메모리의 메소드 영역에서 이루어지므로 여기서는 먼저 메모리 영역에서의 프로그램 배포 개념을 간략히 소개합니다. 가상 머신 메모리 영역은 프로그램 카운터, 스택, 로컬 메서드 스택, 힙, 메서드 영역(일부 영역은 런타임 상수 풀임) 및 직접 메모리로 구분됩니다.
프로그램 카운터는 작은 메모리 공간으로 현재 스레드가 실행하는 바이트코드의 줄 번호 표시라고 볼 수 있습니다. JVM 개념 모델 에서 바이트코드 인터프리터는 실행해야 할 다음 바이트코드 명령을 선택하기 위해 이 카운터의 값을 변경하여 작동합니다.
스택은 지역 변수 테이블, 피연산자 스택, 동적 링크, 메서드 종료 및 기타 정보를 저장하는 데 사용됩니다. 지역 변수 테이블에는 컴파일 중에 제한되는 다양한 기본 데이터 유형과 객체참조가 저장됩니다. 프로그램 카운터와 마찬가지로 스레드 전용입니다.
로컬 메소드 스택은 위에서 소개한 가상 머신 스택과 유사하지만 유일한 차이점은 가상 머신 스택이 Java 메소드(바이트코드)를 실행하는 역할을 한다는 점입니다. ), 로컬 메서드 스택은 가상 머신에서 사용하는 기본 메서드를 제공하며 일부 가상 머신은 두 가지를 하나로 결합하기도 합니다.
힙은 JVM이 관리하는 가장 큰 메모리 조각입니다. 모든 스레드가 공유하는 영역으로, 유일한 목적은 객체 인스턴스를 저장하는 것입니다. 거의 모든 객체 인스턴스가 여기에 메모리를 할당합니다(특수 클래스 객체와 마찬가지로 메모리는 메서드 영역에 할당됩니다). 이 장소는 가비지 수집 관리의 주요 영역이기도 합니다. 메모리 재활용의 관점에서 가비지 수집기는 이제 세대별 수집 알고리즘을 사용하므로(자세한 내용은 나중에 소개하겠습니다) Java 힙을 새로운 세대와 이전 세대로 더 세분화할 수 있습니다. 세대와 신세대 세대는 다시 Eden 공간, From Survivor 공간, To Survivor 공간으로 세분화됩니다. 효율성을 위해 힙을 여러 TLAB(스레드 전용 할당 버퍼)로 나눌 수도 있습니다. 어떻게 분할되든 저장 내용과는 아무런 관련이 없습니다. 어떤 영역에 있든 객체 인스턴스는 여전히 저장됩니다. 객체 인스턴스의 존재 목적은 메모리를 더 잘 재활용하고 할당하는 것입니다.
메소드 영역은 힙과 마찬가지로 스레드가 공유하는 메모리 영역으로 클래스 정보, 상수, 정적 변수, JIT(Just-In-Time) 컴파일러를 저장하는 데 사용됩니다. 가상 머신 코드 및 기타 데이터에 의해 로드된 컴파일입니다. 런타임 상수 풀은 메소드 영역의 일부로 주로 컴파일 타임에 선언된 다양한 리터럴 및 기호 참조를 저장하는 데 사용됩니다.
다이렉트 메모리는 가상 머신 런타임 데이터 영역에 포함되지 않으며 Java 사양에 정의되어 있지 않은 메모리 영역이기도 합니다. . 메모리 할당은 Java 힙 크기의 영향을 받지 않지만 전체 메모리 크기에 의해 제한됩니다.
가상 머신 메모리 영역의 개념에 대해 이야기한 후, 다시 본론으로 돌아가서 클래스 로딩 프로세스는 무엇입니까? 5단계: 로딩, 검증, 준비, 구문 분석, 초기화. 로딩, 검증, 준비, 초기화는 순차적으로 이루어지지만, 파싱은 반드시 초기화 이후에 수행될 수도 있다.
로드 단계 동안 JVM은 세 단계를 완료해야 합니다. 먼저 클래스의 정규화된 이름을 통해 이 클래스를 정의하는 이진 바이트 스트림을 얻은 다음 이것이 나타내는 바이트 스트림 정적 저장 구조는 메소드 영역의 런타임 데이터 구조로 변환되고 마지막으로 이 클래스를 나타내는 java.lang.Class 객체가 메모리에 생성됩니다. 메소드 영역. 바이너리 바이트 스트림을 얻는 첫 번째 단계에서는 *.class 파일에서 얻는다는 것이 명확하게 명시되어 있지 않습니다. 규정의 유연성으로 인해 ZIP에서 얻을 수 있습니다(JAR, EAR/WAR 형식의 기초 제공). ) 패키지를 만들고 네트워크에서 가져옵니다(애플릿), 런타임 시 계산 및 생성(동적 프록시), 생성된 기타 파일(JSP 파일에 의해 생성된 클래스 클래스), 데이터베이스에서 가져옵니다.
검증은 이름에서 알 수 있듯이 실제로 클래스 파일 바이트 스트림에 포함된 정보가 JVM의 요구 사항을 충족하는지 확인하는 것입니다. 왜냐하면 클래스 파일의 소스가 반드시 JVM에서 생성되는 것은 아니기 때문입니다. 컴파일러에서 생성할 수도 있으며 16진수 편집기 를 사용하여 생성할 수도 있습니다. 클래스 파일을 직접 작성합니다. 확인 프로세스에는 파일 형식 확인, 메타데이터 확인 및 바이트코드 확인이 포함됩니다. 여기에서는 구체적인 보안 확인 방법을 자세히 설명하지 않습니다.
준비 단계는 클래스 변수에 대해 정식으로 메모리를 할당하고 초기값을 설정하는 단계입니다. 이러한 변수가 사용하는 메모리는 메소드 영역에 할당됩니다.
구문 분석 단계는 JVM이 상수 풀의 기호 참조를 직접 참조(대상에 대한 포인터, 상대 오프셋 또는 핸들)로 바꾸는 프로세스입니다. 앞서 이야기한 컴파일 패딩 기호 테이블의 값이 여기에 반영됩니다. 구문 분석 프로세스는 클래스나 인터페이스, 필드 및 인터페이스 메서드를 구문 분석하는 것 이상입니다.
클래스 초기화 단계는 클래스 로딩 과정의 마지막 단계로, 준비 단계에서 변수에 초기값을 할당해 주며, 이 단계에서 이를 수행하게 됩니다. 프로그래머가 사용자 정의한 요구 사항에 따라 클래스 변수 및 기타 리소스를 초기화합니다. 이번 단계에서는 앞서 컴파일한 바이트코드 생성 과정에서 언급한
클래스가 초기화되지 않은 경우 new, getstatic, putstatic 또는 Invokestatic의 네 가지 바이트코드 명령어가 나타나면 앞에 있는 다양한 포크 명령어가 무엇인지 트리거해야 합니다. 간단한 이해는 새 개체를 만들 때, 클래스의 정적 필드를 읽거나 설정할 때, 클래스의 정적 메서드를 호출할 때입니다.
java.lang.reflect 패키지의 메소드를 사용하여 클래스에 대한 반사 호출을 수행할 때 클래스가 초기화되지 않은 경우 해당 초기화가 트리거되어야 합니다.
클래스를 초기화하고 해당 상위 클래스가 아직 초기화되지 않은 것을 발견하면 해당 상위 클래스의 초기화 작업이 먼저 트리거됩니다.
가상 머신이 시작되면 사용자는 실행할 메인 클래스(메인 메소드가 위치한 클래스)를 지정해야 하며, 가상 머신은 이 메인 클래스를 먼저 초기화합니다. .
JDK1.7 이상 동적 언어 지원을 사용할 때 java.lang.invoke.MethodHandle 인스턴스의 최종 구문 분석 결과가 REF_getStatic, REF_putStatic, REF_invokeStatic의 메소드 핸들인 경우 메소드 핸들에 해당하는 클래스가 초기화되지 않은 경우 초기화 작업이 트리거됩니다.
위의 두 단계가 끝나면 프로그램이 정상적으로 실행되기 시작합니다. 프로그램 실행 과정에는 다양한 명령어의 계산 작업이 포함됩니다. 프로그램은 어떻습니까? 이곳은 글의 시작 부분에서 언급한 백엔드 컴파일러(JIT just-in-time 컴파일러) + 인터프리터(HotSpot 가상머신은 기본적으로 인터프리터와 컴파일러를 사용함)를 사용하고, 바이트코드 실행 엔진은 다양한 프로그램 계산 작업을 담당합니다. Java 코드를 실행할 때 해석된 실행(인터프리터를 통해 실행)과 컴파일된 실행(Just-In-Time 컴파일러를 통해 생성된 로컬 코드)의 두 가지 옵션이 있을 수 있습니다. 스택 프레임은 가상 머신의 메소드 호출 및 실행을 지원하는 데 사용되는 데이터 구조입니다. 스택을 푸시하고 팝하는 다양한 명령의 구체적인 계산 아이디어에는 고전적인 알고리즘인 Dijkstra 알고리즘이 포함됩니다. 정보를 직접 확인하세요. 이곳은 너무 깊게 들어가지 않습니다. 런타임 최적화 문제는 이 단계에서도 마찬가지로 중요하며 JVM 설계 팀은 이 단계에서 성능 최적화에 집중하여 Javac에서 생성되지 않은 클래스 파일도 컴파일러 최적화의 이점을 누릴 수 있도록 합니다. 구체적인 최적화 기술은 무엇입니까? 대표적인 최적화 기술로는 공통 하위표현식제거, 배열 경계 검사 제거, 메서드 인라인, 이스케이프 분석 등이 있습니다.
드디어 프로그램이 죽음의 국면에 접어들었다고 합니다. JVM은 프로그램 알약을 어떻게 결정합니까? 이곳은 실제로 도달성 분석 알고리즘을 사용하고 있는데, 이 알고리즘의 기본 아이디어는 "GC Roots"라는 일련의 개체를 시작점으로 사용하고 이 노드에서 아래쪽으로 검색하는 것입니다. 체인(Reference)이라 불리는 객체를 GC Roots에 연결하는 참조 체인이 없는 경우(그래프 이론으로 말하면 해당 객체는 GC Roots에서 도달할 수 없음) 객체를 사용할 수 없음을 증명하고 재활용 가능한 객체로 결정합니다. . 재활용할 개체를 이미 알고 있는데 언제 가비지 수집을 시작합니까? 안전점은 GC를 수행하기 위해 프로그램이 일시적으로 실행되는 곳이다. 이를 통해 GC 일시정지 시간이 가비지 수집의 핵심임을 쉽게 알 수 있다. 모든 가비지 수집 알고리즘과 파생된 가비지 수집기는 모두 GC 일시 중지 시간을 최소화하는 데 중점을 두고 있습니다. 이제 최신 G1 가비지 수집기는 예측 가능한 일시 중지 시간 모델을 설정하고 전체 Java 힙에서 전체 GC 일시 중지를 방지할 수 있습니다. 앞에서 메모리 영역 분배 개념을 소개했을 때 신세대와 구세대에 대해 이야기했습니다. 신세대나 구세대에 대해 서로 다른 가비지 컬렉터가 작동할 수 있으며, 세대에 대한 개념조차 없습니다(예: G1 컬렉터). ).) 그런데, 다음은 가비지 컬렉션 알고리즘과 그에 상응하는 가비지 컬렉션
에 대해 자세히 소개한 것입니다. 두 가지 유형: 표시 및 지우기 단계: 먼저 재활용할 모든 개체를 표시합니다. 마킹이 완료된 후 표시된 모든 개체가 균일하게 재활용됩니다. 가장 큰 단점은 비효율적이며 많은 수의 불연속적인 메모리 조각을 생성한다는 것입니다. 이로 인해 프로그램이 실행 중에 큰 개체를 할당할 때 문제가 발생합니다. 힙에 충분한 메모리가 있어도 충분한 연속 메모리를 찾을 수 없습니다. GC 작업을 트리거합니다. 여기서 해당 가비지 수집기는 CMS 수집기입니다.
복사 알고리즘은 효율성 문제를 해결하기 위해 탄생했습니다. 사용 가능한 메모리 용량을 두 개의 동일한 크기 블록으로 나누고 이 블록이 있을 때 한 번에 하나만 사용할 수 있습니다. 메모리가 다 소모되면 남은 객체를 다른 블록에 복사한 후, 사용한 메모리 공간을 한번에 정리하세요. 이렇게 하면 매번 전체 절반 영역에 대해 GC가 수행되므로 메모리 조각화와 같은 문제가 발생하지 않습니다. 오늘날 대부분의 상용 가상 머신은 이 알고리즘을 사용하여 새로운 세대를 재활용합니다. 또한 메모리 분할 비율은 1:1이 아닙니다. 예를 들어 Eden(1개의 Eden 영역)과 Survivor(2개의 Survivor 영역)의 기본 크기 비율은 HotSpot은 Eden과 Survivor 영역 중 하나를 사용할 때마다 즉, 새 세대에서 사용 가능한 메모리 공간이 전체 새 세대의 90%가 됩니다. 마지막으로 방금 사용한 생존자 공간과 Eden을 정리합니다. 주의깊은 독자는 복사 과정에서 사용되지 않은 생존자 공간이 충분하지 않은 경우 어떻게 되는지 알아낼 수 있습니다. 이때 할당 보장을 위해 이전 세대에 의존해야 합니다. 보장이 성공하면 Eden과 Survivor에서 살아남은 객체 중 하나가 보장에 실패하면 가비지 수집이 이루어집니다. 구세대에서 발생합니다. 이를 확장하여 신세대 가비지 수집을 Minor GC라고 합니다. 대부분의 Java 객체는 태어나고 죽기 때문에 Minor GC가 매우 빈번하고 복구 속도가 일반적으로 빠릅니다. Major GC의 속도는 일반적으로 Minor GC의 속도보다 훨씬 느립니다. 앞선 분석 과정에서 Major GC의 발생이 종종 Minor GC를 동반한다는 것을 쉽게 유추할 수 있지만, 따라서 이것이 절대적이지는 않습니다. 우리 GC는 실제로 GC의 속도를 조정하는 것입니다. Major GC의 빈도를 최대한 조절하고 줄이는 것이 가장 좋습니다. 여기서 해당 가비지 수집기는 직렬 수집기, ParNew 수집기(나중에 언급할 구세대 수집기 CMS와 함께 작동할 수 있는 직렬 수집기의 다중 스레드 버전) 및 병렬 Scavenge 수집기입니다.
이 알고리즘을 Old Generation 가비지 컬렉션 알고리즘에 적용한 이유는 Old Generation이 복사 알고리즘만큼 자주 재활용되지 않고 공간도 낭비하기 때문입니다. mark-organize 프로세스는 후속 단계가 재활용 가능한 개체를 직접 지우는 것이 아니라 남아 있는 모든 개체를 한쪽 끝으로 이동한 다음 끝 경계 외부의 메모리를 직접 정리한다는 점을 제외하면 mark-clear와 유사합니다. 여기서 해당 가비지 수집기는 Serial Old 수집기와 Parallel Old 수집기입니다.
현재 상용 가상 머신은 모두 이 알고리즘을 사용하고 있습니다. 앞서 언급한 것처럼 힙 메모리 영역을 세대로 나누는 것이 아이디어입니다. 지역마다 다른 가비지 수집 알고리즘을 사용합니다. Young 세대는 복사 알고리즘을 사용하고 Old 세대는 mark-collation 또는 mark-sweep 알고리즘을 사용합니다.
이전에 너무 많이 이야기했기 때문에 Java 코드의 생명 역사에 대해 어느 정도 알고 있거나 잘 이해하지 못할 수도 있습니다. 여기서는 전체를 검토하기 위해 예를 제시합니다. 새로운 객체를 생성할 때 우리는 무엇을 경험하게 될까요? 이전에 말한 내용과 결합하여 JVM이 새로운 명령어를 만나면 먼저 전체 명령어 매개변수가 메서드 영역의 상수 풀에 있는 클래스의 기호 참조를 찾을 수 있는지 확인하고 해당 클래스가 전체 기호로 표시되는지 확인합니다. 참조가 로드되고 구문 분석되고 초기화되었습니다. 그렇지 않은 경우 해당 클래스 로드 프로세스가 먼저 실행되어야 합니다. 클래스 로딩 검사를 통과한 후 JVM은 새 객체에 대한 메모리를 할당합니다. 이 프로세스는 힙 메모리가 완료된 후 할당 크기를 결정할 수 있습니다. 동일한 거리이면 충분합니다. 이 할당 방법을 "포인터 충돌"이라고 합니다. 분산된 경우 JVM은 사용 가능한 메모리를 기록하기 위해 목록을 유지하고 목록 레코드를 "라고 합니다. free list"는 어떤 방법을 사용하는가에 따라 앞서 언급한 힙에 어떤 가비지 수집기가 사용되는지에 따라 달라집니다. 객체 메모리를 분할한 후 가상 머신은 필요한 초기화 작업을 수행합니다. 다음으로 객체에 대해 필요한 설정을 수행해야 합니다. 이 정보는 객체 헤더에 설정됩니다(클래스 메타데이터 정보, 객체 해시 코드, 객체 GC 생성 기간 등). . ), 이러한 작업이 완료된 후 새로운 객체가 생성됩니다. 이는 실제로 아직 끝나지 않았습니다. 다음 단계는 프로그래머가 계획한 객체 필드 할당을 수행하는 것입니다. 마지막으로 스택의 참조 지점은 힙의 개체가 있는 메모리 주소를 설정합니다(직접 참조). 이때 개체에 대한 다양한 후속 작업과 해당 개체의 최종 소멸에 대해 설명합니다. 앞서 언급한 바이트코드 실행 엔진입니다. 아 GC, 이제 다들 낯설지 않으실 거라 믿습니다.
위 내용은 Java 프로그램의 생활사의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!