Stack Overflow ในการพัฒนาแอปมือถือ — คืออะไร สาเหตุ และวิธีการป้องกัน

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-03-29 เวลาอ่าน: 9 นาที

Stack Overflow เป็นข้อผิดพลาดสแต็กการเรียกทำงานล้น (java.lang.StackOverflowError) ที่เกิดขึ้นเมื่อความลึกสูงสุดของสแต็กของเธรดเกินขีดจำกัด ตาม Java Virtual Machine Specification ความลึกสแต็กทั่วไปใน JVM คือ 1024 เฟรมสำหรับระบบ 64 บิต สาเหตุหลัก คือการเรียกซ้ำแบบไม่สิ้นสุดโดยไม่มีเงื่อนไขพื้นฐาน

ประเด็นสำคัญ

  • StackOverflowError — ข้อผิดพลาด JVM เมื่อเกินขีดจำกัดความลึกสแต็กการเรียก
  • ความลึกสแต็ก จำกัดอยู่ที่ 512–2048 เฟรมขึ้นอยู่กับการกำหนดค่า
  • การเรียกซ้ำแบบไม่สิ้นสุด เป็นสาเหตุที่พบบ่อยที่สุดของ StackOverflowError
  • การเรียกซ้ำท้าย ไม่ได้รับการปรับให้เหมาะสมใน JVM ซึ่งแตกต่างจากภาษาเชิงฟังก์ชัน
  • การแทนที่แบบวนซ้ำ เป็นวิธีที่เชื่อถือได้ในการป้องกันการล้น

Stack Overflow คืออะไร

StackOverflowError เป็นข้อผิดพลาดร้ายแรงของ Java Virtual Machine (JVM) หรือ Android Runtime (ART) ที่เกิดขึ้นเมื่อสแต็กการเรียกของเธรดถึงความลึกสูงสุดที่อนุญาต ซึ่งแตกต่างจาก OutOfMemoryError (หน่วยความจำ Heap ไม่พอ) StackOverflowError เกี่ยวข้องกับพื้นที่หน่วยความจำอื่น — สแต็ก ที่เก็บเฟรมการเรียกเมธอดและตัวแปรท้องถิ่น

ทุกครั้งที่เรียก เมธอด จะสร้างเฟรมในสแต็ก: ที่อยู่ส่งกลับ พารามิเตอร์ และตัวแปรท้องถิ่น เมื่อกลับจากเมธอด เฟรมจะถูกทำลาย ถ้าเมธอดเรียกตัวเอง (การเรียกซ้ำ) โดยไม่มีเงื่อนไขพื้นฐาน เฟรมจะสะสมจนกว่าสแต็กจะเต็ม JVM ไม่สามารถจัดสรรเฟรมใหม่ได้และโยน StackOverflowError พร้อมข้อความ «null» (ใน Java) หรือพร้อมการแสดงบรรทัดสแต็กที่ซ้ำกันไม่สิ้นสุด

ขนาดสแต็กของเธรดถูกกำหนดเมื่อสร้างและไม่เปลี่ยนแปลงระหว่างการทำงาน ใน Android ขนาดสแต็กทั่วไปของเธรดหลักคือ 32–48 KB ซึ่งให้ความลึกประมาณ 512–1024 เฟรมสำหรับเมธอดที่ไม่มีตัวแปรท้องถิ่นจำนวนมาก สำหรับเธรดพื้นหลัง ขนาดเริ่มต้นจะเล็กกว่า — 16–24 KB

สแต็กการเรียกทำงานอย่างไร

สแต็กการเรียก (Call Stack) เป็นโครงสร้างข้อมูล LIFO (Last In, First Out) ที่จัดการลำดับการทำงานของเมธอด ทุกครั้งที่โปรแกรมเรียกเมธอด JVM จะสร้างเฟรมในสแต็กและวางไว้ด้านบน เมื่อเมธอดเสร็จสิ้น เฟรมจะถูกนำออก

แต่ละเฟรม ประกอบด้วย: สแต็กตัวถูกดำเนินการ (สำหรับคำสั่งไบต์โค้ด), อาร์เรย์ของตัวแปรท้องถิ่น (รวมถึง this), การอ้างอิงไปยัง Constant Pool และที่อยู่ส่งกลับ ยิ่งเมธอดมีตัวแปรท้องถิ่นมากเท่าไหร่ ขนาดเฟรมก็ใหญ่ขึ้นเท่านั้น และสามารถเรียกเมธอดได้น้อยลงก่อนที่สแต็กจะเต็ม เมธอดที่มี 10 พารามิเตอร์และ 20 ตัวแปรท้องถิ่นใช้พื้นที่มากกว่าเมธอดที่ไม่มีพารามิเตอร์ประมาณ 3 เท่า

บน Android ART ใช้การใช้งานสแต็กของตัวเอง ซึ่งแตกต่างจาก Desktop JVM ART สามารถเพิ่มสแต็กแบบไดนามิกภายในขอบเขตที่กำหนด แต่ยังคงมีขีดจำกัดตายตัวสำหรับแต่ละเธรด เธรดหลัก (เธรด UI) มีสแต็กใหญ่ที่สุด เพราะจัดการวงจรชีวิตทั้งหมดของ Activity และการประมวลผลเหตุการณ์

kotlin
// การเรียกซ้ำที่นำไปสู่ StackOverflowError
fun recursiveCall(depth: Int): Int {
    return recursiveCall(depth + 1) // ไม่มีเงื่อนไขพื้นฐาน
}

// การเรียกจะทำให้ StackOverflowError ที่ความลึก ~1000
recursiveCall(0)

สาเหตุหลักของสแต็กล้น

ห้า สถานการณ์ทั่วไปนำไปสู่ StackOverflowError ในแอปพลิเคชันมือถือ ส่วนใหญ่เกี่ยวข้องกับการเรียกซ้ำ แต่ก็มีสาเหตุที่ชัดเจนน้อยกว่าด้วย

การเรียกซ้ำแบบไม่สิ้นสุดโดยไม่มีเงื่อนไขพื้นฐาน

สาเหตุ ที่พบบ่อยที่สุด นักพัฒนาเขียนเมธอดแบบเรียกซ้ำโดยไม่มีเงื่อนไขหยุด หรือมีเงื่อนไขที่ไม่เคยเป็น true แต่ละเรียกเพิ่มเฟรม และสแต็กเต็มใน 500–2000 ครั้งขึ้นอยู่กับขนาดเฟรม ตัวอย่างทั่วไป: การคำนวณแฟกทอเรียล n! โดยไม่ตรวจสอบ n == 0

ตรวจสอบ เงื่อนไขพื้นฐาน ที่จุดเริ่มต้นของเมธอดแบบเรียกซ้ำแต่ละตัว ใน Kotlin ให้ใช้ require() หรือ check() เพื่อตรวจสอบพารามิเตอร์เมื่อเริ่มต้น สำหรับการเรียกซ้ำลึก (มากกว่า 100 ระดับ) ให้พิจารณาแทนที่ด้วยวิธีการแบบวนซ้ำ

การพึ่งพาแบบวงจรในคอนสตรัคเตอร์

คลาส A สร้างอินสแตนซ์ของ B คลาส B สร้างอินสแตนซ์ของ A — นี่คือการพึ่งพาแบบวงจรในคอนสตรัคเตอร์ เมื่อพยายามสร้าง A คอนสตรัคเตอร์ของ B จะถูกเรียก ซึ่งเรียกคอนสตรัคเตอร์ของ A และต่อไปจนถึง StackOverflowError เฟรมเวิร์ก DI (Dagger, Hilt) ตรวจพบวงจรเหล่านี้ในเวลาคอมไพล์ แต่การสร้างออบเจ็กต์ด้วยตนเองไม่สามารถตรวจจับได้

ใช้ Dependency Injection พร้อมกราฟการพึ่งพา: Dagger หรือ Koin ตรวจสอบวงจรในเวลาบิลด์ ถ้าหลีกเลี่ยงวงจรไม่ได้ ให้แทนที่การพึ่งพาโดยตรงด้วยอินเทอร์เฟซที่มีการเริ่มต้นแบบขี้เกียจหรือ Provider factory

kotlin
// การพึ่งพาแบบวงจร — StackOverflowError
class A(private val b: B)
class B(private val a: A)

// วิธีแก้แบบขี้เกียจ
class A(private val bProvider: Provider<B>)

การเรียกซ้ำลึกในการท่องกราฟ

การท่อง ต้นไม้ View (ViewGroup.getChildAt()), ระบบไฟล์ หรือโครงสร้าง JSON ผ่านการเรียกซ้ำอาจเกินขีดจำกัดสแต็กที่ความลึกมากกว่า 500–1000 องค์ประกอบ ViewGroup Android ที่มีการซ้อน 20 ระดับนั้นหายาก แต่การแยกวิเคราะห์ JSON แบบเรียกซ้ำที่มีออบเจ็กต์ซ้อน 2000 ตัวนั้นเป็นสถานการณ์จริง

แทนที่ การท่องแบบเรียกซ้ำ ด้วยการท่องแบบวนซ้ำโดยใช้ Stack<T> หรือ ArrayDeque ที่ชัดเจน ซึ่งจะกำจัดความเสี่ยงของสแต็กล้นโดยสิ้นเชิง เนื่องจากออบเจ็กต์ใน Heap ไม่ถูกจำกัดด้วยสแต็ก BFS (การค้นหาในแนวกว้าง) ผ่าน Queue ก็แก้ปัญหาได้เช่นกัน

การจัดการ onConfigurationChanged ที่ไม่ถูกต้อง

สาเหตุ เฉพาะของ Android: การเรียกเมธอดวงจรชีวิตแบบวงจรเมื่อจัดการการกำหนดค่าไม่ถูกต้อง ตัวอย่างเช่น การเรียก recreate() ภายใน onConfigurationChanged ซึ่งเรียก onConfigurationChanged อีกครั้ง และต่อไปจนถึง StackOverflowError เช่นเดียวกัน: setContentView() ภายใน onLayout() ซึ่งกระตุ้นการวัดและเค้าโครงอีกครั้ง

อย่าเรียก recreate() ภายในเมธอดที่เกี่ยวข้องกับการเปลี่ยนแปลงการกำหนดค่า เพื่ออัปเดต UI เมื่อเปลี่ยนธีม ให้ใช้ setTheme() โดยไม่ต้อง recreate สำหรับการเปลี่ยนแนวแบบไดนามิก — เรียก requestOrientation() หนึ่งครั้ง โดยไม่มีแฟล็กในการกำหนดค่า

การทำให้เป็นอนุกรมกับการอ้างอิงแบบวงจร

Gson, Moshi หรือ Kotlin Serialization เมื่อพยายามทำให้ออบเจ็กต์ที่มีการอ้างอิงแบบวงจร (A อ้างอิง B, B อ้างอิง A) กลายเป็นอนุกรม จะเข้าสู่การเรียกซ้ำแบบไม่สิ้นสุดและล้มเหลวด้วย StackOverflowError นี่เป็นปัญหาทั่วไปเมื่อทำให้ Entity ที่มีความสัมพันธ์แบบสองทิศทาง (JPA, Room ที่มี ForeignKey) กลายเป็นอนุกรม

ใช้ @Transient, @JsonIgnore หรือ @kotlinx.serialization.Transient สำหรับด้านหนึ่งของวงจร สำหรับ Gson — JsonSerializer ที่มีขีดจำกัดความลึกชัดเจน สำหรับ Room — อย่าทำให้ Entity กลายเป็นอนุกรมโดยตรง ใช้ DTO mapper

วิธีวินิจฉัยและแก้ไข StackOverflowError

การวินิจฉัย StackOverflowError ง่ายกว่าข้อผิดพลาดหน่วยความจำอื่น: การติดตามสแต็กในกรณีส่วนใหญ่แสดงลำดับการเรียกที่ซ้ำกัน ซึ่งบ่งชี้ถึงการเรียกซ้ำทันที

การอ่านการติดตามสแต็ก

การติดตามสแต็ก ของ StackOverflowError มีลักษณะเฉพาะ: หลังจาก 200–500 บรรทัดแรก รูปแบบการเรียกเดียวกันจะเริ่มซ้ำกัน JVM จะตัดบรรทัดที่ซ้ำกันในตอนท้ายและแสดง «... 1234 more» จำนวนบรรทัดที่ไม่ซ้ำก่อน «...» แสดงความลึกของการเรียกซ้ำที่ทำให้เกิดข้อผิดพลาด

อ่าน บรรทัดแรก ของการติดตามสแต็ก — แสดงว่าเริ่มการซ้ำจากเมธอดไหน ค้นหาเมธอดที่เรียกตัวเองหรือสร้างห่วงโซ่การเรียกที่กลับมายังมัน แก้ไขเงื่อนไขพื้นฐานหรือแทนที่การเรียกซ้ำด้วยลูป

การเพิ่มขนาดสแต็ก (วิธีแก้ปัญหาชั่วคราว)

ชั่วคราว ปัญหาสามารถแก้ไขได้โดยเพิ่มขนาดสแต็กผ่านแฟล็ก JVM -Xss สำหรับ Android ขนาดสแต็กถูกกำหนดผ่าน AndroidManifest: android:largeHeap ไม่มีผลต่อสแต็ก การเพิ่มสแต็กเธรดในโค้ด: Thread(ThreadGroup, Runnable, name, stackSize) stackSize คือขนาดที่ต้องการเป็นไบต์

kotlin
// การสร้างเธรดด้วยสแต็กที่เพิ่มขึ้น
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()

สำคัญ: การเพิ่มสแต็กไม่ได้แก้ปัญหา แค่ชะลอมันออกไป ด้วยการเรียกซ้ำ 10,000 ระดับ สแต็ก 64 KB จะถูกแทนที่ด้วยสแต็ก 128 KB ให้ 20,000 ระดับ — แต่ข้อผิดพลาดจะยังคงเกิดขึ้น แค่ช้าลง วิธีแก้ไขที่ถูกต้องเท่านั้นคือการแทนที่การเรียกซ้ำแบบวนซ้ำ

การแทนที่การเรียกซ้ำด้วยการวนซ้ำ

อัลกอริทึม แบบวนซ้ำไม่ใช้สแต็กการเรียกเพื่อเก็บสถานะกลาง — เก็บไว้ใน Heap (Stack<T> หรือ ArrayDeque) การท่องต้นไม้ทวิภาค การคำนวณแฟกทอเรียล ฟีโบนักชี — การเรียกซ้ำใดๆ สามารถแปลงเป็นการวนซ้ำโดยใช้สแต็กที่ชัดเจน

kotlin
// การท่องต้นไม้แบบวนซ้ำ — ไม่มีความเสี่ยง StackOverflow
fun traverseIterative(root: Node?) {
    val stack = ArrayDeque<Node>()
    stack.push(root)
    while (stack.isNotEmpty()) {
        val node = stack.pop() ?: continue
        process(node)
        node.right?.let { stack.push(it) }
        node.left?.let { stack.push(it) }
    }
}

วิธีป้องกัน Stack Overflow

การป้องกัน StackOverflowError คือชุดของกฎและเครื่องมือที่ระบุวงจรการเรียกซ้ำที่อาจเกิดขึ้นก่อนที่จะถึงระบบผลิต

ขีดจำกัดความลึกการเรียกซ้ำในบิลด์ดีบัก

เพิ่ม ตัวนับความลึกป้องกันในเมธอดแบบเรียกซ้ำในบิลด์ดีบัก ถ้าความลึกเกินเกณฑ์ (เช่น 1000) ให้โยนข้อยกเว้นพร้อมข้อความที่ชัดเจน ซึ่งเปลี่ยน StackOverflowError ที่ติดตามไม่ได้ให้เป็นข้อยกเว้นทางธุรกิจที่เข้าใจได้

kotlin
fun safeRecursive(n: Int, depth: Int = 0): Int {
    if (depth > 1000) {
        throw IllegalStateException("การเรียกซ้ำเกิน 1000 ระดับแล้ว")
    }
    return if (n <= 1) n
           else safeRecursive(n - 1, depth + 1)
}

การวิเคราะห์โค้ดแบบคงที่

Detekt (Kotlin) และ Infer (Facebook) ค้นหาการเรียกซ้ำแบบไม่สิ้นสุดที่อาจเกิดขึ้นในระดับการวิเคราะห์แบบคงที่ Detekt มีกฎ PotentiallyInfiniteRecursion ที่เตือนเกี่ยวกับการเรียกตัวเองโดยไม่เปลี่ยนพารามิเตอร์ เปิดใช้งานในชุดกฎ CI และตั้งค่าระดับความรุนแรงเป็น error

การตรวจสอบโค้ดที่เน้นการเรียกซ้ำ

ในการ ตรวจสอบโค้ด ให้ใส่ใจกับ: เมธอดที่เรียกตัวเอง การเรียกซ้ำภายในแลมบ์ดา (ฟังก์ชันอินไลน์ของ Kotlin) การเรียกแบบวงจรระหว่างคลาสต่างๆ การเรียกซ้ำใน Property Delegate สำหรับแต่ละเมธอดแบบเรียกซ้ำ ให้ตรวจสอบ: มีเงื่อนไขพื้นฐานไหม พารามิเตอร์เปลี่ยนแปลงในแต่ละขั้นตอนไหม การเปลี่ยนแปลงพารามิเตอร์รับประกันการถึงเงื่อนไขพื้นฐานไหม

การแปลงการเรียกซ้ำท้าย (จำกัด)

Kotlin รองรับตัวปรับแต่ง tailrec: ถ้าเมธอดแบบเรียกซ้ำถูกทำเครื่องหมาย tailrec และการเรียกเป็นท้าย (การดำเนินการสุดท้าย) คอมไพเลอร์จะแปลงเป็นการวนซ้ำ อย่างไรก็ตาม tailrec ทำงานเฉพาะกับการเรียกตัวเอง (เมธอดเรียกตัวเองโดยตรง) ไม่ทำงานสำหรับการเรียกซ้ำร่วมกัน และไม่รองรับใน Kotlin เวอร์ชันที่เข้ากันได้กับ Android ก่อน 1.5

kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc
           else factorial(n - 1, acc * n) // การเรียกท้าย
}

คำถามที่พบบ่อย

สามารถจับ StackOverflowError ด้วย try-catch ได้ไหม?

ได้ แต่เฉพาะในระดับ Java Error ก็เหมือน Exception คือ Throwable อย่างไรก็ตาม หลังจาก StackOverflowError สแต็กเสียหาย — เฟรมที่ไม่พอดีไม่สามารถเสร็จสมบูรณ์ได้อย่างถูกต้อง การพยายามสร้างออบเจ็กต์ใหม่ในบล็อก catch อาจทำให้เกิด StackOverflowError อีกครั้ง

ขนาดสแต็กเริ่มต้นใน Android คือเท่าไหร่?

สำหรับเธรดหลัก — 32–48 KB สำหรับเธรดพื้นหลัง — 16–24 KB ขนาดที่แน่นอนขึ้นอยู่กับเวอร์ชัน Android และผู้ผลิตอุปกรณ์ ART ใช้การขยายสแต็กแบบไดนามิก แต่ไม่เกิน 2× ของค่าเริ่มต้น

การเรียกซ้ำท้ายสามารถป้องกัน StackOverflowError ได้ไหม?

ใน Kotlin — ได้ ถ้าเมธอดถูกทำเครื่องหมาย tailrec คอมไพเลอร์แปลงการเรียกซ้ำท้ายเป็นการวนซ้ำ กำจัดการเติบโตของสแต็กโดยสิ้นเชิง ใน Java การเรียกซ้ำท้ายไม่ได้รับการปรับให้เหมาะสมโดย JVM (ซึ่งแตกต่างจากภาษาเชิงฟังก์ชันอย่าง Scala)

ทำไม StackOverflowError เกิดขึ้นบนอีมูเลเตอร์แต่ไม่บนอุปกรณ์?

ขนาดสแต็ก บนอีมูเลเตอร์และอุปกรณ์จริงอาจแตกต่างกัน อีมูเลเตอร์ใช้ Desktop JVM ที่มีสแต็กทั่วไป 512–1024 KB ในขณะที่ Android ART ใช้ 32–48 KB ข้อผิดพลาดจะปรากฏบน ART กว่าบน Desktop JVM

StackOverflowError แตกต่างจาก OutOfMemoryError อย่างไร?

พื้นที่หน่วยความจำ: StackOverflowError เป็นข้อผิดพลาดสแต็ก (เฟรมเรียก), OutOfMemoryError เป็นข้อผิดพลาด Heap (ออบเจ็กต์) StackOverflowError เกือบทุกครั้งเกิดจากการเรียกซ้ำ ในขณะที่ OutOfMemoryError เกิดจากการรั่วไหลของหน่วยความจำหรือออบเจ็กต์ขนาดใหญ่

สรุป

  • StackOverflowError — สแต็กการเรียกทำงานล้นเมื่อเกินขีดจำกัดความลึกการเรียกซ้ำ
  • ความลึกสแต็กใน Android คือ 512–1024 เฟรมบนเธรดหลัก
  • การเรียกซ้ำแบบไม่สิ้นสุด เป็นสาเหตุหลัก; ตรวจสอบเงื่อนไขพื้นฐานในทุกเมธอดแบบเรียกซ้ำ
  • การพึ่งพาแบบวงจร ในคอนสตรัคเตอร์ — สาเหตุที่ชัดเจนน้อยกว่าแต่พบได้บ่อย
  • การแทนที่แบบวนซ้ำ ผ่าน Stack<T> ที่ชัดเจนกำจัดความเสี่ยงโดยสิ้นเชิง
  • tailrec ใน Kotlin แปลงการเรียกซ้ำท้ายเป็นการวนซ้ำในระดับคอมไพเลอร์
  • การวิเคราะห์แบบคงที่ (Detekt, Infer) ค้นหาการเรียกซ้ำไม่สิ้นสุดก่อนรันไทม์

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม