Chapter 02Lesson 02~150 minutes

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.

InstallSHA-512GUI authoringCLI executionJTL

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
Safety ceiling: the first executable plan targets only 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:

  1. Rename the root Test Plan to Chapter 02 First Plan.
  2. Add Threads (Users) → Thread Group. Name it One User — Three Iterations.
  3. Set Number of Threads to 1, Ramp-up to 1 second, and Loop Count to 3.
  4. Under the Thread Group add Sampler → HTTP Request.
  5. Name it GET /work; protocol http; server 127.0.0.1; port 8000; method GET; path /work?delay_ms=30.
  6. Set connect timeout to 1000 ms and response timeout to 2000 ms.
  7. 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?

Why keep JMeter under a clean path such as C:\Tools or ~/tools?

What does -j change in the CLI command?

Why restart the fixture before the final CLI evidence run if you already clicked Run in the GUI?

What is wrong with committing a locally edited jmeter.properties as the project configuration strategy?

Next lesson

Make configuration and distribution choices explicit

Lesson 3 compares official binary archives with pinned external container images, runtime versus JDK needs, GUI versus CLI, local versus committed configuration, and exact pinning versus planned upgrades.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.