Jira Data Center heap size and JVM tuning for a Cloud migration: setenv.sh and JCMA
Mihai Perdum
Author
32 min readSeptember 16, 2026
Key takeaways
END STATE: a Jira Data Center 11.3 cluster on Java 21 whose heap is sized from your own GC log and Atlassian's migration table, whose setenv.sh carries only shipped or documented values, and whose nodes, pool, timeouts and background jobs are set for the Cloud Migration Assistant to run at full speed.
The heap calculator has four inputs, all Atlassian's: the migration table (8 GB heap up to 60,000 issues, 16 GB at 2 million and above, keyed to CPU cores), the size-profile ratio (heap is half of node RAM in three rows and one below half in the fourth: 4/8, 8/16, 16/32, 31/64), the GC-log shape (a third to 40% of the heap in use after collection, young generation under half), and the forbidden band: never 32 to 47 GB, go to 48 or more.
Migration speed is mostly CPU. Atlassian's assistant runs 4, 8, 16 or 32 parallel threads on 2 to 4, 8, 16 or 32 cores, and says the heap 'doesn't affect the migration time as much as in the past, but it's still an important part'. Raise the heap for the migration and lower it afterwards, which Atlassian explicitly permits.
Before the run: pool from 20 to up to 40 connections, open files near 32,768, index snapshot and app jobs off, no backups or reindex or antivirus, one migration plan per two nodes, mail disabled on any clone, attachments and users migrated ahead, the three jcma timeouts raised if a step times out.
What 'massive' means in Atlassian's tiers: 10,000 to 100,000 users is Large, over 100,000 is XLarge. Atlassian's extra-large test ran 7.2 million issues and 80,002 users on 25 and 50 GB heaps; its own production runs about 5,000 concurrent sessions an hour on 24 GB with G1 across three nodes. Three nodes perform well; five or more have struggled.
Do not set a pause target, thread counts, a Metaspace cap or any collector other than G1 or Parallel. Atlassian names none of them for Jira and states the two collectors it supports. Pre-touching the heap is JDK-documented and measurably cuts early pauses; Atlassian is silent on it, so it stays optional here.
Jira Data Center heap size and JVM tuning have one job in the months before a Cloud migration: keep a very large instance stable for its users while the Jira Cloud Migration Assistant runs inside the same JVM, exporting millions of issues as fast as the hardware allows. Those two goals pull on the same heap, the same database pool and the same CPUs, and most of the advice in circulation was written for neither. So this tutorial starts from what Jira 11.3.11 actually ships, adds the layer Atlassian documents for the migration assistant, and gives you a way to calculate the numbers for your own instance instead of borrowing someone else's.
The rule for every value here is the one I would hold a client cluster to: it is shipped by Atlassian, documented by Atlassian, or defined in the JDK 21 reference, or it does not go in. Where Atlassian gives a rule and no number, you get the rule and a method. Where two Atlassian pages disagree, you get both. Where Atlassian is silent and the JDK is not, the flag is labelled as the JDK's and left optional. Where I measured something myself, it is labelled as measured on my machine and not on a Jira node. Every page cited was fetched on 14 September 2026.
Confluence Data Center has a different setenv.sh, a second JVM for Synchrony and its own migration assistant, and gets its own tutorial.
Note
Prerequisites
Jira Software Data Center 11.3 on Linux, with access to <install-directory>/bin, <install-directory>/conf and the Jira home directory. Windows uses setenv.bat with the same variables.
Java 21, and nothing else. Atlassian: "Jira 11.3 has been compiled in JDK 21. This means that from now on you won't be able to run Jira in lower Java versions (8, 11, or 17)." Bundled JRE listed for 11.3.1 through 11.3.10: Temurin 21.0.6_7.
The Jira Cloud Migration Assistant at 1.10.17 or later. Atlassian: "In one of the recent releases, version 1.10.17, we significantly improved migration speed, so it's important that you upgrade to this version or one of the later ones." The current Marketplace release is 1.24.1 (9 September 2026), compatible with Jira 7.6.0 through 11.3.11.
Your instance's numbers. Issues including archived, projects, users, custom fields, attachment size in GB, and the CPU cores per node. Atlassian's size profiles and its migration table are keyed to these, not to a feeling about how big the site is.
A staging clone with production data, its own Server ID saved before the clone and written back after it (Atlassian's checklist, step 22), and ten minutes of GC logs from production under peak load. Both are inputs to step 3.
A migration ticket open with Atlassian if you have over 1,000 users going to Cloud: "we recommend getting in touch with our migration support team at least one to two months in advance."
1
Read the setenv.sh you were shipped, and the second script that finishes its work.
2
Confirm Java 21 the way Jira itself does, and purge the flags that stop it starting.
3
Calculate the heap from Atlassian's migration table, the size-profile ratio, your GC log, and the 32 to 47 GB rule.
4
Leave the collector alone, know where the GC log already is, and decide about pre-touch with the measurements in front of you.
5
Keep the code cache and Metaspace as shipped, and arm the JQL safeguards.
6
Prepare the node for the migration assistant
CPUs, pool, open files, timeouts, jobs, nodes, mail, order of operations.
7
Enforce the cluster rules and read what Atlassian has tested at extra-large scale before you decide on nodes.
8
Verify from the startup log, then alert at Atlassian's thresholds and measure the migration with Atlassian's query.
Step 1 — Read the Jira Data Center setenv.sh you were shipped
Download the 11.3.11 archive, released 2 September 2026, and extract only the scripts and the Tomcat configuration. On macOS or any BSD tar the GNU --wildcards flag does not exist; the pattern works without it:
Jira's file is built from named variables that a later line assembles into the Java command. These carry values, as shipped, with their line numbers:
bash
1# line 4 — the open-files floor Jira applies to itself (step 6)2MIN_NOFILES_LIMIT=1638434# lines 12-14 — "Occasionally Atlassian Support may recommend that you set some specific JVM arguments."5JVM_SUPPORT_RECOMMENDED_ARGS=""67# lines 17-22 — the collector flags; the comment still talks about Java 8 and 118JVM_GC_ARGS="-XX:+ExplicitGCInvokesConcurrent"910# lines 25-28 — "In larger JIRA instances, the maximum amount will need to be increased."11JVM_MINIMUM_MEMORY="384m"12JVM_MAXIMUM_MEMORY="2048m"1314# lines 31-33 — "A high value of reserved size allows Jira to work with more installed apps."15JVM_CODE_CACHE_ARGS='-XX:InitialCodeCacheSize=32m -XX:ReservedCodeCacheSize=512m'1617# line 38 — "The following are the required arguments for Jira." Do not edit.18JVM_REQUIRED_ARGS='-Dlog4j2.contextSelector=... -Djava.awt.headless=true -Datlassian.standalone=JIRA ... -Dfile.encoding=UTF-8 ...'1920# line 42, commented — for a clone or staging node that must not send mail (step 6)21#DISABLE_NOTIFICATIONS=" -Datlassian.mail.senddisabled=true -Datlassian.mail.fetchdisabled=true -Datlassian.mail.popdisabled=true"2223# line 47 — "In general don't make changes below here"24# line 5525JVM_EXTRA_ARGS="-XX:-OmitStackTraceInFastThrow -Djava.locale.providers=COMPAT"2627# lines 94 and 98 — where it all comes together28JAVA_OPTS="${JAVA_OPTS}${TOMCAT_JVM_OPTS}${JVM_CODE_CACHE_ARGS}${JVM_REQUIRED_ARGS}${DISABLE_NOTIFICATIONS}${JVM_SUPPORT_RECOMMENDED_ARGS}${JVM_EXTRA_ARGS}${JIRA_HOME_MINUSD}${START_JIRA_JAVA_OPTS}"29CATALINA_OPTS="-Xms${JVM_MINIMUM_MEMORY} -Xmx${JVM_MAXIMUM_MEMORY}${CATALINA_OPTS}"
The part most runbooks miss: JVM_GC_ARGS is defined on line 22 and never used in this file. It is consumed by bin/set-gc-params.sh, which catalina.sh sources on every start, right after the Java version check. That script is where the GC log comes from:
bash
1if[$java_version-ge9]2then3GC_JVM_PARAMETERS="-Xlog:gc*:file=$LOGBASEABS/logs/atlassian-jira-gc-%t.log:tags,time,uptime,level:filecount=5,filesize=20M ${GC_JVM_PARAMETERS}"4GC_JVM_PARAMETERS="${GC_JVM_PARAMETERS}${JVM_GC_ARGS}"5else6... the Java 8-Xloggc form ...
7fi8CATALINA_OPTS="${GC_JVM_PARAMETERS}${CATALINA_OPTS}"
So on Java 21 the effective collector-related command line, before you change anything, is -Xlog:gc*:file=<install>/logs/atlassian-jira-gc-%t.log:tags,time,uptime,level:filecount=5,filesize=20M -XX:+ExplicitGCInvokesConcurrent -Xms384m -Xmx2048m. No -XX:+UseG1GC is passed anywhere; G1 is the JDK default. Atlassian's GC page confirms the intent: "Starting from Jira 7.4, GC logs are generated automatically, and you can find them in /logs."
Two things in the file are stale and worth knowing as such. The JVM_GC_ARGS comment reads "For Java 8 we recommend default settings / For Java 11 and relatively small heaps we recommend: -XX:+UseParallelGC / For Java 11 and larger heaps we recommend: -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent", on a release that refuses to run on either of those Javas. And Atlassian's "Increasing Jira application memory" page, last modified in 2023, says "it is best to start with 768 MB", while the shipped default is 2048m. The file is what runs; the page is history.
How you know it worked: you can place every uncommented line of your production setenv.sh against a line in the shipped one, and everything you added sits in JVM_SUPPORT_RECOMMENDED_ARGS or JVM_GC_ARGS, which is where Atlassian's startup page says additions go: "Find the section JVM_SUPPORT_RECOMMENDED_ARGS= ... Add all parameters in a space-separated list, inside the quotations." Atlassian's GC-log article adds the rule about duplicates: "We advise filling the JVM_GC_ARGS or JVM_SUPPORT_RECOMMENDED_ARGS if the first's not present. Be mindful that some parameters may already be present elsewhere in the setenv.sh/setenv.bat. We advise removing them from elsewhere to avoid duplicates."
Step 2 — Confirm Java 21 the way Jira does
bin/check-java.sh in 11.3.11 tests if [ $java_version -ne 21 ] and, when it fails, prints a banner whose middle line reads "Wrong JVM version! Jira requires 21 to run.", echoes the java -version it found, and stops. There is an ignore_jvm_version=true escape hatch a few lines further down that script, and the banner tells you about it; the reason not to use it is the supported platforms page, which lists Java 21 and nothing else, with "As a best practice, we recommend using the latest version of Java 21."
bash
1/opt/atlassian/jira/jre/bin/java -version# the bundled one, if you used the installer2java-version# the one JAVA_HOME points at, if you used the archive
text
1openjdk version "21.0.12.1" 2026-08-18
This step matters for the flags you inherited. The Java 8 GC-logging flags that lived in older setenv.sh files are not ignored on Java 21. Atlassian's page for the Java 11 move says "these flags below must be removed, otherwise JIRA will fail to start", and lists them: -XX:+PrintAdaptiveSizePolicy -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+PrintTenuringDistribution -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -Xloggc:<PATH>. On JDK 21:
1Unrecognized VM option 'PrintGCDateStamps'
2Error: Could not create the Java Virtual Machine.
Jira's file does not ship -XX:+IgnoreUnrecognizedVMOptions, so there is no safety net; a stale flag is a node that does not start, discovered in the migration window if you did not grep for it before.
How you know it worked: Jira starts, and the first lines of <jira-home>/log/atlassian-jira.log for that start show the full JVM Input Arguments : ... line that step 8 reads.
Step 3 — Calculate the Jira Data Center heap size (never 32 to 47 GB)
There is no single Atlassian number for a large Jira heap, and there is no ceiling. What Atlassian publishes is four things that together give you a calculation: a table keyed to your data, a ratio in its own sizing profiles, a shape rule for the GC log, and one forbidden band. Do them in order and write down each result; the heap is at least input one, shaped by input two, set by input three, and never inside input four.
Input one: the migration table. Atlassian's "Heap size affecting migration time" page is the only current Atlassian document that maps issue volume to a minimum heap; the older Jira Server sizing guide gives over 1, over 2 and over 4 GB up to 200,000 issues and "Consult our experts" beyond, and is Java 8 era. The current page exists because of the migration assistant: "While exporting issues during the migration, we load their IDs into application memory. The more issues you have, the more memory we need to keep those IDs until entire export is finished. The migration assistant also exports many projects and issues in parallel to speed up the migration, so it's good to have a substantially larger heap. The memory is released once export is done." The same table appears on the speed-up page and on both hosts of the heap-size page:
CPU cores
issue volume
minimum JVM heap
minimum system memory
parallel threads
2
under 10,000
8 GB
8 GB
4
4
15,000
8 GB
over 8 GB
4
8
60,000
8 GB
over 16 GB
8
16
2,000,000
16 GB
over 32 GB
16
32
10,000,000
16 GB
over 32 GB
32
6 rows × 5 columnsHeader row enabled
The floor under all of it, from the checklist: "ensure that your application is running with at least 4GBs of Heap Allocation." Read the table for its scope. It is the export-phase heap the assistant needs, and Atlassian is explicit that it is temporary: "You can increase the heap just for the migration and decrease it later if your current heap size is enough for your instance." It is also keyed to issues and cores, not users; there is no heap-per-user figure anywhere in Atlassian's migration documentation, so an instance serving 20,000 or 30,000 people is sized here by how many issues those people created.
Input two: the size-profile ratio. The same checklist gives Atlassian's benchmark profiles for the whole node: "Extra large 32 CPU, 64 GB RAM, 31 GB Heap / Large 16 CPU, 32 GB RAM, 16 GB Heap / Medium 8 CPU, 16 GB RAM, 8 GB Heap / Small 4 CPU, 8 GB RAM, 4 GB Heap". In three rows of that table the heap is half the RAM, and the extra-large row stops one below half, at 31 GB, one below the boundary in input four. Atlassian does not state the half-of-RAM shape as a rule; it is what its own table does, and the remaining half is the operating system's, plus what Atlassian's article on nodes using more memory than Xmx lists by name: Metaspace, the code cache, the native C++ heap, thread stacks, direct byte buffers and garbage collection overhead. Which profile is yours comes from the size-profiles page: Large is 600,000 to 2,000,000 issues, 800 to 2,500 projects, 10,000 to 100,000 users, 800 to 1,800 custom fields; "Any metric that registers above the Large range is XLarge", and "if any single metric registers at Large, then we believe that it's safe to declare your instance's overall size profile as Large."
Input three: your GC log. Atlassian's GC tuning page gives the target: "After garbage collection, the memory used should be 1/3 the size of the total heap, and that's your goal." Its knowledge base article on defining Xmx from GC logs gives a second, close figure: "The memory footprint area (max and min of the saw-toothed like lines) should be around 40% of your total Java heap size", and adds that "The Young generation size should not exceed more than half the total Java heap size. Typically, approximately 40% of the heap size is adequate." Two Atlassian pages, a third and 40%; use them as a band. The method is the one every step of this tutorial comes back to: take ten minutes of GC logs at peak, read the heap occupancy after each young collection and after any concurrent cycle, and that live number divided by 0.33 to 0.40 is the heap your steady state wants. Step 4 shows what those lines look like. The JDK's own guidance runs the other way and both stand: "Unless you have problems with pauses, try granting as much memory as possible to the virtual machine", against Atlassian's "Do not increase the heap further than required, as this will result in longer garbage collection."
Input four: the forbidden band. This is the one numeric heap rule Atlassian publishes for Jira, and it is recent: "Java disables Compressed Oops for application heaps larger than 32 GB, which changes the memory allocation size from 4 to 8 bytes, reducing the number of objects it can store in the heap. This means that increasing the maximum heap to a value near to 32 GB and up to 47 GB will actually decrease the amount of memory available, leading to possible OutOfMemoryErrors." The instruction: "If increasing Jira memory heap size near to, or higher than, 32 GB, set it to >=48 GB and continue adding memory from that point. As a best practice it's also recommended using the same value for Xms and Xmx". The JDK reference gives the mechanism: compressed pointers "will automatically limit the maximum ergonomically determined Java heap size to the maximum amount of memory that can be covered by compressed pointers. By default this range is 32 GB." On JDK 21 the switch happens exactly at the number:
Then the two common rules. Increase the default, and make the minimum equal the maximum, from the cluster page: "The maximum heap (-Xmx) for the Jira application is set in the setenv.sh or setenv.bat file. The default should be increased for Data Center. We recommend keeping the minimum (Xms) and maximum (Xmx) heap the same value." Grow on evidence: "Start with the smallest heap possible, and increase it in 512 MB or 1 GB allotments when you experience OutOfMemory errors or run into any memory-related problems." And never above the box: "Setting the -Xmx above the available amount on the server runs the risk of OutOfMemoryErrors due to lack of physical memory. If that occurs the system will use swap space, which greatly decreases performance."
The calculation, worked for the instance this tutorial is for. Say 25,000 users, 3,000,000 issues including archived, 1,200 custom fields, 16-core nodes with 64 GB of RAM. Input one: 3,000,000 issues on 16 cores is the 16 GB row, and the 16 GB heap is the minimum for the export. Input two: users and custom fields are Large, issues are XLarge, so the instance is XLarge and Atlassian's benchmark row is 32 cores, 64 GB, 31 GB heap; that row is a shape, not a candidate, and it tells you two things about your 64 GB node: Atlassian's own extra-large row keeps the heap at about half the RAM, and it stops at 31, one below the band in input four. Why 31 is not stated; what is arithmetic is that 48 GB on a 64 GB node leaves 16 GB for everything the other half was covering. Input three: if the GC log at peak shows 9 GB live after collection, 9 divided by 0.33 to 0.40 is 22 to 27 GB. The heap is 24 GB for the steady state, which is also what Atlassian runs on its own largest Jira, and the migration needs no more than that. If the live set were 12 GB, the arithmetic would say 30 to 36 GB, the band would forbid the top of that range, and the honest answer is 31 GB or a node with more RAM and 48 GB. Write the result as Atlassian's own procedure verifies it:
And what the numbers Atlassian has run look like next to that: its Large-profile AWS benchmark used 8 GB, "The JVM heap was configured to 8G for all tests"; its XLarge benchmark used "a maximum of 50GB for all tests. For c5.4xlarge, we set the maximum heap at 25GB"; its own production is "JVM 24GB maximum heap size, using the G1 Garbage Collector" on three-node clusters of 72 GB and 64 GB machines; its Marketplace performance toolkit runs a million-issue, 21,000-user rig on jira_max_heap = "12288m" in a 16 GiB pod. None of those is a recommendation, and Atlassian never says why 50 and 25 were chosen for the extra-large test.
How you know it worked: Atlassian's own check, from the memory page: "check the /logs/atlassian-jira.log or catalina.out file. A section in the startup appears like this: JVM Input Arguments : ... -Xms256m -Xmx384m ...", and "This display is also available by viewing your system information." The checklist's version is a grep on the support zip: grep -i "xmx" <Support Zip>/application-properties/application.xml. And the JVM will confirm the shape before Jira starts:
{command line} is a value you set; {ergonomic} is the JVM's choice for this machine; {default} is a value you did not set, which for ParallelGCThreads is still derived from this machine's CPU count, so the 20 above is this Mac's number. G1 is on without anyone asking, the pause target is the JDK's 200 milliseconds, and Metaspace is unlimited.
Step 4 — JVM tuning: leave the collector alone, and read what the GC log already tells you
Atlassian's collector guidance for Jira is three sentences and one exclusion. "Don't use the Concurrent Mark Sweep (CMS) Collector unless otherwise adviced by Atlassian Support. It requires extensive manual tuning and testing, and is likely to result in degraded performance. Instead, we recommend that you use Garbage First Garbage Collector (G1GC), especially if your heap is larger than 6 GB." The knowledge base article on Xmx puts the threshold at "heaps larger than 4-6 GB" and the GC-overhead page at "large heaps (>4GB)"; three Atlassian numbers, one message. The exclusion is on the Xmx article: "Jira and Confluence only support parallel GC or G1GC." That sentence is why this tutorial does not discuss ZGC, which JDK 21 documents as available; Atlassian does not mention it anywhere, and the JDK 21 documentation set is itself inconsistent about ZGC's flags between its collectors page and its ZGC chapter.
The one flag Jira does pass, -XX:+ExplicitGCInvokesConcurrent, has no Atlassian explanation beyond the setenv.sh comment; the JDK 21 reference defines it as "Enables invoking of concurrent GC by using the System.gc() request. This option is disabled by default and can be enabled only with the -XX:+UseG1GC option." What it does is observable. A small allocation program on JDK 21 with the flag, logging at the gc level; the collection numbered GC(8) is triggered by an explicit System.gc() call:
1[2026-09-14T10:52:58.431+0300][0.209s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 26M->5M(512M) 1.903ms
2[2026-09-14T10:52:58.449+0300][0.227s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 58M->16M(512M) 1.118ms
3[2026-09-14T10:52:58.480+0300][0.259s][info][gc] GC(8) Pause Young (Concurrent Start) (System.gc()) 157M->144M(512M) 2.461ms
Without the flag, an explicit System.gc() is a full, stop-the-world collection. With it, the JVM starts a concurrent cycle instead. Apps and the JVM's own diagnostics can call System.gc(); that flag is why it does not stall a node when they do. Keep it.
Then the rule that saves more clusters than any flag adds: "Most of the time simply allowing JVM to tune itself will give you better performance. If you have added JVM parameters in the past and are experiencing difficulties with GC now, we'd recommend you remove all GC related parameters, unless you added them to solve a specific problem, and they did in fact solve that problem." The JDK's G1 tuning guide says the same: "start by removing all options that affect garbage collection, and only set the pause-time goal and overall heap size by using -Xmx and optionally -Xms." That is why this tutorial sets no -XX:MaxGCPauseMillis, -XX:ParallelGCThreads, -XX:ConcGCThreads, -XX:G1HeapRegionSize or string deduplication. The JDK documents each; no Atlassian page gives a Jira value for any of them, and the JDK describes the pause target as "a soft goal ... By default, for G1 the maximum pause time target is 200 milliseconds" with a cost: "If you prefer high throughput, then relax the pause-time goal by using -XX:MaxGCPauseMillis or provide a larger heap."
Reading the log for step 3. The log is already on. set-gc-params.sh writes -Xlog:gc*:file=<install>/logs/atlassian-jira-gc-%t.log:tags,time,uptime,level:filecount=5,filesize=20M on every Java 9 or later start, which is character for character the flag Atlassian's GC-log article recommends, including the decorators it explains: "This is due to JRASERVER-71420 - Java 11 GC logging configuration is not parseable in GCViewer. To work this around add tags and level to the configuration." The same flag on JDK 21 produces exactly what a Jira node's log opens with:
The gc,init block a few lines further prints Heap Region Size, Compressed Oops and Heap Max Capacity, which is how you confirm step 3's fourth input on the running node. The pause lines have the shape 157M->144M(512M) 2.461ms: before, after, capacity, duration. The "after" numbers across ten minutes at peak are the live set for input three. Atlassian's diagnostics article names the two things to read: "Two key metrics are Throughput and Number of full gc pauses", and GCViewer is the tool: "For processing GC logs, Atlassian Support uses and recommends GCViewer." A Pause Full (G1 Compaction Pause) line in steady state means the heap is too small or something is leaking; the JDK adds that "The probability to run into a Full GC can be compounded by the allocation of many humongous objects", and the Humongous regions: X->Y lines in the same log are where you see them. The full-GC ceiling Atlassian states: "A full GC should not take longer than 3 to 5 seconds."
What G1 does as the heap grows, measured. Two JDK rules decide the shape of a large heap and neither has an Atlassian number: the region size "is determined ergonomically based on the heap size with a goal of approximately 2048 regions", between 1 and 32 MB, and "Humongous objects are objects larger or equal the size of half a region." I ran a synthetic allocation workload on JDK 21 at six heap sizes on a 96 GB Mac Studio, with the live set at 55% of the heap and about 200 GB of short-lived allocation per run. This is not Jira; it is what the collector does at these sizes with these flags.
-Xmx
region
compressed oops
steady young pause p50 / p95
fill-phase young pause p50, no pre-touch
same, with pre-touch
pre-touch cost at startup, by the JVM's own gc,init timestamp
8g
4M
on
0.8 / 0.9 ms
34.9 ms
1.2 ms
0.2 s
16g
8M
on
1.0 / 1.2 ms
70.1 ms
11.5 ms
0.4 s
24g
16M
on
1.0 / 1.2 ms
105.7 ms
13.1 ms
0.5 s
31g
16M
on
1.0 / 1.7 ms
137.4 ms
18.5 ms
0.7 s
32g
16M
off
1.1 / 2.3 ms
138.1 ms
13.0 ms
0.7 s
48g
32M
off
2.5 ms / tail
208.9 ms
150.0 ms
3.5 s
7 rows × 7 columnsHeader row enabled
Three things in that table are worth carrying to a Jira node. Region size steps 4, 8, 16, 32 MB as the heap grows, so a 48 GB heap treats any 16 MB object as humongous; a large attachment stream or a big search result buffer qualifies. Compressed pointers switch off at 32 GB, as step 3 said. And the difference between the two fill-phase columns is -XX:+AlwaysPreTouch, which the JDK defines as "Requests the VM to touch every page on the Java heap after requesting it from the operating system and before handing memory out to the application" and recommends alongside Xms equal to Xmx: "Avoid the delays by setting minimum and maximum heap sizes to the same value using the options -Xms and -Xmx, and pre-touching all memory using -XX:+AlwaysPreTouch to move this work to the VM startup phase ... in order to make pause-times more consistent." The delays it avoids are exactly the tens-to-hundreds of milliseconds in the no-pre-touch column: first-touch page faults inside collection pauses while the heap fills after a restart, which on a Jira node is the first hours after a migration-window restart, with users on it. The cost is startup time, about two-tenths of a second per 10 GB up to 32 GB here, and 3.5 seconds at 48 GB on a machine under memory pressure. The 48 GB rows carry a caveat: that machine shared its memory with other processes during the run and the tail pauses there are reported, not trusted. Atlassian says nothing about pre-touch for Jira. It is a JDK-documented option with a JDK-documented purpose and a measured effect; whether it goes into JVM_SUPPORT_RECOMMENDED_ARGS is your call, and the honest label is that it is not on any Atlassian page.
What you do add is the diagnostic pair Atlassian's GC-overhead page names: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/your/heapdump/directory, with "Ensure the specified directory has sufficient disk space to store heap dumps" and "Remember to implement these changes consistently across all nodes in your Jira Data Center cluster."
The path is an example. Create the directory first and make it writable by the Jira user: on JDK 21, if it does not exist the JVM writes a single file by that name on the first OutOfMemoryError and refuses to overwrite it on the next, so a second incident leaves no dump. Atlassian's advice for reading one: "It's important to lower the size of the heap as much as possible before creating a heap dump, as a very large heap can make it harder to analyse", and in Eclipse MAT "Running the Leak Suspects report is a good place to start."
How you know it worked:<install>/logs holds atlassian-jira-gc-<timestamp>.log files that open with Using G1, the gc,init block shows the region size and compressed-oops state you expect from step 3, and ten minutes of them at peak give you the live set. The threshold to hold it against is Atlassian's, from the GC-overhead page: "You should aim to keep the GC Overhead below 5% for optimal performance", with "Requires attention" between 5 and 10% and "Needs attention" above 10%.
Step 5 — Keep the code cache and Metaspace as shipped, and arm the JQL safeguards
The code cache is sized for apps: -XX:InitialCodeCacheSize=32m -XX:ReservedCodeCacheSize=512m, and the startup page says the shipped value is the answer for most sites: "The default configuration should be optimal for most Jira instances and solve any problems with the code cache getting full." The one documented reason to raise it announces itself in catalina.out: Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled. The fix is -XX:InitialCodeCacheSize=32m -XX:ReservedCodeCacheSize=1024m, "if you've already increased to 1024m, increase to 2048m", and then "Clear catalina.out log file under <JIRA_INSTALL>/logs directory because the health check may be responding to outdated messages in catalina.out." Until you see that line, 512m is the value.
Metaspace stays unlimited. The shipped file sets nothing, the JDK default is "the size isn't limited", and Atlassian's own article says "It's generally unnecessary to specify the maximum size for Metaspace unless you encounter a large memory leak problem." A cap on a node with many apps is an OutOfMemoryError waiting for the next app install.
Four properties on the startup page protect the heap from the other direction, and a large instance should know them before the migration window, because a dashboard gadget with a pathological JQL is a real way to take a node down while the assistant is exporting. They are the Lucene search safeguards, each with a default: jira.search.platform.max.result.window at 10000, jira.lucene.search.memory.circuit.breaker.percentage at 95, which "Rejects new JQL queries when Jira's memory consumption exceeds the defined threshold", jira.search.lucene.query.timeout.ms at 0 meaning disabled, and jira.clause.count.limit at 65000, the "Maximum number of clauses allowed in a plain query to prevent out-of-memory errors". Atlassian's warning goes with them: "Don't set these limits lightly. Only use them if the alternative is a node crash or if failing such JQL searches is acceptable. These properties apply only to the Lucene search engine."
How you know it worked:PrintFlagsFinal shows ReservedCodeCacheSize = 536870912 {command line}, which is 512 MB, and catalina.out has no "CodeCache is full" warning across a full week of production.
Step 6 — Prepare the node for the Jira Cloud Migration Assistant: the pre-migration checklist
Everything above keeps the instance healthy. This step makes the migration fast, and every item is from Atlassian's checklist, its speed-up page, or a migration knowledge base article.
CPUs decide most of the speed; the heap holds the IDs and, in Atlassian's words, "doesn't affect the migration time as much as in the past, but it's still an important part." From the speed-up page: "The number of CPUs control how many projects we can migrate in parallel, that's why we recommend to have at least 16 CPUs. If you add more, we'll allocate them dynamically to migrate projects or issues to achieve the best performance." The table in step 3 has the thread column: 2 projects times 2 issues on 2 or 4 cores, 4 times 2 on 8, 8 times 2 on 16, 8 times 4 on 32, and "we ensure that the thread count doesn't exceed the available CPU cores (except the first scenario)." Also: "This recommendation applies to large instances, with over 500 users ... In general, we recommend a minimum of 16 CPUs for each node. Adding additional nodes (for Data Center) and additional CPUs will increase the speed further." And a warning about the rehearsal: "with fewer CPUs, your test migration will be slower than the actual one later on." There is no property to set thread counts or batch sizes; none exists in Atlassian's documentation. Cores are the lever.
Raise the heap for the run, lower it after. Step 3's table gives the minimum; Atlassian's procedure is "Shut down your Jira instance ... Edit the setenv.sh ... Add the recommended values in the JVM_MINIMUM_MEMORY and JVM_MAXIMUM_MEMORY properties ... Start your Jira instance", and the sentence that permits the round trip: "You can increase the heap just for the migration and decrease it later if your current heap size is enough for your instance." If step 3 already gave you 24 GB, the migration needs nothing more.
Database pool: 20 to 40. The checklist: "when adding Jira Cloud Migration Assistant on top, it may at times exhaust the database connection pool if it's not properly sized. If that happens, Jira will suffer a major performance impact, which will impact the entire application, not only the Migration Assistant ... By default, Jira has 20 as the maximum DB connection pool size. A common fix will be to increase that number in increments of 10, in order to fine-tune the connection pool to accommodate both Jira and the Migration Assistant. Increasing your maximum DB connection pool size up to 40 should be sufficient to support your migration needs." The key is pool-max-size in <jira-home>/dbconfig.xml; the tuning page's rules on its neighbours still hold, pool-prepared-statements and max-open-prepared-statements at their defaults because "Do not change the default value as it will cause exceptions", and validation-query-timeout "only for MySQL". Then the cluster sum, from the scaling page: "if you have three Jira Data Center nodes, and each node supports a maximum of 80 connections, then your shared database must allow at least 240 connections." Atlassian's own production runs 62 active connections on average.
Open files: as close to 32,768 as you can. The shipped setenv.sh raises its own soft limit to at least 16,384, and the checklist wants more for a migration: "Open Files limit ... Ideally, it should be as close as possible to 32768", verified in the support zip's <max-file-descriptor>. A systemd unit caps this below what setenv.sh asks for unless LimitNOFILE is raised in the unit; the too-many-open-files and native-thread articles give nofile and nproc at 16384 and 32768 in limits.conf, and note that if DefaultTasksMax in /etc/systemd/system.conf "is not 65535, then uncomment the line". Verify from the running process with cat /proc/<jira-pid>/limits. The index must be on local disk: "If the $JIRA_HOME/caches/indexes folder is mounted over NFS move it to a local mount ... NFS is not supported".
The three timeouts, if a step times out. The only migration-assistant JVM properties Atlassian documents are timeouts, and they are documented on a ticket, MIG-1864, not a page. The symptom is the literal string A step of migration workflow timed out, "because of a timeout executing a particular JCMA step such as generating the Post Migration Report." The workaround: "JCMA 1.9.8 and later releases offer the ability to customize timeout values using JVM arguments ... jcma.request.timeout - timeout for obtaining a connection from the connection manager (Default=20) / jcma.socket.timeout - maximum period between two consecutive data packets (Default=20 for JCMA < 1.10.2, and Default=40 for JCMA>=1.10.2) / jcma.connect.timeout - timeout until the connection is established (Default=20)", set as export JVM_SUPPORT_RECOMMENDED_ARGS="-Djcma.request.timeout=120 -Djcma.socket.timeout=120 -Djcma.connect.timeout=120", with the reminder that "If the parameter JVM_SUPPORT_RECOMMENDED_ARGS exists on setenv.sh/setenv.bat, make sure to keep the existing parameters while adding the ones above." The ticket does not state the unit of those numbers, and it is in "Gathering Interest"; treat 120 as Atlassian's documented workaround value, not as shipped behaviour, and set it only when you have seen the string.
Switch off what competes. The speed-up page: "Disable non-essential tasks on the server ... Antivirus scan / Taking Jira backups (XML) / Full reindexing / Automation or Marketplace apps that might be updating issues in bulk / Other non-essential tasks. As much as possible, keep the resources available to the migration only." The knowledge base article on background jobs names the one most people forget: "Scheduled index snapshot that is by default setup to run every night", switched off at "Administration > System > Indexing > Scheduled index snapshot > Edit Settings > Turn off the Enable index recovery", and app jobs at "System > Scheduler details > Jobs tab ... Check for all jobs that are taking longer time to run and running too frequently ... Disable all such apps", with "Remember to enable Scheduled index snapshot and all disabled apps after your migration is complete". One more from the dark-feature article: if assessment and pre-flight run together with com.atlassian.jira.migration.enable.relations.analysis.during.instance.assessment enabled, "this can cause clogging on the issuelink table"; disable the flag if the database CPU climbs during assessment.
Nodes: one migration per two. Atlassian does not say to run the assistant on one node. It says the opposite-shaped thing: "we recommend only running one migration at a time per two Data Center nodes", because recovery depends on it: "if the server failure occurs during export, automatically recover the migration up to two times, as long as there are two Data Center nodes per migration and both don't crash at the same time / if the server failure occurs during import, automatically recover the migration an unlimited number of times". And the assessment "collects the data from the entire Data Center instance ... You also can't choose a specific node to run it on." So the nodes stay up and identical. Atlassian's speed-up page says not to run plans concurrently at all; the install page's ratio is about recovery, not a licence to run two.
Mail, on any clone. The shipped switch is line 42 of setenv.sh, uncommented. Atlassian's article on development servers explains why it is not optional on a staging clone: "if you bring up an identical copy of your running instance, you can have a recipe for disaster as it could result in incoming emails being parsed by your development instance, or erroneous notifications going out to users ... Extreme cases have resulted in system outages." It goes further than the flag: delete the mail handlers and the SMTP and IMAP or POP servers, and clear the queue with delete from filtersubscription; and delete from notificationinstance;. A second knowledge base article, on Jira still sending mail with the flag set, warns that the flag is not absolute: "Some add-ons may bypass the Jira global email settings and send email from Jira", naming ScriptRunner escalation scripts, Mail this issue, and Issue Reminders.
Order of operations, from Atlassian's downtime page. Attachments first, and early: "Migrating attachments is typically the longest part of the migration ... You can migrate attachments weeks or months ahead of the project or space migration ... We recommend that you migrate attachments before migrating other project or space data. This is because if you migrate attachments after migrating project or space, some attachments may not link correctly". Users a day ahead: "for large user bases, preferably 1 day before", and all of them rather than a selection: "migrating all users is much faster than selected users ... when migrating selected users, Jira Cloud Migration Assistant needs to run every single time through the entire user database". Projects grouped by size in one plan: "Don't run multiple migration plans at the same time. Projects within a migration plan are migrated in parallel. For faster migrations, we recommend that you include multiple projects with similar issue count in a single plan. Typically, the total migration time equals the time it takes to migrate the largest project." Jira has no read-only mode; Atlassian's substitute is "creating a permission scheme that only allows browse permission and applying it to all projects." Clean up before, never during: "Don't attempt to clean up your instance while a migration is running ... Cleaning up your data while a migration is running can cause the migration to fail", and for PostgreSQL the maintenance it names is vacuum full on jiraissue, jiraaction, changegroup, changeitem, worklog and customfieldvalue.
What to expect, in Atlassian's numbers. Throughput: "the average throughput of Jira Cloud Migration Assistant is 5,000,000 issues / 24 hours ... In some migrations, we've observed throughput as high as 21 million issues / 24 hours"; the insight page says "The median migration rate (based on all migrations we've observed in the past 3 months) is 7,5M issues per 24 hours"; the version page gives the before and after of 1.10.17 as 1,800,000 and 3,700,000 issues per 24 hours; attachments move at "600 GB per 24 hours". Three Atlassian pages, three throughput figures, all quoted. Pre-flight alarms: "Small instances: Over 2 hours / Medium instances: Over 4 hours / Large instances: Over 6 hours". Network: "we've observed that 250 Mbps improves the data upload", and "Ensure that your production environment doesn't have any additional security controls (e.g. egress traffic scanners) that may slow down data upload". Above 7,500,000 issues, Atlassian's own advice is phases, archiving inactive projects ("last issue update set to 24 months") and pre-migrating archived ones, with the caveat that "The biggest risk with the phased migration method is configuration drift."
The Cloud limits that bound what arrives. These are not JVM settings, and a large migration fails on them more often than on memory. Descriptions and comments: "Jira Cloud doesn't support more than 32,767 characters", and the assistant logs We couldn't import Comment <comment-id> ... Reason: CommentBodyCharacterLimitExceededException: No message. and We couldn't import Issue <issue-key>. Reason: description: The entered text is too long. It exceeds the allowed limit of 32,767 characters..; find them first with WHERE LENGTH(ji.description) > 32767 and WHERE a.actiontype = 'comment' AND LENGTH(a.actionbody) > 32767. Per work item since September 2025: 2000 attachments, 2000 links, 5000 comments, 10,000 worklogs; migrations are exempt and then transformed: "We'll remove excess attachments from your issue. We'll add them to an archive, and attach it to the original issue", and comments over the limit "into CSV files ... with the oldest comments being transformed first." Attachments: "The default (per file) is 1 GB. The maximum attachment size (per file) is 2 GB." Site guardrail: 30,000,000 work items. Custom fields: the insight fires above 24,000, and "in many cases a custom field is actually used by fewer than 10 projects."
How you know it worked: the pool never saturates in Database Monitoring during a test migration; cat /proc/<pid>/limits shows the raised open-files limit; the Scheduler details page shows the snapshot and app jobs disabled; and the test migration's duration, measured with Atlassian's query on the assistant's own tables, is inside the throughput band above:
sql
1SELECT pe."PLAN_NAME"AS plan_name, to_timestamp(pe."CREATED_TIMESTAMP"/1000)AS start_date,2 to_timestamp(me."UPDATED_TIMESTAMP"/1000)AS finish_date,3ROUND((me."UPDATED_TIMESTAMP"- pe."CREATED_TIMESTAMP")/1000/60,2)AS duration_minutes
4FROM"AO_6FF49D_PLAN_ENTITY" pe JOIN"AO_6FF49D_MIGRATION_ENTITY" me ON pe."ID"= me."PLAN_ID"5WHERE pe."PLAN_NAME"IN('<plan name>');
Step 7 — Enforce the cluster rules, and read what Atlassian has tested at scale
The cluster page's rules are what make step 3's heap a cluster setting: "All cluster nodes must: be located in the same data center, or region (for AWS and Azure); run the same Jira version; have the same OS, Java and application server version; have the same memory configuration (both the JVM and the physical memory) (recommended); be configured with the same time zone (and keep the current time synchronized)." There is no shared setenv.sh; the install page's instruction for adding a node is to "Copy the Jira installation and home directories from an existing node to the new node", and it names <installation-directory>/bin/setenv.sh among the files you modified and must carry. In cluster.properties, ehcache.peer.discovery stays at default, which the page marks recommended, against automatic, which "can be difficult to configure, and is not recommended by Atlassian"; the RMI ports 40001 and 40011 are open between nodes and closed to everyone else; the clocks agree, because "Unsynchronised server times can break the cache synchronisation."
Now the question a 20,000 to 30,000-user instance actually asks, which is how many nodes and how big. Atlassian's answer is in what it has tested and run, and the honest summary is that nothing Atlassian has published load-tests 2,000 concurrent users; its largest published figures are its own production and a 2018 whitepaper.
Its own production, as of the 2018 sampling on the deployment page: two Jira instances, one with "over 1.26 million" issues and "approximately 5000 concurrent users per hour, on average", the other with 661,000 issues and 3,500, each on three nodes, c5.9xlarge of 36 CPUs and 72 GB for the support instance and m4.4xlarge of 16 CPUs and 64 GB for jira.atlassian.com, both on a 24 GB G1 heap, 62 active database connections on average, and 40 to 43 apps. "Concurrent users per hour" is defined on the page as new authenticated sessions per hour, assumed to stay for the hour.
The extra-large benchmark on AWS: a dataset of 7,206,140 issues, 3,001 projects, 80,002 users, 1,513 custom fields and 55 million comments, driven by 144 scripted browsers "equivalent to 1,296,000 HTTP requests per hour", on heaps of 50 GB, or 25 GB on 16-core c5.4xlarge nodes. Atlassian's recommendations from it depend on the Jira version tested, six c5.9xlarge nodes for 8.5, two c5.4xlarge for 8.13, five c5.4xlarge for 9.12, and an older copy of the same study in Atlassian's security space says seven; quote them with the version. Two findings travel: from that study, "c5.18xlarge instances (72 CPUs and 144 GB RAM) performed worse than c4.8xlarge for Jira 8.5 and than c5.9xlarge for Jira 8.13", and from Atlassian's own per-release performance report, the caution that "you should not equate the number of browsers to represent the number of real-world concurrent users."
On node count Atlassian is consistent across three pages. "Atlassian's internal testers have tested up to 4 application nodes for Jira Data Center." "We've seen clusters with three nodes perform well in many enterprise-scale customer environments. We've also seen instances with five or more nodes struggle to keep up with lighter loads. Start testing your load with one node, then gradually scale until you get acceptable performance." "adding more application nodes to a growing Medium-sized instance doesn't always improve performance (in fact, the opposite might happen)." With document-based replication on, "the more nodes there are in the cluster, the better is the overall throughput while maintaining search consistency", tested on eight nodes at 400 requests per second where "the total network traffic volume amounted to 25% of total capacity of a 1Gbps link."
The 2018 whitepaper is the one Atlassian document with a concurrent-user curve, and it is about a single server versus a cluster: "Even with 500 concurrent users, 1 in 5 requests to a single server can take over 30 seconds to complete. Once we scale to 750 and 1,000 users, the server reaches the point where users begin to experience unacceptably slow response times." Its four data scenarios run from 2 to 10 million issues and 120,000 to 282,000 users, and "Data Center only needed 3 nodes to return requests nearly 20 times faster than a single server" for the 2-million scenario, six nodes for the 8 and 10 million ones. No heap is stated in it.
What drives the load is not the user count. From the size-profiles page: "The number of Custom Fields can greatly affect your instance's performance", and "You should also focus on keeping the following metrics down to Medium whenever possible: Custom Fields / Workflows / Permission Schemes / Issue Security Levels." From Atlassian's 7.7 scaling test: "of the eight data points we tested, the number of custom fields had the greatest effect on average response times." And the user guardrail page puts 20,000 to 30,000 users well inside its "Optimal: Less than 75,000 users" band, with degradation named above 100,000: "Slower login times / Degraded performance in user-related operations ... Longer reindexing times."
Practitioner reports, labelled as such. These are not Atlassian guidance and not verified instances; they are what named people have said in public. Matt Doar, author of "Practical JIRA Administration", on the Atlassian Community in 2021: "avoid JVM heap sizes of exactly 32GB due to Java oddness around that. 31GB or 34GB is our guideline internally"; note that 34 sits inside the band Atlassian's 2025 article forbids, and the Atlassian article is the primary source. Nic Brough, in the same thread: "For 250 users, a 20Gb heap probably is overkill. I'd generally start at 8 for that number, but the devil is in the detail of how they're using it", and "if you have a system that's suffering because it's doing frequent GC, you can make it worse by giving it more memory." A 2022 Community answer reports a Data Center instance "over 11 million issues ... making full use of archiving", with no sizing given. Atlassian's whitepaper narrates two customers, Cerner isolating API traffic to a dedicated node and a financial firm dedicating a node to indexing, with no sizes. Vendor blogs asserting specific heap-per-node-type numbers without naming an instance exist and are omitted here on purpose.
How you know it worked: every node shows the same memory column under Administration, System, Clustering; the Data Center health checks are green, which the troubleshooting article calls the goal: "Ultimately, we want to see the Data Center Health Checks all green"; and the replication statistics the performance article reads as healthy hold: timeToSendMillis around 10 ms, queueSize at 0, index replay timeInMillis around 5 seconds.
Step 8 — Verify from the startup log, then alert and measure
Every change above is verified the same way and on every node, because setenv.sh fails silently: a typo in a variable name is a variable Jira never reads.
The startup log first: <jira-home>/log/atlassian-jira.log, last startup, the line beginning JVM Input Arguments :. It should contain your -Xms and -Xmx, the code cache pair, the -Xlog:gc* line that set-gc-params.sh added, -XX:+ExplicitGCInvokesConcurrent, the heap-dump pair, and, during the migration, any jcma.* timeout you set. Then Administration, System, System support, System Info, where "The heap size is shown in the Java VM Memory Statistics section". Then the health checks: "The best first step to troubleshoot Jira Data Center is from the Health Check tools ... go to ⚙ > System > Support Tools."
Then the alerts, which are the most useful numbers on any Atlassian page about this subject because they are the thresholds Atlassian runs on its own Jira, over a five-minute window: garbage collection time, "An application node slows down if it spends too much CPU time in Garbage Collection", warning above 10%, critical above 20%; and Java heap consumed, warning above 90%, critical above 95%, with "If this alert gets triggered too often, it could mean we have a memory leak. Or it could mean we need to tweak the JVM Heap." The GC-overhead page's own target is stricter, below 5%; both are Atlassian, and the alert thresholds are the ones that page a human. Time in GC comes from the log; heap used from JMX or System Info. With both wired in, step 3's heap stops being an estimate and becomes a response to a measurement, which is the only way the "grow in 512 MB or 1 GB allotments" instruction can be followed honestly.
When an alert fires, collect before you change anything: "Take 3 thread dumps, 10 seconds apart", the GC logs, and a heap dump if memory is the suspect. Atlassian's symptom table maps "CPU usage spikes significantly (e.g.: 80-100% usage)" to "JVM Configuration / Insufficient Memory", with "If the CPU is spiking, check if there are consistent full GCs occurring, as garbage collection is CPU intensive." And the OutOfMemory page's caution before you grow: "Often, there is a root cause for OutOfMemory Errors that may be better to address than just increasing memory."
The migration itself is measured with the query in step 6, and the migration's data problems are found before it with the assessment SQL, which is the data-side precursor to this tutorial's host-side preparation; afterwards, the reason the migration's own report cannot verify it is why the audit has to be yours.
What this tutorial left out, on purpose
No heap ceiling, because Atlassian publishes none; the closest it comes is the forbidden 32 to 47 GB band and its own 24 GB in production. No pause target, thread count, region size or Metaspace cap, because no Atlassian page sets one for Jira and the JDK's advice on G1 is to remove every such flag. No ZGC, because Atlassian states it supports Parallel and G1 only. Pre-touch is in, measured and labelled JDK-only, because the JDK documents both the flag and the reason and the effect was large enough to show. No Jira maxThreads figure, because the one Atlassian page on it is behind a login. The disagreements between Atlassian's pages, 768 MB against the shipped 2048m, a third against 40% of heap in use, 5 million against 7.5 million issues a day, are shown as disagreements. And every command output above is OpenJDK 21.0.12.1 on a Mac, not a Jira node; the flags are Jira's flags, the workload in the sweep is synthetic, and the machine is not yours.
That is what a configuration you can put your name on looks like before a migration: every line traceable to the vendor who has to support it, every number either Atlassian's or measured, and the one calculation that is yours done from your own GC log.
Key takeaways
Start from the shipped setenv.sh and read set-gc-params.sh with it. The GC log flag is added on every start; JVM_SUPPORT_RECOMMENDED_ARGS is where additions go, in one place only.
Java 21, checked by Jira itself. The Java 8 GC flags stop the JVM on 21 and Jira ships no IgnoreUnrecognizedVMOptions; grep before the window.
Calculate the heap from four Atlassian inputs. The migration table (8 GB to 60,000 issues, 16 GB at 2 million and up, by cores), half of node RAM in three of Atlassian's four profile rows and one below half in the fourth, a third to 40% of heap live after collection in your own GC log, and never 32 to 47 GB. Xms equals Xmx; grow 512 MB or 1 GB at a time on evidence.
G1 as shipped, plus the heap-dump pair. Read the gc,init block for region size and compressed oops; keep GC overhead under 5%; pre-touch is JDK-documented, cut fill-phase pauses 6 to 11 times between 16 and 32 GB in the sweep and 29 times at 8 GB, costs about two-tenths of a second per 10 GB at start, and is not on any Atlassian page.
Code cache 512m until "CodeCache is full", then 1024m; Metaspace unlimited; know the four JQL safeguards.
For the migration: 16 or more cores, pool 20 to 40, open files near 32,768, the three jcma timeouts only on the timed-out string, index snapshot and app jobs off, one plan per two nodes, mail disabled on clones, attachments and users ahead, projects grouped by size, 5 to 7.5 million issues a day expected. Find the 32,767-character rows and the 2000-attachment and 5000-comment issues before, not after.
Same heap and RAM on every node, database discovery, RMI ports closed, clocks synced. Atlassian has tested four nodes, seen three perform well and five struggle; its extra-large test ran 7.2 million issues and 80,000 users on 25 and 50 GB heaps; custom fields, not users, drove response time in its tests.
Verify from JVM Input Arguments, alert at 10% and 20% time in GC and 90% and 95% heap, measure the migration with Atlassian's query. Those thresholds are Atlassian's own, and they turn the heap from a guess into a response.