토끼 굴을 따라 메모리 누수 차선으로
당신은 소프트웨어 엔지니어이고 모니터링을 늘리고 개선하는 임무를 맡은 새로운 팀에 막 합류했습니다. 이 새 작업을 수행하는 동안 복구, 보고서 및 기타 장기 실행 작업과 같은 모든 백그라운드 작업을 처리하는 방치된 서비스를 찾습니다. 그러나 그것은 "뒤뜰"이기 때문에 아무도 측정할 가치가 있다고 생각하지 않았습니다… 지금까지. 하지만 일단 시작하면 이 서비스가 항상 90% 이상의 메모리 사용량으로 지속적으로 작동한다는 것을 알게 됩니다.
"우리가 어떻게 여기까지 왔는지," 당신은 스스로 생각합니다. "그리고 지금은?"
귀하의 팀은 귀하가 팀에 참여하고 신뢰를 얻는 가장 좋은 방법은 이 문제를 처리하는 것이라고 생각합니다.
저는 10년 이상의 경력을 가진 소프트웨어 엔지니어인 Shay입니다. 저는 이와 같은 몇 가지 문제에 직면했습니다. 유사한 문제가 있거나 향후 문제가 발생할 수 있다고 생각되면 여기에서 몇 가지 유용한 팁을 찾을 수 있습니다. 그럼 시작해 볼까요?
기본 사항
문제를 처리하기 전에 몇 가지 기본 규칙을 정하는 것이 중요합니다. 이 단계에 대한 지름길은 없지만 도움이 될 수 있고 모든 개발자가 읽어야 할 두 권의 책이 있습니다. 바로 Bob 삼촌(Uncle Bob)인 Robert Cecil Martin의 Clean Code 와 The Clean Coder 입니다. 두 책 모두 무엇에 대한 확실한 설명을 제공합니다. 좋은 코드를 만들고 무엇이 나쁜 코드를 만들 수 있는지. Bealdung은 모범 사례와 추가 정보도 제공합니다.
다음 단계는 서비스를 살펴보고 무엇이 문제를 일으킬 수 있는지 이해한 다음 로컬 컴퓨터에서 프로젝트를 실행하여 애플리케이션 작동 방식을 이해하는 것입니다. 기본 이해가 완료되면 작업을 완료할 때입니다.
알려진 문제 및 손쉬운 결과
아래의 문제는 비교적 찾기 쉽고 수정하면 작업 시간을 줄일 수 있다고 생각합니다. 그러나 그들은 큰 코드 영역에 퍼질 수 있으므로 시간을 갖고 가장 중요한 것은 애플리케이션 비즈니스 흐름을 손상시키지 않고 누출을 수정하는 것임을 기억하십시오.
중첩 된 루프
중첩된 루프는 처음에는 무해해 보이지만 메모리를 크게 증가시킬 수 있습니다. 이는 가비지 수집기가 이러한 개체의 메모리를 해제하기 위해 코드 블록이 확인된 후 사용 가능으로 표시되어야 하기 때문입니다. 예를 들어 두 개의 항목 목록이 있고 두 목록에 나타나는 모든 항목을 찾고 싶다고 가정해 보겠습니다.
public List<Item> matchedItems(List<Item> liOne, List<Item> liTwo){
List<Item> outcome = new ArrayList<Item>();
for(Item one : liOne){
for(Item two : liTwo){
if (one.value().equals(two.value()){
outcome.add(one);
break;
}
}
}
return outcome
}
또한 중첩된 루프는 일반적으로 코드에서 "숨깁니다". 다음과 같이 보일 수 있습니다.
public List<Item> matchedItems(List<Item> liOne, List<Item> liTwo){
List<Item> outcome = new ArrayList<Item>();
for(Item one : liOne){
if(existsInList(liTwo , one.value)){
outcome.add(one);
break;
}
return outcome
}
private boolean existsInList(List<Item> items , String value){
for(Item item : items){
if (item.value().equals(value){
return true;
}
}
return false;
}
목록을 반복하기 전에 목록을 정렬하고 중단점을 추가하는 등 수행할 수 있는 몇 가지 작업이 있습니다. 이렇게 하면 반복해야 하는 항목의 양이 줄어들 수 있지만 반복을 위해 맵과 같은 다른 솔루션을 사용하면 어떻게 될까요?
public List<Item> matchedItems (List<Item> liOne, List<Item> liTwo){
Map<String value,Item item> mappedItems = new HashMap();
for(Item one : liOne){
mappedItems.put(liOne.value(), one);
}
List<Item> outcome = new ArrayList<Item>();
for(Item two: liTwo) {
if(mappedItems.containsKey(two.value()) {
outcome.put(two);
}
}
return outcome;
}
스케줄러 거의 모든 최신 Java 프레임 워크
는 Quartz 또는 Spring Boot 등 스케줄러를 사용합니다. 따라서 무거운 페이로드를 사용해야 하는 경우 문제가 발생할 수 있습니다.
@Scheduled(fixedDelayString = "1000")
public void collectMessageFromSQS() {
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(sqsUrl);
receiveMessageRequest.setMaxNumberOfMessages(config.getMaxMessageToPull());
ReceiveMessageResult receiveMessageResult = config.amazonSQSAsync().receiveMessage(receiveMessageRequest);
//handle SQS message
}
그래서 무엇을 할 수 있습니까? 첫째, 비동기 작업은 프레임워크가 스레드 풀을 사용하여 새 스레드를 생성하여 도달 프로세스에 대한 새 스레드를 생성하도록 합니다. 이러한 방식으로 여러 스레드에 대한 Java 프레임워크 기능을 활용하고 있습니다.
동일한 연결을 사용하거나 연결 풀을 정의할 수 있습니다(SQS의 경우 관련 없음).
스트림 및 익명 함수
지난 몇 년 동안 Java 언어를 구축한 똑똑한 사람들은 스트림과 익명 함수를 추가하여 Java 언어를 현대화하려고 시도했습니다. 그들이 한 일 중 일부는 경험과 작성 복잡성 모두에서 상당한 개선을 이루어 작성되는 코드의 양을 줄였습니다.
위의 모든 것은 디버깅을 위한 좋은 방법이지만 익명 함수 작업은 더 어렵습니다. 예를 들면 다음과 같습니다.
List<Prop> goodProps = new ArrayList();
list.stream().forEach(item -> {
for(prop p : item.props){
if (p instanceof GoodValue){
goodProps.add(p);
}
}
});
return goodProps;
List<Prop> goodPropes = list
.stream()
.map(Prop :: item.getEnrichedProp)
.filter(p -> p instanceOf GoodValue)
.collect(toList());
return goodProps;
문제가 있는 정적 메서드 및 정적 개체 찾기
정적 변수는 GC에서 수집하지 않으므로 일단 생성하면 JVM의 수명이 다할 때까지 유지됩니다. 그렇다면 무엇을 해야 할까요? 예를 살펴보겠습니다.
public static Optional<Object> convertToJson(final String jsonString) {
ObjectMapper mapper = new ObjectMapper();
try {
return Optional.of(mapper.readValue(jsonString, Object.class));
} catch (Exception e) {
return Optional.empty();
}
}
1과 2의 두 스레드가 있다고 가정해 보겠습니다. 둘 다 동일한 정적 함수에 액세스하지만 코드 블록이 동기화되지 않고 싱글톤이 아니기 때문에 경우에 따라 두 개의 코드 인스턴스가 생성될 수 있습니다. 또한 코드 자체를 잘 살펴보면 모든 요청에 대해 새 ObjectMapper를 생성하고 있습니다. 즉, 메서드가 정적이므로 매번 새 개체를 할당한다는 의미입니다.
그래서 우리는 무엇을 다르게 할 수 있습니까?
public Optional<Object> convertToJson(final String jsonString) {
ObjectMapper mapper = new ObjectMapper();
try {
return Optional.of(mapper.readValue(jsonString, Object.class));
} catch (Exception e) {
return Optional.empty();
}
}
public class jsonUtils {
private final ObjectMapper mapper = new ObjectMapper();
public Optional<Object> convertToJson(final String jsonString) {
try {
return Optional.of(mapper.readValue(jsonString, Object.class));
} catch (Exception e) {
return Optional.empty();
}
}
}
상수도 마찬가지입니다.
public class MyConstants{
public static String MY_STRING = "this is my String";
}
올바른 방법은 다음과 같습니다
public final class MyConstants{
private MyConstants()
public final static String MY_STRING = "this is my String";
}
타사 SDK, 데코레이터 및 대기열
일반적으로 SQS나 관리형 Kafka 서비스와 같은 클라우드 관리형 서비스의 경우 SDK로 작업할 때 일부는 연결 생성에 능숙하지만 리소스 할당 문제를 찾는 것이 어렵고 추가 작업이 필요합니다. Google 검색을 통해 누군가 GITHUB에 문제를 제기했는지 또는 그러한 문제에 대한 해결 방법을 제공했는지 확인하는 것이 가장 좋습니다.
C 남은 연결을 모두 잃습니다.
데이터베이스에 대한 연결을 생성하든, 웹 소켓을 열든, 아니면 일반 HTTP 클라이언트를 열든 상관없습니다. 연결을 얻은 경우 연결을 닫고 해제하는 방법에 대해 생각해야 합니다.
일부 API는 잠긴 개체를 해제하는 방법을 모르거나 연결을 재사용하는 방법을 모릅니다. 제 시간에 연결을 해제하지 않고 계속해서 연결을 할당하는 API가 발생할 수 있습니다. 이로 인해 GC의 수명이 단축되고 연결이 닫히지 않습니다.
연결 풀링이 오랫동안 열려 있을 수 있으며 정리할 준비가 된 것으로 표시되지 않을 수 있습니다.
로거 형식 및 MDC
대부분의 로그 형식 및 형식 템플릿은 매핑된 진단 컨텍스트(MDC)라는 개체에 의존합니다. 이 개체는 매우 오래된 개체(Java 1.1부터)이며 처음부터 변경되지 않았습니다. 스레드로부터 안전하지는 않지만 대부분의 템플릿에서 사용합니다.
사용 중인 경우 완료되면 다음을 수행하여 값을 해제하십시오.
mdc.clear();
메모리 덤프
이 작업을 수행할 수 있는 바로 가기가 없습니다. 해당 JVM을 보유한 항목에 로그인해야 합니다. 머신, 가상 머신 또는 각각에 대한 kubernetes 포드일 수 있습니다. 스레드 덤프와 힙 덤프라는 두 가지 유형의 샘플링을 수행하는 것이 좋습니다.
응용 프로그램의 메모리가 급증하기 전과 메모리가 증가한 후에 각 종류의 샘플을 하나씩 가져와야 합니다. 이것의 이면에 있는 논리는 어떤 프로세스와 어떤 스레드가 메모리를 보유하고 있고 리소스를 해제하지 않는지 비교할 수 있는 기능을 제공한다는 것입니다. 또한 기본 개체를 가져오는지 또는 클래스 자체를 가져오는지 알 수 있습니다.
메모리 소비율
제가 보기에 누출에는 세 가지 유형이 있습니다.
"Hit the wall"은 응용 프로그램 내의 무언가가 한 번에 모든 메모리를 소모함을 의미합니다.
비교적 짧은 기간에 점진적으로 증가하는 "Stairway"
장기간에 걸쳐 지속적으로 증가하는 "누수"
이들 각각은 코드 내에서 서로 다른 문제를 나타낼 수 있지만 공통점은 메모리가 할당되는 만큼 빠르게 해제되지 않는다는 것입니다.
읽기, 평가, 인쇄 루프
이것은 결코 재미있는 작업이 아니지만 내 경험에 따르면 응용 프로그램은 서로 다른 위치에서 둘 이상의 누출이 있을 수 있으므로 지속적으로 유지하는 것이 중요합니다. 세부 사항을 주의 깊게 조사하면서 인내와 일관성을 가지고 이러한 프로세스를 두 번 이상 반복해야 합니다.
드라이런
그래서, 당신은 모든 누출을 찾았고 당신이 끝났다고 생각하지만… 이제 할 시간이다
비프로덕션 환경을 QA에 배포하여 테스트 실행합니다. 그런 다음 종단 간 테스트를 실행하여 소량의 데이터에 대한 메모리 할당 성능을 측정합니다.
나는 이것이 가장 중요한 단계라고 생각합니다. 가장 어려운 단계일 수 있지만 건너뛰면 큰 문제가 발생할 수 있습니다.
최종 단어
이 지점에 도달해 주셔서 감사합니다! 메모리 누수에 대한 작업은 어렵고 짜증나고 실망스럽습니다. 하지만 이러한 통찰력이 여러분의 여정에 도움이 되기를 바랍니다.
이 팁과 요령을 사용할 때도 시간이 걸리므로 예리하고 침착하십시오. 코드가 신중하게 검토되지 않으면 누수가 발생하므로 문제를 찾아 수정해야 합니다. 하지만 괜찮습니다. 실수는 발생합니다. 중요한 것은 이러한 실수로부터 배우고 배운 교훈을 팀과 공유하는 것입니다.
저의 진행 상황은 다음과 같습니다.
많은 시행착오가 있었음을 알 수 있지만 결국 애플리케이션의 메모리 사용량을 97%에서 약 75%로 줄일 수 있었습니다. 그것은 많은 것이며 내 응용 프로그램의 안정성에 영향을 미칠 것입니다.
즐겁게 코딩하고 계속 배우세요!

![연결된 목록이란 무엇입니까? [1 부]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































