{"id":569893,"date":"2025-05-26T13:18:20","date_gmt":"2025-05-26T12:18:20","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=idea&#038;p=569893"},"modified":"2025-05-27T19:03:13","modified_gmt":"2025-05-27T18:03:13","slug":"sources-bytecode-debugging","status":"publish","type":"idea","link":"https:\/\/blog.jetbrains.com\/pt-br\/idea\/2025\/05\/sources-bytecode-debugging","title":{"rendered":"Sources, Bytecode, Debugging"},"content":{"rendered":"<p>When debugging Java programs, developers are often under the impression that they&#8217;re interacting directly with the source code. This isn\u2019t surprising \u2013 Java\u2019s tooling does such an excellent job of hiding the complexity that it almost feels as if the source code exists at runtime.<\/p>\n<p>If you\u2019re just starting with Java, you likely remember those diagrams showing how the compiler transforms source code into bytecode, which is then executed by the JVM. You might also wonder: if that\u2019s the case, why do we examine and step through the source code rather than the bytecode? How does the JVM know anything about our sources?<\/p>\n<p>This article is a little different from my <a href=\"https:\/\/blog.jetbrains.com\/author\/igor-kulakov-jetbrains-com\/\">previous posts<\/a> on debugging. Instead of focusing on how to debug a specific problem, such as an unresponsive app or a memory leak, it explores how Java and debuggers work behind the scenes. Stick around \u2013 as always, a couple of handy tricks are included.<\/p>\n<h2>Bytecode<\/h2>\n<p>Let\u2019s start with a quick recap. The diagrams found in Java books and guides are indeed correct \u2013 the JVM executes bytecode.<\/p>\n<p>Consider the following class as an example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"\">package dev.flounder;\n\npublic class Calculator {\n    int sum(int a, int b) {\n        return a + b;\n    }\n}<\/pre>\n<p>When compiled, the <code>sum()<\/code> method will turn into the following bytecode:<\/p>\n<pre><code lang=\"plain\">int sum(int, int);\n    descriptor: (II)I\n    flags: (0x0000)\n    Code:\n      stack=2, locals=3, args_size=3\n         0: iload_1\n         1: iload_2\n         2: iadd\n         3: ireturn\n<\/code><\/pre>\n<p><b>Tip<\/b>: You can inspect the bytecode of your classes using the <code>javap -v<\/code> command included with the JDK. If you are using IntelliJ IDEA, you can also do this from the IDE: after building your project, select a class, and then click <i>View<\/i> | <i>Show Bytecode<\/i>.<\/p>\n<p><b>Note<\/b>: Since class files are binary, citing their raw contents would not be informative. For readability, the examples in this article follow the format of <code>javap -v<\/code> output.<\/p>\n<p>Bytecode consists of a series of compact platform-independent instructions. In the example above:<\/p>\n<ol>\n<li><code>iload_1<\/code> and <code>iload_2<\/code> load the variables onto the operand stack.<\/li>\n<li><code>iadd<\/code> adds the contents of the operand stack, leaving a single result value on it.<\/li>\n<li><code>ireturn<\/code> returns the value from the operand stack.<\/li>\n<\/ol>\n<p>In addition to instructions, bytecode files also include information on the constants, the number of parameters, local variables, and the depth of the operand stack. This is all the JVM needs to execute a program written in a JVM language, such as Java, Kotlin, or Scala.<\/p>\n<h2>Debug information<\/h2>\n<p>Since bytecode looks completely different from your source code, referring to it while debugging would be inefficient. For this reason, the interfaces of Java debuggers \u2013 such as the JDB (the console debugger bundled with the JDK) or the one in IntelliJ IDEA \u2013 display the source code rather than bytecode. This allows you to debug the code that you wrote without having to think about the underlying bytecode being executed.<\/p>\n<p>For example, your interaction with the JDB might look like this:<\/p>\n<pre><code lang=\"plain\">Initializing jdb ...\n\n&gt; stop at dev.flounder.Calculator:5\n\nDeferring breakpoint dev.flounder.Calculator:5.\nIt will be set after the class is loaded.\n\n&gt; run\n\nrun dev\/flounder\/Main\nSet uncaught java.lang.Throwable\nSet deferred uncaught java.lang.Throwable\nVM Started: Set deferred breakpoint dev.flounder.Calculator:5\nBreakpoint hit: \"thread=main\", dev.flounder.Calculator.sum(), line=5 bci=0\n\n&gt; locals\n\nMethod arguments:\na = 1\nb = 2\n<\/code><\/pre>\n<p>IntelliJ IDEA will display the debug-related information in the editor and in the <i>Debug<\/i> tool window:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/05\/idea.png\" alt=\"IntelliJ IDEA shows the executed line and variable values during a debugging session\" width=\"903px\" \/><\/p>\n<p>As you can see, both debuggers use the correct variable names and reference valid lines from our code snippet above.<\/p>\n<p>Since the runtime doesn\u2019t have access to the source files, it must collect this data elsewhere. This is where debug information comes into play. Debug information (also referred to as debug symbols) is compact data that links the bytecode to the application\u2019s sources. It is included in the <code>.class<\/code> files during compilation.<\/p>\n<p>There are three types of debug information:<\/p>\n<ul>\n<li><a href=\"#line-numbers\">Line numbers<\/a><\/li>\n<li><a href=\"#variable-names\">Variable names<\/a><\/li>\n<li><a href=\"#source-file-names\">Source file names<\/li>\n<\/ul>\n<p>In the following chapters, I\u2019ll briefly explain each type of debug information and how the debugger uses it.<\/p>\n<h3>Line numbers<\/h3>\n<p>Line number information is stored in the <code>LineNumberTable<\/code> attribute within the bytecode file, and it looks like this:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"\">\nLineNumberTable:\nline 5: 0\nline 6: 2<\/pre>\n<p>The table above tells the debugger the following:<\/p>\n<ul>\n<li>Line <code>5<\/code> contains the instruction at offset <code>0<\/code><\/li>\n<li>Line <code>6<\/code> contains the instruction at offset <code>2<\/code><\/li>\n<\/ul>\n<p>This type of debug information helps external tools, such as debuggers or profilers, trace the exact line where the program executes in the source code.<\/p>\n<p>Importantly, line number information is also used for source references in exception stack traces. In the following example, I compiled code from <a href=\"https:\/\/flounder.dev\/posts\/efficient-debugging-exceptions\" target=\"_blank\" rel=\"noopener\">my other tutorial<\/a> without line number information:<\/p>\n<pre><code lang=\"plain\">Exception in thread \"main\" java.lang.NumberFormatException: For input string: \"\"\nat java.base\/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)\nat java.base\/java.lang.Integer.parseInt(Integer.java:672)\nat java.base\/java.lang.Integer.parseInt(Integer.java:778)\nat dev.flounder.Airports.parse(Airports.java)\nat java.base\/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)\nat java.base\/java.util.Iterator.forEachRemaining(Iterator.java:133)\nat java.base\/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)\nat java.base\/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)\nat java.base\/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)\nat java.base\/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)\nat java.base\/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)\nat java.base\/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)\nat java.base\/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)\nat dev.flounder.Airports.main(Airports.java)\n<\/code><\/pre>\n<p>The executable compiled without line number information produced a stack trace that lacks line numbers for the calls corresponding to my project code. The calls from the standard library and dependencies still include line numbers because they have been compiled separately and weren\u2019t affected.<\/p>\n<p>Besides stack traces, you may encounter a similar situation where line numbers are involved, for example, in IntelliJ IDEA\u2019s <i>Frames<\/i> tab:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/05\/frames-without-line-numbers.png\" alt=\"IntelliJ IDEA's Frames tab showing -1 instead of line numbers\" width=\"490px\" \/><\/p>\n<p>So, if you see <code>-1<\/code> instead of actual line numbers and want to avoid this, make sure your program is compiled with line number information.<\/p>\n<p><b>Tip<\/b>: You can view bytecode offset right in IntelliJ IDEA\u2019s <i>Frames<\/i> tab. For this, add the following <a href=\"https:\/\/youtrack.jetbrains.com\/articles\/SUPPORT-A-1030\/How-to-edit-IntelliJ-IDE-registry\" target=\"_blank\" rel=\"noopener\">registry key<\/a>: <code>debugger.stack.frame.show.code.index=true<\/code>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/05\/idea-bytecode-offset.png\" alt=\"IntelliJ IDEA shows bytecode offset next to line numbers in the Frames tab\" width=\"616px\" \/><\/p>\n<h3>Variable names<\/h3>\n<p>Like line number information, variable names are stored in class files. The variable table for our example looks as follows:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"\">\nLocalVariableTable:\nStart  Length  Slot  Name   Signature\n    0       4     0  this   Ldev\/flounder\/Calculator;\n    0       4     1     a   I\n    0       4     2     b   I<\/pre>\n<p>It contains the following information:<\/p>\n<ol>\n<li><b>Start<\/b>: The bytecode offset where the scope of this variable begins.<\/li>\n<li><b>Length<\/b>: The number of instructions during which this variable remains in scope.<\/li>\n<li><b>Slot<\/b>: The index at which this variable is stored for reference.<\/li>\n<li><b>Name<\/b>: The variable\u2019s name as it appears in the source code.<\/li>\n<li><b>Signature<\/b>: The variable\u2019s data type, expressed in Java\u2019s type signature notation.<\/li>\n<\/ol>\n<p>If variables are missing from the debug information, some debugger functionality might not work as expected, and you will see <code>slot_1<\/code>, <code>slot_2<\/code>, etc. instead of the actual variable names.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/05\/idea-without-variables.png\" alt=\"IntelliJ IDEA displays slot_1, slot_2, etc. instead of variable names in the Debug tool window\" width=\"665px\" \/><\/p>\n<h3>Source file names<\/h3>\n<p>This type of debug information indicates which source file was used to compile the class. Like line number information, its presence in the class files affects not only external tooling, but also the stack traces that your program generates:<\/p>\n<pre><code lang=\"plain\">Exception in thread \"main\" java.lang.NumberFormatException: For input string: \"\"\n\tat java.base\/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)\n\tat java.base\/java.lang.Integer.parseInt(Integer.java:672)\n\tat java.base\/java.lang.Integer.parseInt(Integer.java:778)\n\tat dev.flounder.Airports.parse(Unknown Source)\n\tat java.base\/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)\n\tat java.base\/java.util.Iterator.forEachRemaining(Iterator.java:133)\n\tat java.base\/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)\n\tat java.base\/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)\n\tat java.base\/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)\n\tat java.base\/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)\n\tat java.base\/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)\n\tat java.base\/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)\n\tat java.base\/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)\n\tat dev.flounder.Airports.main(Unknown Source)\n<\/code><\/pre>\n<p>Without source file names, the corresponding stack trace calls will be marked as <code>Unknown Source<\/code>.<\/p>\n<h2>Compiler flags<\/h2>\n<p>As a developer, you have control over whether to include debug information in your executables and, if so, which types to include. You can manage this by using the <code>-g<\/code> compiler argument, like this:<\/p>\n<p><code>javac -g:lines,vars,source<\/code><\/p>\n<p>Here is the syntax:<\/p>\n<table>\n<tbody>\n<tr>\n<td>Command<\/td>\n<td>Result<\/td>\n<\/tr>\n<tr>\n<td><code>javac<\/code><\/td>\n<td>Compiles the application with line numbers and source file names (default for most compilers)<\/td>\n<\/tr>\n<tr>\n<td><code>javac -g<\/code><\/td>\n<td>Compiles the application with all available debug information:<br \/>\nline numbers, variables, and source file names<\/td>\n<\/tr>\n<tr>\n<td><code>javac -g:lines,source<\/code><\/td>\n<td>Compiles the application with the specified types of debug information \u2013<br \/>\nline numbers and source file names in this example<\/td>\n<\/tr>\n<tr>\n<td><code>javac -g:none<\/code><\/td>\n<td>Compiles the application without the debug information<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><b>Note<\/b>: Defaults might vary between compilers. Some of them completely exclude debug information unless instructed otherwise.<\/p>\n<p>If you are using a build system, such as Maven or Gradle, you can pass the same options through compiler arguments.<\/p>\n<p>Maven example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"\">\n&lt;plugin&gt;\n    &lt;groupId&gt;org.apache.maven.plugins&lt;\/groupId&gt;\n    &lt;artifactId&gt;maven-compiler-plugin&lt;\/artifactId&gt;\n    &lt;version&gt;3.11.0&lt;\/version&gt;\n    &lt;configuration&gt;\n        &lt;compilerArgs&gt;\n            &lt;arg&gt;-g:vars,lines&lt;\/arg&gt;\n        &lt;\/compilerArgs&gt;\n    &lt;\/configuration&gt;\n&lt;\/plugin&gt;<\/pre>\n<p>Gradle example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"\">\ntasks.compileJava {\n    options.compilerArgs.add(&quot;-g:vars,lines&quot;)\n}<\/pre>\n<h2>Why remove debug information?<\/h2>\n<p>As we\u2019ve just seen, debug symbols enable the debugging process, which is convenient during development. For this reason, debug symbols are usually included in development builds. In production builds, they are often excluded; However, this ultimately depends on the type of project you are working on.<\/p>\n<p>Here are a couple of things you may want to consider:<\/p>\n<h3>Security<\/h3>\n<p>Since a debugger can be used to tamper with your program, including debug information makes your application slightly more vulnerable to hacking and reverse engineering, which may be undesirable for some applications.<\/p>\n<p>Although the absence of debug symbols might make it somewhat more difficult to interfere with your program using a debugger, it does not fully protect it. Debugging remains possible even with partial or missing debug information, so this alone will not prevent a determined individual from accessing your program\u2019s internals. Therefore, if you are concerned about the risk of reverse engineering, you should employ additional measures, such as code obfuscation.<\/p>\n<h3>Executable size<\/h3>\n<p>The more information an executable contains, the larger it becomes. Exactly how much larger depends on various factors. The size of a particular class file might easily be dominated by the number of instructions and the size of the constant pool, making it impractical to provide a universal estimate. Still, to demonstrate that the difference can be substantial, I experimented with <a href=\"https:\/\/flounder.dev\/posts\/efficient-debugging-exceptions\" target=\"_blank\" rel=\"noopener\">Airports.java<\/a>, which we used earlier to compare stack traces. The results are <b>4,460<\/b> bytes without debug information compared to <b>5,664<\/b> bytes with it.<\/p>\n<p>In most cases, including debug symbols won\u2019t hurt. However, if executable size is a concern, as is often the case with embedded systems, you might want to exclude debug symbols from your binaries.<\/p>\n<h2>Adding sources for debugging<\/h2>\n<p>Typically, the required sources reside within your project, so the IDE will have no trouble finding them. However, there are less common situations \u2013 for example, when the source code needed for debugging is outside your project, such as when stepping into a library used by your code.<\/p>\n<p>In this case, you need to add source files manually: either by placing them under a <a href=\"https:\/\/www.jetbrains.com\/help\/idea\/content-roots.html\" target=\"_blank\" rel=\"noopener\">sources root<\/a> or by specifying them as a dependency. During debugging, IntelliJ IDEA will automatically detect and match these files with the classes executed by the JVM.<\/p>\n<h3>When the project is missing<\/h3>\n<p>In most cases, you would build, launch, and debug an application in the same IDE, using the original project. But what if you have only a few source files, and the project itself is missing?<\/p>\n<p>Here\u2019s a bare-bones debugging setup that will do the trick:<\/p>\n<ol>\n<li>Create an empty Java project.<\/li>\n<li>Add the source files under a sources root or specify them as a dependency.<\/li>\n<li>Launch the target application with the debug agent. In Java, this is typically done by adding a VM option, such as: <code>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005<\/code>.<\/li>\n<li>Create a <a href=\"https:\/\/www.jetbrains.com\/help\/idea\/attach-to-process.html#create-rc\" target=\"_blank\" rel=\"noopener\">Remote JVM Debug<\/a> run configuration with the correct connection details. Use this run configuration to attach the debugger to the target application.<\/li>\n<\/ol>\n<p>With this setup, you can debug a program without accessing the original project. IntelliJ IDEA will match the available sources with the runtime classes and let you use them in a debugging session. This way, even a single project or library class gives you an entry point for debugging.<\/p>\n<p>For a hands-on example, check out <a href=\"https:\/\/flounder.dev\/posts\/debugger-god-mode\" target=\"_blank\" rel=\"noopener\">Debugger.godMode() \u2013 Hacking JVM Applications With the Debugger<\/a>, where we use this technique to change a program\u2019s behavior without accessing its source code.<\/p>\n<h2>Source mismatch<\/h2>\n<p>One confusing situation you might encounter during debugging is when your application appears suspended at a blank line or when the line numbers in the <i>Frames<\/i> tab don\u2019t match those in the editor:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/05\/bytecode-mismatch.png\" alt=\"IntelliJ IDEA highlights a blank line as if it were executed\" width=\"606px\" \/><\/p>\n<p>This occurs when debugging decompiled code (which we\u2019ll discuss in another article) or when the source code doesn\u2019t fully match the bytecode that the JVM is executing.<\/p>\n<p>Since the only link between bytecode and a particular source file is the name of the file and its classes, the debugger has to rely on this information, assisted by some heuristics. This works well for most situations. However, the version of the file on disk may differ from the one used to compile the application. In the case of a partial match, the debugger will identify the discrepancies and attempt to reconcile them rather than failing fast. Depending on the extent of the differences, this might be useful, for example, if the only source that you have isn\u2019t the closest match.<\/p>\n<p>Fortunately, if you have the exact version of the sources elsewhere, you can fix this issue by adding them to the project and re-running the debug session.<\/p>\n<h2>Conclusion<\/h2>\n<p>In this article, we\u2019ve explored the connection between source files, bytecode, and the debugger. While not strictly required for day-to-day coding, having a clearer picture of what happens under the hood can give you a stronger grasp of the ecosystem and may occasionally help you out of non-standard situations and configuration problems. I hope you found the theory and tips useful!<\/p>\n<p>There are still many more topics to come in this series, so stay tuned for the next one. If there\u2019s anything specific you\u2019d like to see covered, or if you have ideas and feedback, we\u2019d love to hear from you!<\/p>\n","protected":false},"author":1206,"featured_media":570294,"comment_status":"closed","ping_status":"closed","template":"","categories":[4759,3903,5088,601],"tags":[632,264,155],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/idea\/569893"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/idea"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/types\/idea"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/users\/1206"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/comments?post=569893"}],"version-history":[{"count":10,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/idea\/569893\/revisions"}],"predecessor-version":[{"id":570571,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/idea\/569893\/revisions\/570571"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/media\/570294"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/media?parent=569893"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/categories?post=569893"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/tags?post=569893"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/pt-br\/wp-json\/wp\/v2\/cross-post-tag?post=569893"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}