JMeter Architecture, Java Setup, Installation, and First Test Plan: Guided Hands-On Workflow
Create a reproducible JMeter installation from the official binary release, verify its integrity before extraction, inspect the distribution, author a tiny loopback plan in the GUI, save the JMX, and execute the exact saved plan in CLI mode with raw and derived artifacts.
Learning objectives
- Verify Java identity and choose a clean JMeter installation path.
- Download the official JMeter 5.6.3 binary archive and verify its published SHA-512 before extraction.
-
Inspect
JMETER_HOME/bin, launchers, property files, and documentation layout. - Create a minimal loopback HTTP plan using the GUI and save it as versionable JMX.
-
Run the same plan from CLI with unique JTL,
jmeter.log, and dashboard paths. - Compare configured work with the independent fixture counter and generated artifacts.
1. Create a disposable project workspace
Use a path you control and keep the JMeter installation separate from the project. The project stores JMX, fixtures, configuration, and results; the Apache distribution remains an external tool dependency.
jmeter-ch02/
├── fixtures/
│ └── fixture_server.py
├── plans/
│ └── first-plan.jmx
├── conf/
├── results/
└── RUNBOOK.md
127.0.0.1:8000, uses one thread, one-second
ramp-up, and three iterations. Do not redirect it to a public or
shared service, and do not increase load while learning installation
mechanics.
2. Verify Java before downloading JMeter
Use Java 17 for the course labs. First prove which executable will run.
Linux/macOS:
command -v java
java -version
Windows PowerShell:
Get-Command java -All
java -version
If multiple Java installations appear, do not delete them. Record the selected path and decide deliberately which one should precede others in the lab shell. System-wide Java management is outside this chapter; a temporary shell-specific PATH is safer for the lesson.
3. Download the official archive and integrity material
The official download page currently offers
apache-jmeter-5.6.3.zip, a SHA-512 file, and a PGP
signature. Download the binary from an Apache mirror and obtain
integrity/signature material from the Apache distribution service.
Windows PowerShell:
$Version = "5.6.3"
$Zip = "apache-jmeter-$Version.zip"
Invoke-WebRequest `
"https://dlcdn.apache.org/jmeter/binaries/$Zip" `
-OutFile $Zip
Invoke-WebRequest `
"https://downloads.apache.org/jmeter/binaries/$Zip.sha512" `
-OutFile "$Zip.sha512"
Invoke-WebRequest `
"https://downloads.apache.org/jmeter/binaries/$Zip.asc" `
-OutFile "$Zip.asc"
Linux/macOS:
VERSION=5.6.3
ZIP="apache-jmeter-${VERSION}.zip"
curl -fLO "https://dlcdn.apache.org/jmeter/binaries/${ZIP}"
curl -fLO "https://downloads.apache.org/jmeter/binaries/${ZIP}.sha512"
curl -fLO "https://downloads.apache.org/jmeter/binaries/${ZIP}.asc"
4. Verify SHA-512 before extraction
For the current 5.6.3 binary ZIP, the published SHA-512 is:
387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076
Windows PowerShell:
$Expected = "387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076"
$Actual = (Get-FileHash .\apache-jmeter-5.6.3.zip -Algorithm SHA512).Hash.ToLowerInvariant()
if ($Actual -ne $Expected) {
throw "SHA-512 mismatch. Do not extract or run this archive."
}
"SHA-512 OK: $Actual"
Linux:
echo "387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076 apache-jmeter-5.6.3.zip" | sha512sum -c -
macOS:
echo "387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076 apache-jmeter-5.6.3.zip" | shasum -a 512 -c -
The Apache download page also publishes an OpenPGP signature and KEYS file. If GnuPG is available, verify the signature as an additional authenticity check:
curl -fLo KEYS https://downloads.apache.org/jmeter/KEYS
gpg --import KEYS
gpg --verify apache-jmeter-5.6.3.zip.asc apache-jmeter-5.6.3.zip
Do not continue after a checksum/signature failure. “It probably downloaded correctly” is not an acceptable toolchain provenance rule.
5. Unpack into a clean path
Apache's installation guidance is to unpack the release. It also warns that spaces in the installation directory can cause problems, especially in client/server mode. Use a short, stable path.
Windows PowerShell:
New-Item -ItemType Directory -Force C:\Tools | Out-Null
Expand-Archive .\apache-jmeter-5.6.3.zip -DestinationPath C:\Tools
$env:JMETER_HOME = "C:\Tools\apache-jmeter-5.6.3"
$env:PATH = "$env:JMETER_HOME\bin;$env:PATH"
Linux/macOS:
mkdir -p "$HOME/tools"
unzip apache-jmeter-5.6.3.zip -d "$HOME/tools"
export JMETER_HOME="$HOME/tools/apache-jmeter-5.6.3"
export PATH="$JMETER_HOME/bin:$PATH"
These environment changes are intentionally shell-local. They do not require changing machine-wide settings for the lesson.
6. Inspect the distribution before editing anything
Read the structure instead of treating the install as a black box.
Typical top-level directories include bin,
docs, extras, lib,
licenses, and printable_docs. The
launchers and primary property files live under bin.
Linux/macOS:
printf 'JMETER_HOME=%s
' "$JMETER_HOME"
ls -la "$JMETER_HOME"
ls -la "$JMETER_HOME/bin" | sed -n '1,60p'
jmeter -v
Windows PowerShell:
"JMETER_HOME=$env:JMETER_HOME"
Get-ChildItem $env:JMETER_HOME
Get-ChildItem "$env:JMETER_HOME\bin" |
Select-Object -First 60 Name, Length
jmeter.bat -v
Locate, but do not edit, jmeter.properties,
user.properties, system.properties, and
log4j2.xml. Later lessons will change configuration
through explicit project/local layers rather than silently modifying
distribution defaults.
7. Start the disposable loopback target
Save the following as fixtures/fixture_server.py. It
binds only to loopback, exposes read-only health/stats endpoints,
and caps the synthetic delay.
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs
import json
import threading
import time
HOST = "127.0.0.1"
PORT = 8000
lock = threading.Lock()
total = 0
active = 0
class Handler(BaseHTTPRequestHandler):
def _json(self, status, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
global total, active
parsed = urlparse(self.path)
if parsed.path == "/health":
self._json(200, {"status": "ok"})
return
if parsed.path == "/stats":
with lock:
snapshot = {"active": active, "total": total}
self._json(200, snapshot)
return
if parsed.path != "/work":
self._json(404, {"error": "not_found"})
return
values = parse_qs(parsed.query)
try:
delay_ms = int(values.get("delay_ms", ["30"])[0])
except ValueError:
delay_ms = 30
delay_ms = min(max(delay_ms, 0), 250)
with lock:
total += 1
active += 1
request_number = total
active_now = active
try:
time.sleep(delay_ms / 1000.0)
self._json(
200,
{
"status": "ok",
"request_number": request_number,
"active_at_start": active_now,
"delay_ms": delay_ms,
},
)
finally:
with lock:
active -= 1
def log_message(self, format, *args):
return
if __name__ == "__main__":
print(f"fixture=http://{HOST}:{PORT}")
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
Start it from the project root:
python fixtures/fixture_server.py
Preflight:
curl --fail --silent http://127.0.0.1:8000/health
PowerShell:
(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8000/health).Content
8. Author the first plan in the GUI
Launch JMeter only for authoring:
jmeter
In the GUI:
- Rename the root Test Plan to Chapter 02 First Plan.
- Add Threads (Users) → Thread Group. Name it One User — Three Iterations.
- Set Number of Threads to 1, Ramp-up to 1 second, and Loop Count to 3.
- Under the Thread Group add Sampler → HTTP Request.
-
Name it GET /work; protocol
http; server127.0.0.1; port8000; method GET; path/work?delay_ms=30. - Set connect timeout to 1000 ms and response timeout to 2000 ms.
- Save as
plans/first-plan.jmx.
A Test Plan is the root configuration. A Thread Group defines the worker-user schedule. An HTTP Request sampler creates one HTTP sample per execution. Their deeper scope/execution rules belong to Chapter 03; for now, the important fact is that the sampler sits inside the Thread Group and therefore executes for each thread iteration.
9. Inspect the saved JMX as code
Close neither the evidence chain nor your editor at the GUI
boundary. Open plans/first-plan.jmx in a text editor or
source-control diff. Its essential structure should correspond to:
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan"
testname="Chapter 02 First Plan" enabled="true">
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables"
elementType="Arguments"
guiclass="ArgumentsPanel"
testclass="Arguments"
testname="User Defined Variables">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup"
testname="One User — Three Iterations" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller"
elementType="LoopController"
guiclass="LoopControlPanel"
testclass="LoopController"
testname="Loop Controller">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">3</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">1</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui"
testclass="HTTPSamplerProxy"
testname="GET /work" enabled="true">
<elementProp name="HTTPsampler.Arguments"
elementType="Arguments"
guiclass="HTTPArgumentsPanel"
testclass="Arguments"
testname="User Defined Variables">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">127.0.0.1</stringProp>
<stringProp name="HTTPSampler.port">8000</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.path">/work?delay_ms=30</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.connect_timeout">1000</stringProp>
<stringProp name="HTTPSampler.response_timeout">2000</stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
Exact formatting/order may differ when JMeter saves the file. Compare semantic fields, not whitespace. The JMX should still show the loopback host, port 8000, one thread, three loops, sampler label, method, path, and timeouts.
10. Execute the saved plan from CLI into unique artifacts
Create only the parent run directory; let the report output path be new/empty as required by the report generator.
Linux/macOS:
mkdir -p results/run-001
jmeter -n -t plans/first-plan.jmx -l results/run-001/results.jtl -j results/run-001/jmeter.log -e -o results/run-001/report
Windows PowerShell:
New-Item -ItemType Directory -Force results\run-001 | Out-Null
jmeter.bat -n `
-t plans\first-plan.jmx `
-l results\run-001\results.jtl `
-j results\run-001\jmeter.log `
-e -o results\run-001\report
The same saved JMX is now executed without the GUI. The expected configured sample count is three. Query the fixture afterward:
curl --fail --silent http://127.0.0.1:8000/stats
If the fixture started fresh and no GUI debug run was performed,
total should be 3. If you did perform a bounded GUI
debug run first, record that fact and restart the fixture before the
CLI evidence run so the counter has a known baseline.
11. Inspect the output directory tree
results/run-001/
├── results.jtl
├── jmeter.log
└── report/
├── index.html
├── content/
└── sbadmin2-1.0.7/
The exact dashboard asset set can evolve, so do not gate on every
internal file name. Gate on the important artifacts: raw JTL exists
and is non-empty, jmeter.log exists, the report
directory contains index.html, and the sample
count/correctness matches the experiment.
12. Challenge: which state should change?
You need a second local environment using the same JMeter binary and plan but a different loopback port. Which layer should carry that change?
Do not copy and hand-edit the JMeter distribution. Prefer an explicit project property/CLI override or a version-controlled alternate plan, depending on how the project is designed. Chapter 06 will teach variables and properties fully. The key reasoning is that environment-local target configuration should be traceable and should not mutate global tool defaults.
Knowledge check
Why verify SHA-512 before extraction?
Because toolchain provenance starts before execution. A mismatched archive must not be trusted or run.
Why keep JMeter under a clean path such as C:\Tools or ~/tools?
It reduces path/space ambiguity and keeps the external tool distribution separate from project source and results.
What does -j change in the CLI command?
It gives the JMeter engine log an explicit per-run path instead of relying on the default jmeter.log location/name.
Why restart the fixture before the final CLI evidence run if you already clicked Run in the GUI?
So the target counter begins from a known state and can independently verify the CLI run's request count.
What is wrong with committing a locally edited jmeter.properties as the project configuration strategy?
It mutates distribution defaults, couples the project to one installation, and makes upgrades/reproduction harder. Prefer explicit user/project property layers or CLI overrides.
Official references and version notes
- Apache JMeter downloads — production release, binary/source archives, SHA-512, PGP signatures, and release Java requirement.
- Getting Started — installation layout, launchers, GUI/CLI boundary, CLI flags, logging, property overrides, and directory-path guidance.
- Best Practices — CLI execution and listener/resource guidance.
-
Properties Reference
—
user.properties,system.properties, and property layering. -
Generating Dashboard Report
—
-e -oand post-run report generation. - Apache JMeter development repository — current development-line runtime requirement; do not confuse it with the 5.6.3 release requirement.
Version-sensitive statements were rechecked against current Apache
JMeter primary sources on 2026-09-04. The current production
release is Apache JMeter 5.6.3, whose download
page states Java 8+; the 5.6.x change notes
recommend Java 17 or later. The current development
repository/next major line requires Java 17, so those development
requirements are not retroactively applied to the 5.6.3 release.
Mandatory labs use Java 17, the official 5.6.3 binary archive, no
third-party plugins, the loopback target
127.0.0.1:8000, and CLI mode for the actual load run.
The published SHA-512 for apache-jmeter-5.6.3.zip is
387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.