Java에서 메모리 누수를 작성하려면 어떻게 해야 하나요?
방금 인터뷰를 했는데 자바에서 메모리 누수를 만들어 달라는 요청을 받았습니다.
말할 필요도 없이, 저는 어떻게 그것을 만들기 시작해야 할지 전혀 알지 못하는 바보 같은 기분이 들었습니다.
어떤 예를 들까요?
다음은 순수 Java에서 진정한 메모리 누수(실행 코드로 액세스할 수 없지만 메모리에 저장된 객체)를 생성하는 좋은 방법입니다.
- 응용 프로그램은 장기 실행 스레드를 생성합니다(또는 스레드 풀을 사용하여 더 빨리 누출됩니다).
- 는 (으로 커스텀)를 통해 합니다.
ClassLoader
. - 클래스는 합니다( "Memory"는 "Memory"는 "Memory"는 "Memory"와 "Memory"는 "Memory"와 "Memory"는 "Memory"와 "Memory"와 같이 할당됩니다
new byte[1000000]
)는 강한 , 그 를 「」, 「」, 「」, 「」, 「」에 합니다.ThreadLocal
추가 메모리의 할당은 옵션이지만(클래스인스턴스를 리크하는 것으로 충분합니다), 리크가 훨씬 빨리 동작합니다. - 「Custom Class」에 를 모두 합니다.
ClassLoader
로딩되었습니다. - 따라하다.
ThreadLocal
Oracle JDK입니다.
- ★★
Thread
분야가threadLocals
이치 - 이 맵의 각 키는 이 맵에 포함된
ThreadLocal
그는, 「 」ThreadLocal
오브젝트는 가비지 처리되며 엔트리는 맵에서 삭제됩니다. - 단, 각 값은 강력한 참조가 되기 때문에 (직접 또는 간접적으로) 값이 다음 값을 가리킬 때
ThreadLocal
오브젝트(키)는 스레드가 존속하는 한 그 오브젝트는 가비지 검색되지 않으며 맵에서 삭제도 되지 않습니다.
이 예에서는 강력한 참조 체인은 다음과 같습니다.
Thread
→ "오브젝트" →threadLocals
→ class → class → static map → static map → map map map map map map map map → map map map map map map map map map → map map map map map map map map map map map map map map mapThreadLocal
→ 필 field →ThreadLocal
★★★★★★ 。
(the)ClassLoader
예: class → 누 doesn: 、 doesn doesn 、 :예예)) → 예예예예예예예예예예예예예예예예예 ( )))))) → ) 。ClassLoader
→ 로드한 모든 클래스입니다. Java 7 악화되었습니다.와 JVM의 「JVM」, 「Java 7」의 「Java 7」의 「JVM」의 「Java 7」의 「JVM」의 「JVM」의 「JVM」의 「JVM」의 「JVM」의 「Java 7」의 「JVM」의 「Java 7」의 「JVM」의 「JClassLoader
은, 「」를 사용하고 있는 재구현하면,Tomcat 등가 체처럼 할 수 입니다.ThreadLocal
어떤 식으로든 자기 자신을 가리키고 있는 것입니다.이 문제는 여러 가지 미묘한 이유로 발생할 수 있으며 디버깅 및 수정이 어려운 경우가 많습니다.
업데이트: 많은 사람들이 계속해서 요구하기 때문에, 다음은 이 동작을 보여주는 몇 가지 예시 코드입니다.
오브젝트 참조를 유지하는 정적 필드 [특히 a] 최종 필드]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
긴 문자열 호출
String str = readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();
(비닫힘) 오픈스트림(파일, 네트워크 등)
try {
BufferedReader br = new BufferedReader(new FileReader(inputFile));
...
...
} catch (Exception e) {
e.printStacktrace();
}
미닫힘 접속
try {
Connection conn = ConnectionFactory.getConnection();
...
...
} catch (Exception e) {
e.printStacktrace();
}
네이티브 메서드를 통해 할당된 메모리와 같이 JVM의 가비지 컬렉터에서 연결할 수 없는 영역입니다.
웹 응용 프로그램에서는 응용 프로그램이 명시적으로 중지 또는 삭제될 때까지 일부 개체가 응용 프로그램 범위에 저장됩니다.
getServletContext().setAttribute("SOME_MAP", map);
JVM 옵션이 올바르지 않거나 부적절합니다(예:noclassgc
하지 않는 가비지
IBM JDK 설정을 참조하십시오.
하지 않는)입니다.hashCode()
★★★★★★★★★★★★★★★★★」equals()
' '증명서', '증명서', '증명서', '증명서', '증명서'를중복은 무시하지 않고 세트가 계속 커지기 때문에 제거할 수 없습니다.
이러한 잘못된 키/요소를 계속 사용하려면 다음과 같은 정적 필드를 사용할 수 있습니다.
class BadKey {
// no hashCode or equals();
public final String key;
public BadKey(String key) { this.key = key; }
}
Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.
아래에는 Java가 리스너, 정적 참조, 해시맵의 가짜/수정 가능한 키, 또는 라이프 사이클을 종료할 기회 없이 스레드만 스택하는 일반적인 케이스 외에 누출되는 명백한 케이스가 있습니다.
File.deleteOnExit()
- 문자열이항상누수됩니다.문자열이 서브스트링일 경우 누수는 더욱심합니다(기본문자[]도누수됩니다). Java 7에서는 하위스트링도 복사되므로 나중에 적용되지 않습니다.@Daniel, 투표할 필요는 없습니다.
주로 스레드에 집중하여 관리되지 않는 스레드의 위험성을 나타낼 것입니다. 스윙을 만지고 싶지도 않습니다.
Runtime.addShutdownHook
RemoveShutdownremove Shutdown"..."을 되지 않은 스레드 그룹 의 버그로 되지 않을 이 있는 "은 스레드 그룹을 으로 누출합니다..수집되지 않을 수 있는 스레드와 관련된 ThreadGroup 클래스의 버그로 인해 후크가 발생하면 ThreadGroup이 효과적으로 리크됩니다.JGroup gossip Gossip Router 。않고 작성
Thread
위와 같은 카테고리로 분류됩니다.하면 스레드가 됩니다.
ContextClassLoader
★★★★★★★★★★★★★★★★★」AccessControlContext
및 를 .ThreadGroup
임의의 and and and and andInheritedThreadLocal
이러한 모든 참조는 클래스 로더에 의해 로드된 전체 클래스 및 모든 정적 참조 및 ja-ja와 함께 잠재적인 누출입니다.c. 전체 j.u.c.에서 볼 수 있습니다.에서 볼 수 있다. 심플한 「」를 으로 하는 실행자 .ThreadFactory
그러나 대부분의 개발자들은 잠재되어 있는 위험에 대한 단서를 가지고 있지 않습니다.또, 많은 라이브러리는, 요구에 따라서 스레드를 기동합니다(업계의 통용 라이브러리가 너무 많습니다).ThreadLocal
캐시: 그것들은 많은 경우에 악합니다.ThreadLocal thread thread thread thread thread thread thread thread thread thread 。나쁜 소식은 스레드가 ClassLoader의 라이프 사이클에 기대 이상의 속도로 계속 진행된다면 이는 매우 좋은 작은 누수라는 것입니다.꼭 필요한 경우가 아니면 스레드 로컬 캐시를 사용하지 마십시오." "
ThreadGroup.destroy()
스레드 그룹에 스레드 자체는 없지만 하위 스레드 그룹은 그대로 유지됩니다.스레드 그룹을 부모로부터 삭제할 수 없게 되는 불량 누수입니다만, 모든 자녀는 열거할 수 없게 됩니다.Weak Hash Map weak ( in ) 。을 사용하다은 모든 확장자에 됩니다.
Weak/SoftReference
감시 대상물에 대한 확실한 언급이 있을 수 있습니다「」를 사용합니다.
java.net.URL
HTTP(S)를 사용합니다. ★★★★★★★★★★★★★★★★★★★★★★★★★★.KeepAliveCache
는 현재 스레드의 컨텍스트클래스로더를 누설하는 새로운 스레드를 시스템 스레드 그룹에 만듭니다.스레드는 활성 스레드가 존재하지 않을 때 첫 번째 요청 시 작성되므로 운이 좋거나 누출될 수 있습니다.누수는 Java 7에서 이미 수정되었으며 스레드를 올바르게 작성하는 코드에 의해 컨텍스트클래스로더가 삭제됩니다.몇가지케이스가 더 있습니다(이미지 등).유사한 스레드를 만드는 Fetcher, 또한 고정).「」를 사용합니다.
InflaterInputStream
.new java.util.zip.Inflater()
PNGImageDecoder
를 를 호출하지 .end()
인플레이터의 경우.건설업자를 통과하면new
아니, 그럴 리 없어...네, 전화하고 있습니다.close()
인플레이터가 생성자 매개 변수로 수동으로 전달될 경우 인플레이터가 닫히지 않습니다.파이널라이저에 의해 방출될 것이기 때문에 실제 누출은 아닙니다.필요하다고 판단될 때.그 순간까지 네이티브메모리를 너무 많이 소비하여 Linux oom_killer로 인해 프로세스가 중단될 수 있습니다.주요 문제는 Java에서의 최종화는 매우 신뢰할 수 없으며 G1은 7.0.2까지 이를 악화시켰다는 것입니다.이야기의 교훈: 가능한 한 빨리 토종 자원을 풀어라; 최종 결정자가 너무 형편없다.같은 경우
java.util.zip.Deflater
이것은 Deflater가 Java에서 메모리가 부족하기 때문에 훨씬 더 심각합니다. 즉, 항상 15비트(최대)와 8개의 메모리 레벨(9최대)을 사용하여 수백 KB의 네이티브 메모리를 할당합니다.다행히도.Deflater
널리 사용되고 있지 않고, JDK에는 오용이 없는 것으로 알고 있습니다.항상 전화하다end()
수동으로 작성하면Deflater
또는Inflater
지난 두 가지 중 가장 좋은 점은 일반적인 프로파일링 도구로는 찾을 수 없다는 것입니다.
(요청 시 발생한 시간 낭비를 추가할 수 있습니다.)
행운을 빌고 안전하게 지내라. 새는 것은 악이다!
대부분의 예는 "너무 복잡"합니다.엣지 케이스입니다.이러한 예에서 프로그래머는 실수(예: 등호/해시코드를 재정의하지 않음)하거나 JVM/JAVA(스태틱한 클래스 로드...)의 코너 케이스에 물렸습니다.저는 면접관이 원하는 예나 가장 흔한 경우가 아니라고 생각합니다.
하지만 메모리 누수는 더 간단한 경우가 있습니다.가비지 컬렉터는 더 이상 참조되지 않는 것만 해방합니다.자바 개발자로서 우리는 메모리에 대해 신경 쓰지 않는다.필요할 때 할당하고 자동으로 해방시킵니다.좋아.
그러나 수명이 긴 모든 애플리케이션은 공유 상태를 갖는 경향이 있습니다.뭐든 될 수 있어, 정전기, 싱글톤...단순하지 않은 응용 프로그램은 종종 복잡한 개체 그래프를 만드는 경향이 있습니다.참조를 null로 설정하는 것을 잊거나 컬렉션에서 개체를 삭제하는 것을 잊는 경우가 많으면 메모리 누수가 발생할 수 있습니다.
물론 모든 종류의 리스너(UI 리스너 등), 캐시 또는 장기간의 공유 상태는 적절하게 처리되지 않으면 메모리 누수가 발생할 수 있습니다.이해해야 할 것은 이것이 Java 코너 케이스나 가비지 콜렉터의 문제가 아니라는 것입니다.설계상의 문제입니다.장기간에 걸친 오브젝트에 리스너를 추가하도록 설계하고 있습니다만, 불필요하게 되었을 때는 리스너를 삭제하지 않습니다.개체를 캐시하지만 캐시에서 제거할 전략은 없습니다.
계산에 필요한 이전 상태를 저장하는 복잡한 그래프가 있을 수 있습니다.그러나 이전 상태는 그 이전 상태와 연결되어 있습니다.
SQL 연결이나 파일을 닫아야 하는 것처럼요.적절한 참조를 null로 설정하고 컬렉션에서 요소를 제거해야 합니다.적절한 캐싱 전략(최대 메모리 크기, 요소 수 또는 타이머)이 필요합니다.리스너에게 통지할 수 있는 모든 오브젝트는 addListener 메서드와 removeListener 메서드를 모두 제공해야 합니다.또한 이러한 알림이 더 이상 사용되지 않으면 수신기 목록을 지워야 합니다.
메모리 누수는 정말로 가능하며 완벽하게 예측 가능합니다.특별한 언어 기능이나 코너 케이스는 필요 없습니다.메모리 누수는, 무엇인가 부족하거나 설계상의 문제가 있는 것을 나타내는 지표입니다.
답은 전적으로 면접관이 무엇을 묻고 있다고 생각하느냐에 달려 있습니다.
실제로 자바 유출이 가능한가요?물론 그렇습니다. 그리고 다른 답변에는 많은 예가 있습니다.
하지만 여러 개의 메타 질문이 있을 수 있어요?
- 이론적으로 "완벽한" Java 구현은 누출에 취약합니까?
- 지원자는 이론과 현실의 차이를 이해하고 있습니까?
- 지원자는 가비지 수집이 어떻게 작동하는지 이해하고 있습니까?
- 아니면 쓰레기 수거가 이상적인 경우 어떻게 작동해야 할까요?
- 네이티브 인터페이스를 통해 다른 언어를 호출할 수 있다는 것을 알고 있습니까?
- 다른 언어로 메모리 유출하는 거 알아요?
- 지원자는 메모리 관리가 무엇인지, Java에서 백그라운드에서 무슨 일이 일어나고 있는지 알고 있습니까?
저는 당신의 메타 질문을 "이 면접 상황에서 내가 할 수 있는 답변은 무엇인가?"라고 읽고 있습니다.그래서 저는 자바 대신 인터뷰 스킬에 초점을 맞출 것입니다.자바 유출 방법을 알아야 하는 상황보다 면접에서 질문에 대한 답을 모르는 상황을 반복할 가능성이 높다고 생각합니다.이게 도움이 됐으면 좋겠네요
면접을 위해 개발할 수 있는 가장 중요한 기술 중 하나는 질문에 적극적으로 귀를 기울이는 법을 배우고 면접관과 협력하여 그들의 의도를 파악하는 것입니다.이것은 여러분이 그들의 질문에 그들이 원하는 방식으로 대답할 수 있을 뿐만 아니라 여러분이 중요한 의사소통 기술을 가지고 있다는 것을 보여줍니다.그리고 동등한 능력을 가진 많은 개발자들 사이에서 선택을 하게 되면, 나는 그들이 매번 반응하기 전에 듣고, 생각하고, 이해하는 사람을 고용할 것이다.
다음은 JDBC를 이해하지 못하는 경우 매우 의미 없는 예입니다.또는 적어도 JDBC가 개발자가 어떻게 종료할 것으로 예상하는가?Connection
,Statement
그리고.ResultSet
예를 폐기하거나 참조를 잃기 전에, 예를 들어, 실행 방법에 의존하지 않고, 예를 들어, 예를 들어맞습니다.finalize
.
void doWork() {
try {
Connection conn = ConnectionFactory.getConnection();
PreparedStatement stmt = conn.preparedStatement("some query");
// executes a valid query
ResultSet rs = stmt.executeQuery();
while(rs.hasNext()) {
// ... process the result set
}
} catch(SQLException sqlEx) {
log(sqlEx);
}
}
상기의 문제점은 다음과 같습니다.Connection
오브젝트는 닫히지 않기 때문에 물리적인 접속은 가비지 컬렉터가 나타나 도달할 수 없음을 확인할 때까지 열린 상태로 유지됩니다.GC가 호출합니다.finalize
JDBC 드라이버는 이 기능을 실장하지 않습니다.finalize
적어도 같은 방법은 아닙니다.Connection.close
구현되어 있습니다.그 결과, 도달 불가능한 오브젝트가 수집되어 메모리가 회수되는 한편, 메모리(메모리 포함)와 관련된 자원(메모리 포함)이Connection
단순히 오브젝트가 회수되지 않을 수 있습니다.
이 경우,Connection
의finalize
방법은 모든 것을 청소하는 것은 아닙니다.데이터베이스 서버에 대한 물리적인 접속은 실제로 몇 번의 가비지 수집 사이클을 지속하고 있는 것을 데이터베이스 서버가 최종적으로 검출할 때까지(존재하고 있는 경우) 확인할 수 있습니다.
JDBC 드라이버가 실장되어 있는 경우에서도,finalize
최종화 중에 예외가 발생할 수 있습니다.그 결과 현재 "dormant" 객체와 관련된 모든 메모리가 회수되지 않습니다.finalize
는 1회만 호출됩니다.
오브젝트 최종화 중에 예외가 발생하는 위의 시나리오는 메모리 누수를 일으킬 수 있는 다른 시나리오와 관련되어 있습니다.즉, 오브젝트 부활입니다.오브젝트 부활은 종종 다른 오브젝트로부터 오브젝트에 대한 강력한 참조를 작성함으로써 의도적으로 이루어집니다.오브젝트 부활이 오용되면 메모리 누수가 다른 메모리 누수의 원인이 될 수 있습니다.
그 밖에도 생각할 수 있는 예가 많이 있습니다.
- 관리
List
목록에 추가만 하고 삭제는 하지 않는 인스턴스(단, 더 이상 필요 없는 요소를 삭제해야 함) 또는 - 오프닝
Socket
또는File
s. 단, 더 이상 필요하지 않은 경우에는 닫지 않는다(위의 예시와 마찬가지로,Connection
클래스) - Java EE 애플리케이션을 다운시킬 때 싱글톤을 언로드하지 않는다.singleton 클래스를 로드한 Classloader는 클래스에 대한 참조를 유지하므로 singleton 인스턴스는 수집되지 않습니다.응용 프로그램의 새 인스턴스가 배포되면 일반적으로 새 클래스 로더가 생성되며 싱글톤으로 인해 이전 클래스 로더가 계속 존재합니다.
잠재적인 메모리 누수의 가장 간단한 예 중 하나이며 이를 회피하는 방법은 ArrayList.remove(int)를 구현하는 것입니다.
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
elementData[--size] = null; // (!) Let gc do its work
return oldValue;
}
직접 실장하고 있는 경우는, 사용되지 않게 된 어레이 요소를 클리어 하는 것을 생각하고 계십니까( ).elementData[--size] = null
? 그 참조는 거대한 물체를 계속 살려둘지도 모릅니다...
더 이상 필요하지 않은 개체에 대한 참조를 주변에 유지할 때마다 메모리 누수가 발생합니다.Java에서 메모리 누수가 나타나는 방법과 이에 대해 수행할 수 있는 작업에 대한 예제는 Java 프로그램에서 메모리 누수 처리를 참조하십시오.
sun.misc로 메모리 누수를 할 수 있습니다.안전하지 않은 클래스.실제로 이 서비스 클래스는 다른 표준 클래스(예를 들어 java.nio 클래스)에서 사용됩니다.이 클래스의 인스턴스를 직접 만들 수는 없지만 반사를 사용하여 만들 수는 있습니다.
코드가 Eclipse IDE에서 컴파일되지 않음 - 명령을 사용하여 컴파일javac
(컴파일 중에 경고가 표시됩니다)
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class TestUnsafe {
public static void main(String[] args) throws Exception{
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
System.out.print("4..3..2..1...");
try
{
for(;;)
unsafe.allocateMemory(1024*1024);
} catch(Error e) {
System.out.println("Boom :)");
e.printStackTrace();
}
}
}
여기서 답변을 복사할 수 있습니다.Java에서 메모리 누수를 일으키는 가장 쉬운 방법
메모리 누수는 컴퓨터 과학(또는 이 맥락에서 누수)에서 컴퓨터 프로그램이 메모리를 소비하지만 운영체제로 되돌릴 수 없을 때 발생합니다.(Wikipedia)
쉬운 답은 '할 수 없다'입니다.Java는 자동 메모리 관리를 수행하며 사용자에게 필요하지 않은 리소스를 해방합니다.이런 일이 일어나는 걸 막을 수는 없어요항상 자원을 해방할 수 있습니다.메모리 관리를 수동으로 실시하는 프로그램에서는, 이것은 다릅니다.malloc()를 사용하면 C에서 메모리를 얻을 수 있습니다.메모리를 해방시키려면 malloc이 반환한 포인터와 그 포인터에 free()를 호출해야 합니다.그러나 포인터가 더 이상 없는 경우(덮어쓰기되거나 수명이 초과됨) 이 메모리를 해방할 수 없기 때문에 메모리 누수가 발생합니다.
지금까지의 다른 모든 답변은 메모리 누설이 아닌 제 정의에 있습니다.그들은 모두 무의미한 것들로 기억을 빨리 채우는 것을 목표로 하고 있다.그러나 작성한 오브젝트를 언제든지 참조 해제하여 메모리를 해방할 수 있습니다.->누출은 없습니다.하지만 그의 해결책은 쓰레기 수집기를 끝없는 루프로 강제함으로써 효과적으로 "파쇄"하는 것이기 때문에 그의 대답은 꽤 비슷합니다.
긴 답은 JNI를 사용하여 Java용 라이브러리를 작성하면 메모리 누수가 발생할 수 있다는 것입니다.JNI는 메모리 관리를 수동으로 할 수 있기 때문에 메모리 누수가 발생할 수 있습니다.이 라이브러리를 호출하면 Java 프로세스에서 메모리가 누출됩니다.또는 JVM에서 메모리가 손실되도록 JVM에 버그가 있을 수 있습니다.JVM에는 버그가 있을 수 있습니다.가비지 컬렉션은 그다지 중요하지 않기 때문에 알려진 버그가 있을 수 있습니다.하지만 여전히 버그입니다.이것은 설계상 불가능합니다.이러한 버그로 인해 영향을 받는 Java 코드를 요구할 수 있습니다.죄송하지만 저는 모릅니다.다음 Java 버전에서는 버그가 아닐 수도 있습니다.
다음은 http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29을 통한 간단한/사악한 예입니다.
public class StringLeaker
{
private final String muchSmallerString;
public StringLeaker()
{
// Imagine the whole Declaration of Independence here
String veryLongString = "We hold these truths to be self-evident...";
// The substring here maintains a reference to the internal char[]
// representation of the original string.
this.muchSmallerString = veryLongString.substring(0, 1);
}
}
하위 문자열은 원본 문자열의 내부 표현을 의미하기 때문에 원본 문자열은 메모리에 남아 있습니다.따라서 StringLeaker를 플레이하고 있는 한 오리지널 스트링 전체를 메모리에 저장할 수 있습니다.단, 1개의 스트링만을 사용하고 있다고 생각할 수도 있습니다.
원래 문자열에 대한 불필요한 참조가 저장되지 않도록 하려면 다음과 같은 작업을 수행합니다.
...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...
나쁜 점을 더하기 위해.intern()
서브스트링:
...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...
이렇게 하면 StringLeaker 인스턴스가 폐기된 후에도 원래 긴 문자열과 파생된 하위 문자열이 모두 메모리에 유지됩니다.
GUI 코드의 일반적인 예로는 위젯/컴포넌트를 작성하고 수신기를 정적/어플리케이션 범위 개체에 추가한 후 위젯이 파괴되었을 때 수신기를 제거하지 않는 경우가 있습니다.메모리 누수뿐만 아니라 무엇을 듣고 있든 이벤트가 발생하면 오래된 청취자가 모두 호출됩니다.
서블릿 컨테이너(Tomcat, Jetty, GlassFish 등)에서 실행 중인 웹 애플리케이션을 모두 사용할 수 있습니다.애플리케이션을 10~20회 연속으로 재구현합니다(서버의 autodeploy 디렉토리에서 WAR 를 터치하는 것만으로 충분합니다).
실제로 테스트한 사람이 없는 한, 애플리케이션이 스스로 정리하지 않았기 때문에 몇 번 재구현 후 Out Of Memory Error가 발생할 가능성이 높습니다.이 테스트에서는 서버에서 버그를 발견할 수도 있습니다.
문제는 컨테이너의 수명이 애플리케이션의 수명보다 길다는 것입니다.응용 프로그램의 개체 또는 클래스에 대한 컨테이너의 모든 참조가 가비지 수집될 수 있는지 확인해야 합니다.
웹 응용 프로그램의 배포 해제에서 살아남은 참조가 하나만 있는 경우 해당 클래스로더와 그 결과 웹 응용 프로그램의 모든 클래스가 가비지 수집될 수 없습니다.
응용 프로그램에 의해 시작된 스레드, ThreadLocal 변수, 로깅 추가는 클래스로더 누수의 원인이 되는 일반적인 원인 중 하나입니다.
어쩌면 JNI를 통해 외부 네이티브 코드를 사용해서?
순수 자바로는 거의 불가능합니다.
그러나 이는 "표준" 유형의 메모리 누수에 관한 것으로, 더 이상 메모리에 액세스할 수 없지만 여전히 애플리케이션에 의해 메모리가 소유됩니다.대신 사용되지 않은 개체에 대한 참조를 유지하거나 나중에 닫지 않고 스트림을 열 수 있습니다.
PermGen 및 XML 해석과 관련하여 메모리 누설이 발생한 적이 있습니다.우리가 사용한 XML 파서(어느 쪽인지 기억이 나지 않는다)는 비교를 빠르게 하기 위해 태그 이름에 String.intern()을 실행했습니다.고객 중 한 명이 데이터 값을 XML 속성이나 텍스트가 아닌 tagname으로 저장하는 아이디어를 가지고 있었기 때문에 다음과 같은 문서를 작성했습니다.
<data>
<1>bla</1>
<2>foo</>
...
</data>
실제로 숫자가 아닌 더 긴 텍스트 ID(약 20자)를 사용했는데, 이 ID는 독특하고 하루에 1000만-1500만 개의 비율로 들어왔다.이로 인해 하루에 200MB의 쓰레기가 발생하는데, 이 쓰레기는 두 번 다시 필요하지 않으며, (PermGen에 있기 때문에) GCed도 발생하지 않습니다.펌겐은 512MB로 설정되어 있기 때문에 Out-of-Memory Exception(OOME; 메모리 부족 예외)이 도착할 때까지2일 정도 걸렸어요
메모리 리크가 뭐죠?
- 버그나 디자인 불량으로 인해 발생합니다.
- 그것은 기억의 낭비다.
- 시간이 지날수록 더 심해진다.
- 가비지 컬렉터가 치료할 수 없습니다.
일반적인 예:
오브젝트 캐시는 일을 망치기 위한 좋은 출발점입니다.
private static final Map<String, Info> myCache = new HashMap<>();
public void getInfo(String key)
{
// uses cache
Info info = myCache.get(key);
if (info != null) return info;
// if it's not in cache, then fetch it from the database
info = Database.fetch(key);
if (info == null) return null;
// and store it in the cache
myCache.put(key, info);
return info;
}
캐시가 점점 더 커집니다.그리고 곧 전체 데이터베이스가 메모리로 흡수됩니다.보다 나은 설계에서는 LRUMap(최근에 사용한 객체만 캐시에 보관)을 사용합니다.
물론, 상황을 훨씬 더 복잡하게 만들 수 있습니다.
- 스레드 로컬 구성을 사용합니다.
- 더 복잡한 참조 트리를 추가합니다.
- 또는 서드파티 라이브러리로 인한 누출.
자주 일어나는 일:
이 Info 객체에 다른 객체에 대한 참조가 있으면 다른 객체에 대한 참조가 다시 표시됩니다.어떤 면에서는 메모리 누전(설계 불량으로 인한)이라고 생각할 수도 있습니다.
인터뷰 진행자는 아마도 아래 코드와 같은 순환 참조를 찾고 있었을 것입니다(참조 카운트를 사용한 매우 오래된 JVM에서만 메모리가 누출됩니다).이러한 정보는 더 이상 존재하지 않습니다.하지만 이는 매우 애매한 질문이기 때문에 JVM 메모리 관리에 대한 이해를 과시할 수 있는 절호의 기회입니다.
class A {
B bRef;
}
class B {
A aRef;
}
public class Main {
public static void main(String args[]) {
A myA = new A();
B myB = new B();
myA.bRef = myB;
myB.aRef = myA;
myA=null;
myB=null;
/* at this point, there is no access to the myA and myB objects, */
/* even though both objects still have active references. */
} /* main */
}
그 후, 레퍼런스 카운트를 실시하면, 상기의 코드로 메모리가 리크 되는 것을 설명할 수 있습니다.그러나 대부분의 최신 JVM은 더 이상 참조 카운트를 사용하지 않습니다.대부분의 경우 스위프 가비지 컬렉터를 사용하여 실제로 이 메모리를 수집합니다.
다음으로 기본 네이티브 리소스가 있는 개체 생성에 대해 다음과 같이 설명할 수 있습니다.
public class Main {
public static void main(String args[]) {
Socket s = new Socket(InetAddress.getByName("google.com"),80);
s=null;
/* at this point, because you didn't close the socket properly, */
/* you have a leak of a native descriptor, which uses memory. */
}
}
그러면 엄밀히 말하면 메모리 누수라고 설명할 수 있지만, 실제로는 누수는 JVM의 네이티브코드가 기본 네이티브리소스를 할당하고 있기 때문에 발생합니다.이러한 네이티브리소스는 Java 코드로 해방되지 않았습니다.
결국 최신 JVM을 사용하는 경우 JVM의 일반적인 인식 범위를 벗어나는 네이티브 리소스를 할당하는 Java 코드를 작성해야 합니다.
나는 아무도 내부 수업 예제를 사용하지 않는 것이 재미있다고 생각했다.내부 클래스가 있는 경우 기본적으로 포함된 클래스에 대한 참조가 유지됩니다.물론 기술적으로 메모리 누수는 아닙니다.Java가 최종적으로 메모리를 정리하기 때문입니다.그러나 이로 인해 클래스가 예상보다 오래 걸릴 수 있습니다.
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
여기서 Example1을 호출하여 Example2가 Example1을 파기해도 Example1 오브젝트에 대한 링크는 기본적으로 남아 있습니다.
public class Referencer {
public static Example2 GetAnExample2() {
Example1 ex = new Example1();
return ex.getNewExample2();
}
public static void main(String[] args) {
Example2 ex = Referencer.GetAnExample2();
// As long as ex is reachable; Example1 will always remain in memory.
}
}
변수가 특정 시간 이상 존재할 경우 Java는 항상 존재할 것으로 가정하고 코드로 도달할 수 없는 경우 이를 정리하려고 하지 않는다는 소문도 들었습니다.하지만 그것은 완전히 검증되지 않았다.
최근 log4j로 인해 메모리 누수가 발생하였습니다.
Log4j에는 NDC(Nested Diagnostic Context)라고 불리는 메커니즘이 있습니다.NDC는 인터리브 로그 출력을 다른 소스와 구별하는 도구입니다.NDC가 동작하는 입도는 스레드이기 때문에 로그 출력을 다른 스레드와 구분합니다.
스레드 고유의 태그를 저장하기 위해 log4j의 NDC 클래스는 스레드 ID가 아닌 스레드 오브젝트 자체에 의해 키 지정되는 해시 테이블을 사용합니다.따라서 NDC 태그가 메모리에 남아 있을 때까지 스레드 오브젝트에 걸려 있는 모든 오브젝트도 메모리에 남습니다.웹 애플리케이션에서는 NDC를 사용하여 로그 출력에 요청 ID를 사용하여 태그를 지정하여 로그와 단일 요청을 구분합니다.NDC 태그를 스레드에 연관짓는 컨테이너도 요청에서 응답을 반환하는 동안 NDC 태그를 제거합니다.요청 처리 중에 다음 코드와 같은 하위 스레드가 생성되었을 때 문제가 발생했습니다.
pubclic class RequestProcessor {
private static final Logger logger = Logger.getLogger(RequestProcessor.class);
public void doSomething() {
....
final List<String> hugeList = new ArrayList<String>(10000);
new Thread() {
public void run() {
logger.info("Child thread spawned")
for(String s:hugeList) {
....
}
}
}.start();
}
}
따라서 생성된 인라인 스레드에 NDC 컨텍스트가 관련지어졌습니다.이 NDC 컨텍스트의 키였던 스레드오브젝트는 hugeList 오브젝트가 매달려 있는 인라인스레드입니다따라서 스레드가 동작을 종료한 후에도 nDC 컨텍스트 Hastable에 의해 guargeList에 대한 참조가 활성 상태로 유지되어 메모리 누수가 발생합니다.
정적 맵을 만들고 하드 참조를 계속 추가합니다.그것들은 결코 쓰레기 수거가 되지 않을 것이다.
public class Leaker {
private static final Map<String, Object> CACHE = new HashMap<String, Object>();
// Keep adding until failure.
public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}
누구나 네이티브 코드 루트를 항상 잊어버립니다.누출에 대한 간단한 공식은 다음과 같습니다.
- 네이티브 메서드를 선언합니다.
- 네이티브 메서드에서는
malloc
전화하지 마세요.free
. - 네이티브 메서드를 호출합니다.
네이티브 코드의 메모리 할당은 JVM 힙에서 이루어집니다.
클래스의 finalize 메서드에서 클래스의 새 인스턴스를 만들어 이동 메모리 누수를 만들 수 있습니다.파이널라이저가 여러 인스턴스를 생성할 경우 보너스 포인트입니다.다음은 힙 크기에 따라 몇 초에서 몇 분 사이에 힙 전체를 누수하는 간단한 프로그램입니다.
class Leakee {
public void check() {
if (depth > 2) {
Leaker.done();
}
}
private int depth;
public Leakee(int d) {
depth = d;
}
protected void finalize() {
new Leakee(depth + 1).check();
new Leakee(depth + 1).check();
}
}
public class Leaker {
private static boolean makeMore = true;
public static void done() {
makeMore = false;
}
public static void main(String[] args) throws InterruptedException {
// make a bunch of them until the garbage collector gets active
while (makeMore) {
new Leakee(0).check();
}
// sit back and watch the finalizers chew through memory
while (true) {
Thread.sleep(1000);
System.out.println("memory=" +
Runtime.getRuntime().freeMemory() + " / " +
Runtime.getRuntime().totalMemory());
}
}
}
finalize() 메서드를 덮어쓰면 오브젝트를 부활시켜 finalize()가 참조를 어딘가에 저장할 수 있습니다.가비지 컬렉터는 오브젝트에서 한 번만 호출되므로 그 이후에는 오브젝트가 파기되지 않습니다.
최근에 더 미묘한 종류의 자원 유출을 발견했어요.클래스 로더의 getResourceAsStream을 통해 리소스를 열었는데 입력 스트림 핸들이 닫히지 않았습니다.
음, 당신은 바보라고 말할지도 몰라요.
이 방법을 사용하면 JVM의 힙에서가 아니라 기본 프로세스의 힙 메모리를 누출할 수 있습니다.
Java 코드에서 참조되는 파일이 포함된 jar 파일만 있으면 됩니다.jar 파일이 클수록 메모리가 더 빨리 할당됩니다.
이러한 항아리는 다음 클래스로 쉽게 만들 수 있습니다.
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class BigJarCreator {
public static void main(String[] args) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
zos.putNextEntry(new ZipEntry("resource.txt"));
zos.write("not too much in here".getBytes());
zos.closeEntry();
zos.putNextEntry(new ZipEntry("largeFile.out"));
for (int i=0 ; i<10000000 ; i++) {
zos.write((int) (Math.round(Math.random()*100)+20));
}
zos.closeEntry();
zos.close();
}
}
BigJarCreator.java라는 파일에 붙여넣기만 하면 다음 명령행에서 컴파일하여 실행할 수 있습니다.
javac BigJarCreator.java
java -cp . BigJarCreator
Et voila: 현재 작업 디렉토리에서 2개의 파일이 들어 있는 jar 아카이브를 찾을 수 있습니다.
두 번째 클래스를 만듭니다.
public class MemLeak {
public static void main(String[] args) throws InterruptedException {
int ITERATIONS=100000;
for (int i=0 ; i<ITERATIONS ; i++) {
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
}
System.out.println("finished creation of streams, now waiting to be killed");
Thread.sleep(Long.MAX_VALUE);
}
}
이 클래스는 기본적으로 아무것도 하지 않고 참조되지 않은 InputStream 개체를 만듭니다.이러한 개체는 즉시 가비지가 수집되므로 힙 크기에 영향을 주지 않습니다.이 예에서는 기존 리소스를 jar 파일에서 로드하는 것이 중요하며, 이 경우 크기가 중요합니다.
의심스러운 경우 위의 클래스를 컴파일하여 시작하지만 적절한 힙 크기(2MB)를 선택해야 합니다.
javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
여기서 OOM 에러는 발생하지 않습니다.레퍼런스는 보관 유지되지 않기 때문에 위의 예에서 ITERATIONs를 선택한 크기에 관계없이 응용 프로그램은 계속 실행됩니다.애플리케이션이 wait 명령어를 실행하지 않는 한 프로세스의 메모리 소비량(상단(RES/RSS) 또는 프로세스 탐색기)은 증가합니다.위의 설정에서는 약 150MB의 메모리를 할당합니다.
응용 프로그램을 안전하게 재생하려면 만든 위치에서 입력 스트림을 닫으십시오.
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
프로세스는 반복 횟수에 관계없이 35MB를 초과할 수 없습니다.
꽤 단순하고 놀랍습니다.
많은 사람들이 제안했듯이 자원 유출은 JDBC의 사례처럼 발생하기가 매우 쉽습니다.실제 메모리 누수는 조금 더 어렵습니다.특히 JVM의 파손된 비트에 의존하지 않으면...
설치 공간이 매우 큰 개체를 만들고 액세스할 수 없다는 생각은 실제 메모리 누수가 아닙니다.아무 것도 접근할 수 없으면 쓰레기 수집이 되고, 뭔가가 접근할 수 있다면 유출이 아니라...
하지만 예전엔 잘 먹혔던 방법 중 하나는 - 아직도 그럴지는 모르겠지만 - 세 개의 깊은 원형 체인을 갖는 것이다.오브젝트 A와 마찬가지로 오브젝트 B는 오브젝트 C를 참조하고 오브젝트 C는 오브젝트 A를 참조합니다.GC는 A <--> B와 같이 2개의 딥 체인을 안전하게 수집할 수 있는 것은 A와 B가 다른 어떤 것으로도 접근할 수 없지만 3방향 체인을 처리할 수 없는 경우라는 것을 충분히 알고 있었습니다.
메모리 누설이 발생할 가능성이 있는 다른 방법은 다음과 같은 참조를 유지하는 것입니다.Map.Entry<K,V>
의TreeMap
.
이것이 왜 에만 적용되는지는 판단하기 어렵다.TreeMap
그러나 실장을 보면 다음과 같은 이유가 있을 수 있습니다.TreeMap.Entry
는 형제자매에 대한 참조를 저장합니다.따라서,TreeMap
수집 준비가 되었지만, 다른 클래스는 그 중 하나를 참조하고 있습니다.Map.Entry
그러면 전체 맵이 메모리에 유지됩니다.
실제 시나리오:
큰 값을 반환하는 DB 쿼리가 있다고 가정해 보십시오.TreeMap
데이터 구조주로 사용하는 것은TreeMap
s는 요소 삽입 순서가 유지됩니다.
public static Map<String, Integer> pseudoQueryDatabase();
쿼리가 여러 번 호출된 경우 각 쿼리에 대해 (따라서 각 쿼리에 대해)Map
returned)를 저장했습니다.Entry
어딘가에서, 기억은 계속 자라날 것이다.
다음 래퍼 클래스를 고려합니다.
class EntryHolder {
Map.Entry<String, Integer> entry;
EntryHolder(Map.Entry<String, Integer> entry) {
this.entry = entry;
}
}
응용 프로그램:
public class LeakTest {
private final List<EntryHolder> holdersCache = new ArrayList<>();
private static final int MAP_SIZE = 100_000;
public void run() {
// create 500 entries each holding a reference to an Entry of a TreeMap
IntStream.range(0, 500).forEach(value -> {
// create map
final Map<String, Integer> map = pseudoQueryDatabase();
final int index = new Random().nextInt(MAP_SIZE);
// get random entry from map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue().equals(index)) {
holdersCache.add(new EntryHolder(entry));
break;
}
}
// to observe behavior in visualvm
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static Map<String, Integer> pseudoQueryDatabase() {
final Map<String, Integer> map = new TreeMap<>();
IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
return map;
}
public static void main(String[] args) throws Exception {
new LeakTest().run();
}
}
각각 다음에pseudoQueryDatabase()
콜,map
인스턴스는 수집 준비가 되어 있어야 하지만 적어도1개의 인스턴스가 있기 때문에 그렇게 되지 않습니다.Entry
다른 곳에 저장되어 있습니다.
고객님의 요구에 따라jvm
초기 단계에서 어플리케이션이 크래쉬 할 수 있습니다.OutOfMemoryError
.
이걸 보면 알 수 있어요visualvm
어떻게 메모리가 계속 증가하는지를 그래프로 표시합니다.
해시된 데이터 구조에서는 동일한 현상이 발생하지 않습니다(HashMap
).
이 그래프는 다음 명령어를 사용하는 경우의HashMap
.
해결책?키/값을 직접 저장하기만 하면 됩니다(아마도 이미 저장했을 것입니다).Map.Entry
.
나는 여기에 좀 더 광범위한 벤치마크를 썼다.
면접관은 순환 참조 솔루션을 찾고 있었을 가능성이 있습니다.
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
이것은 참조 가비지 컬렉터의 전형적인 문제입니다.그런 다음 JVM이 이러한 제한이 없는 훨씬 더 정교한 알고리즘을 사용한다는 점을 정중하게 설명합니다.
스레드는 종료될 때까지 수집되지 않습니다.그것들은 쓰레기 수집의 뿌리 역할을 한다.이러한 개체는 단순히 해당 개체를 잊거나 참조를 지우는 것만으로 회수되지 않는 몇 안 되는 개체 중 하나입니다.
고려사항: 워커 스레드를 종료하는 기본 패턴은 스레드에서 볼 수 있는 조건 변수를 설정하는 것입니다.스레드는 변수를 주기적으로 확인하고 종료 신호로 사용할 수 있습니다.변수가 선언되지 않은 경우volatile
변수에 대한 변경은 스레드에 의해 인식되지 않을 수 있으므로 종료 여부를 알 수 없습니다.또는 일부 스레드가 공유 개체를 업데이트하려고 하지만 개체를 잠그려고 할 때 교착 상태가 되는 경우를 상상해 보십시오.
스레드가 몇 개밖에 없는 경우 프로그램이 제대로 작동하지 않기 때문에 이러한 버그는 쉽게 알 수 있습니다.필요에 따라 더 많은 스레드를 생성하는 스레드 풀이 있는 경우 오래된 스레드 또는 스택된 스레드가 인식되지 않고 무한히 누적되어 메모리 누수가 발생할 수 있습니다.스레드에서는 어플리케이션 내의 다른 데이터를 사용할 가능성이 높기 때문에 직접 참조하는 데이터는 수집되지 않습니다.
장난감의 예:
static void leakMe(final Object object) {
new Thread() {
public void run() {
Object o = object;
for (;;) {
try {
sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {}
}
}
}.start();
}
불러System.gc()
원하는 만큼, 하지만 그 대상은leakMe
절대 죽지 않을 거야
Java에는 메모리 누수의 좋은 예가 많이 있습니다.이 답변에서는 그 중 2가지를 언급하겠습니다.
예 1:
다음은 Effective Java, Third Edition(항목 7: 사용되지 않는 개체 참조 제거)의 메모리 누수의 좋은 예입니다.
// Can you spot the "memory leak"?
public class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size];
}
/*** Ensure space for at least one more element, roughly* doubling the capacity each time the array needs to grow.*/
private void ensureCapacity() {
if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
이 책에서는 이 구현으로 인해 메모리 누수가 발생하는 이유를 설명합니다.
스택이 커지고 축소되면 스택에서 삭제된 오브젝트는 스택을 사용하는 프로그램에서 더 이상 참조가 없는 경우에도 가비지 수집되지 않습니다.이는 스택이 이러한 오브젝트에 대한 오래된 참조를 유지하기 때문입니다.사용되지 않는 참조는 단순히 다시 참조되지 않는 참조일 뿐입니다.이 경우 요소 배열의 "액티브 부분" 이외의 참조는 사용되지 않습니다.활성 부분은 인덱스가 크기보다 작은 요소로 구성됩니다.
이 메모리의 누전에 대처하기 위한 대처법을 다음에 나타냅니다.
이러한 종류의 문제에 대한 수정은 간단합니다. 참조가 사용되지 않게 되면 null로 처리됩니다.Stack 클래스의 경우 아이템에 대한 참조는 스택에서 삭제되는 즉시 사용되지 않게 됩니다.pop 메서드의 수정 버전은 다음과 같습니다.
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
하지만 메모리 누수가 발생하는 것을 어떻게 예방할 수 있을까요?이 책의 좋은 경고는 다음과 같습니다.
일반적으로 클래스가 자신의 메모리를 관리할 때마다 프로그래머는 메모리 누수에 대해 경계해야 합니다.요소가 해방될 때마다 요소에 포함된 객체 참조는 모두 무효로 해야 합니다.
예 2:
옵서버 패턴으로 인해 메모리 누수가 발생할 수도 있습니다.이 패턴에 대해서는, 「Overserver pattern」링크를 참조해 주세요.
다음은 옵저버 패턴의 1가지 구현입니다.
class EventSource {
public interface Observer {
void update(String event);
}
private final List<Observer> observers = new ArrayList<>();
private void notifyObservers(String event) {
observers.forEach(observer -> observer.update(event)); //alternative lambda expression: observers.forEach(Observer::update);
}
public void addObserver(Observer observer) {
observers.add(observer);
}
public void scanSystemIn() {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
notifyObservers(line);
}
}
}
이 실장에서는,EventSource
옵저버 설계 패턴에서 관찰 가능한 것으로, 에의 링크를 유지할 수 있습니다.Obeserver
이 링크는 에서 삭제되지 않습니다.observers
에 참가하다.EventSource
그래서 그것들은 쓰레기 수집기에 의해 수집되지 않습니다.이 문제에 대처하기 위한 해결책 중 하나는 클라이언트에게 전술한 옵서버를 삭제하기 위한 다른 방법을 제공하는 것입니다.observers
더 이상 관찰자가 필요하지 않을 때 필드:
public void removeObserver(Observer observer) {
observers.remove(observer);
}
언급URL : https://stackoverflow.com/questions/6470651/how-can-i-create-a-memory-leak-in-java
'source' 카테고리의 다른 글
C의 값 주소 또는 포인터를 인쇄합니다. (0) | 2022.07.23 |
---|---|
GCC에서 x86 어셈블리의 Intel 구문을 사용할 수 있습니까? (0) | 2022.07.23 |
포획은 루프 안쪽으로 할까요, 아니면 바깥쪽으로 할까요? (0) | 2022.07.23 |
Vue 3 - 대체 Vue.delete (0) | 2022.07.23 |
C기준은 임의의 값을 포인터에 할당하여 증가시키는 것을 허용합니까? (0) | 2022.07.23 |