Most of the Talend estates we are asked to look at in 2026 are still compiling and running against Java 8. That was fine for a long time. It is not fine now: vendor support windows for JDK 8 have narrowed to paid extended terms, security scanners flag the runtime on every audit, modern driver versions (Snowflake, JDBC Postgres, Kafka clients, AWS SDK v2) have dropped or are dropping Java 8 baselines, and the current Talend 8 patch line is built and certified against Java 17. Java 21 is the next long-term-support target and already shows up on newer remote engine and container images.
This post is the runbook we hand to clients doing that move. It assumes a Talend 8.0.1 estate with Studio, a Git repo, a Maven/Nexus build, and either Talend Management Console job servers or a self-hosted scheduler. Nothing here requires you to rewrite jobs.
Do the upgrade in this order
The single biggest source of pain is upgrading pieces out of order and then not knowing which layer broke. Move one layer at a time, and keep each layer able to talk to the one below it.
- Runtime for a throwaway job server first. Stand up one job server or one container image on the new JDK and run existing Java 8-compiled artifacts on it. Java is backward compatible at the bytecode level, so a job built with
--release 8will usually run on 17 unchanged. This step alone flushes out reflection and security-manager problems without touching a single build. - Build agents second. Point your CI runner at the new JDK but keep the Maven compiler target at 8 initially. You now prove the toolchain (Maven, the Talend CommandLine, plugin versions) works on the new JVM.
- Compiler target third. Flip the target to 17 and rebuild everything. This is the step that surfaces genuine source-level breakage.
- Studio last, and per developer. Studio has its own bundled JRE and its own supported-JDK matrix per patch level. Check your exact patch release notes before switching, and upgrade one developer's workstation first so a broken Studio does not stop the whole team.
- Retire the old path only after two full release cycles. Keep the Java 8 job server alive and able to run a rollback artifact until you have shipped twice on the new one.
If your Talend patch level predates Java 17 certification, patch Talend before you touch the JDK. Running a certified-for-8-only Studio on 17 produces build failures that look like code problems and are not.
What actually breaks in job code
Across migrations, the failures cluster in a short list. None of them are exotic; they are all things that were legal on 8 and are not anymore.
Internal JDK classes in routines. Anything importing sun.misc.*, com.sun.* internals, or javax.xml.bind.* fails outright. The two common offenders in Talend routines are sun.misc.BASE64Encoder and JAXB. Replace the first with java.util.Base64:
// old, does not compile on 17
// return new sun.misc.BASE64Encoder().encode(bytes);
public static String encode(byte[] bytes) {
if (bytes == null) { return null; }
return java.util.Base64.getEncoder().encodeToString(bytes);
}
JAXB was removed from the JDK in 11. If a routine or a tJava block uses it, add the standalone artifacts (jakarta.xml.bind-api plus a runtime implementation) to the job's Maven dependencies rather than trying to re-enable the old module.
Illegal reflective access that used to be a warning. On Java 8 and 11, libraries poking at private JDK internals printed a warning. On 17 the strong encapsulation is enforced and you get InaccessibleObjectException. Old Jackson, old POI, old Groovy, old Hibernate and hand-rolled serialization helpers are the usual culprits. The right fix is upgrading the library. The tactical fix, when a component ships a pinned old jar you cannot replace, is a targeted --add-opens:
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
Add them one at a time based on the exception message. A blanket list of twenty --add-opens flags is a smell and will hide the next real problem.
Date and time formatting drift. Java 9 switched the default locale data provider from JRE to CLDR. Month abbreviations, week-of-year numbering and some short date patterns change. If your jobs parse or emit dates with SimpleDateFormat and a locale-dependent pattern — anything using MMM, EEE, or ww — either pin the old behaviour with -Djava.locale.providers=COMPAT,CLDR as a bridge or, better, fix the patterns and move the routine to java.time.format.DateTimeFormatter with an explicit locale.
tMap expressions that relied on lenient parsing. We regularly find tMap expressions doing Integer.parseInt(row1.value) on strings with leading + signs or whitespace where the old behaviour happened to be forgiving in combination with an old library. Wrap them in the same defensive routine you should already have, and add a reject path.
Removed -XX flags. CMS (-XX:+UseConcMarkSweepGC) was removed in 14. Several PermGen-era flags are gone. Any job server start script carrying flags copied from a 2014 wiki page will refuse to start the JVM at all. Strip the script back to the minimum and re-add only what you can justify.
Signed or shaded jars in the routine classpath. Java 17 is stricter about jars with broken or weak signatures (MD5-signed jars in particular). If a vendor JDBC driver from 2013 is in your lib folder, this is your cue to upgrade the driver.
JVM flags worth setting on 17 and 21
Once you are on a modern JDK, the defaults are much better than they used to be, so keep the list short.
-XX:MaxRAMPercentage=75
-XX:+UseG1GC
-XX:+ExitOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/talend/heapdumps
-Djava.security.egd=file:/dev/urandom
-Duser.timezone=UTC
-Dfile.encoding=UTF-8
The two that matter most in practice:
MaxRAMPercentageinstead of a fixed-Xmx. Modern JVMs are container-aware and read cgroup limits. Setting a percentage means the same job image behaves sensibly whether it gets a 2 GB or a 16 GB pod, which is exactly what you want if you are running jobs as Kubernetes CronJobs. Fixed-Xmxvalues baked into a job's Advanced settings are the number one cause of "it OOMs only in prod".ExitOnOutOfMemoryError. A Talend job that hits OOM in one subjob and limps on will happily write a partial load and exit 0. Failing hard is what makes your scheduler and your reruns honest.
On Java 21, note that file.encoding now defaults to UTF-8 everywhere (that landed in 18). If any job reads a Windows-1252 flat file and previously relied on the platform default, set the encoding explicitly on the tFileInput component rather than fighting it globally. Also worth knowing: generational ZGC is available on 21 and is genuinely good for long-running route or streaming jobs with large heaps, but G1 remains the right default for short-lived batch jobs.
Virtual threads are the headline Java 21 feature and they will not help a generated Talend job. Talend's generated code uses its own threading model for parallel execution; you do not get Loom benefits for free, and hand-writing virtual threads in a tJava block inside a job server you do not control is not a trade we recommend.
Container base images
If you already containerize jobs, this migration is mostly a one-line change plus a test pass:
# was: FROM eclipse-temurin:8-jre-alpine
FROM eclipse-temurin:21-jre-alpine
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError -Duser.timezone=UTC"
COPY target/job/ /opt/job/
ENTRYPOINT ["/opt/job/jobname_run.sh"]
Two cautions. First, pin to a digest, not a floating tag, or your "unchanged" job will silently move JDK patch levels between runs. Second, if you use Alpine, confirm your drivers and any native libraries work against musl; the -jre Debian-based variants are a safer default for data work and the size difference rarely matters.
A test plan that does not require a freeze
You do not need a change freeze for this, and asking for one usually kills the project. What you need is a comparison harness.
- Pick 20 representative jobs, not the 20 easiest: one big bulk load, one CDC job, one API consumer, one XML or fixed-width parser, one job with heavy routine code, one job with a date-sensitive transformation, one MDM or data-quality job if you still have them.
- Run each on both JDKs against the same input snapshot, writing to two schemas. Compare row counts, checksums per column, and reject-file contents. Column-level checksums are what catch the locale and encoding problems; row counts alone will pass while dates silently shift.
- Capture wall-clock and peak heap for each. Most jobs get modestly faster on 17 and 21. Any job that gets materially slower is usually a GC-flag leftover, not the JDK.
- Then batch the rest by pattern. Once a pattern is proven, the remaining jobs of that pattern move in groups of fifty with a spot-check, not a full diff.
- Keep the rollback trivial. The old artifact and the old job server stay available. If the rollback plan is "rebuild everything on 8 in a hurry", you do not have one.
Where this usually lands
For an estate of a few hundred jobs with a normal amount of routine code, the JDK move is typically two to four weeks of work, and the majority of it is the comparison harness rather than code fixes. The code fixes themselves are usually a handful of routines, one or two driver upgrades and a start script that needed cleaning up anyway.
The reason to do it now is not the JDK for its own sake. It is that everything else you are likely to want next — current Snowflake and Databricks drivers, Iceberg writes, modern Kafka clients, Qlik Talend Cloud remote engines, container-based scheduling — assumes a modern runtime. Staying on Java 8 quietly closes those doors one driver release at a time.
If you want a second pair of eyes on a Java upgrade plan, or a routine-code audit before you flip the compiler target, get in touch — it is a well-trodden path and there is no reason to discover the breakage in production.