Switched to Java 16 as minimal build environment. With this version (since Java 9) we can't use javac "bootclasspath", and "source", and "target", but "release". Also, removed RMI sub generation for the debug API, as rmic was removed since JDK 13 or so, and modern RMI doesn't need stubs anyway (also probably nobody uses the debug API).
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1fff45a..95403ad 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,6 +47,12 @@
           java-version: 8
           distribution: temurin
 
+      - name: Set up JDK 16
+        uses: actions/setup-java@v3
+        with:
+          java-version: 16
+          distribution: zulu
+
       - name: Prepare build.properties
         shell: bash
         run: >-
diff --git a/README.md b/README.md
index c2dda86..201a6ab 100644
--- a/README.md
+++ b/README.md
@@ -106,7 +106,7 @@
 the source code repository. See repository locations here:
 https://freemarker.apache.org/sourcecode.html
 
-You need JDK 8 (not JDK 9!), Apache Ant (tested with 1.9.6) and Ivy (tested
+You need JDK 16, Apache Ant (tested with 1.10.6) and Ivy (integrated into
 with 2.5.0) to be installed. To install Ivy (but be sure it's not already
 installed), issue `ant download-ivy`; it will copy Ivy under `~/.ant/lib`.
 (Alternatively, you can copy `ivy-<version>.jar` into the Ant home `lib`
@@ -114,7 +114,7 @@
 
 It's recommended to copy `build.properties.sample` into `build.properties`,
 and edit its content to fit your system. (Although basic jar building should
-succeeds without the build.properties file too.)
+succeed without the build.properties file too.)
 
 To build `freemarker.jar`, just issue `ant` in the project root directory, and
 it should download all dependencies automatically and build `freemarker.jar`. 
@@ -165,7 +165,7 @@
     Number of imports required for .*: 99
     Number of static imports needed for .*: 1
   - Java -> Installed JRE-s:
-    Ensure that you have JDK 8 installed, and that it was added to Eclipse.
+    Ensure that you have JDK 16 installed, and that it was added to Eclipse.
     Note that it's not JRE, but JDK.
   - Java -> Compiler -> Javadoc:
     "Malformed Javadoc comments": Error
@@ -188,7 +188,7 @@
         src/test/resources
     - On the "Libraries" tab:
       - Delete everyhing from there, except the "JRE System Library [...]"
-      - Edit "JRE System Library [...]" to "Execution Environment" "JavaSE 1.8"
+      - Edit "JRE System Library [...]" to "Execution Environment" "JavaSE 16"
       - Add all jar-s that are directly under the "ide-dependencies" directory
         (use the "Add JARs..." and select all those files).
    - Press "Finish"
@@ -206,7 +206,6 @@
     _Jython22*.java,
     _FreeMarkerPageContext2.java,
     FreeMarkerJspFactory2.java,
-    Java8*.java
   Also, close these files if they are open. Now you shouldn't have any errors.
 - At Project -> Properties -> Java Code Style -> Formatter, check in "Enable
   project specific settings", and then select "FreeMarker" as active profile.
@@ -233,7 +232,7 @@
 
 - "New" -> "Project". In order as the IntelliJ will prompt you:
 
-  - Select "Java" on the left side, and "1.8" for SDK on the right side. Press "Next".
+  - Select "Java" on the left side, and "16" for SDK on the right side. Press "Next".
   
   - Template selection: Don't chose anything, "Next"
   
@@ -265,7 +264,7 @@
     - Test Resource Folders:  
       src/test/resources
       
-  - Still inside the "Sources" tab, change the "Language level" to "7". (Yes, we use Java 8 SDK with
+  - Still inside the "Sources" tab, change the "Language level" to "7". (Yes, we use Java 16 SDK with
     language level 7 in the IDE, due to the tricks FreeMarker uses to support different Java versions.)
     
   - Switch over to the "Dependencies" tab (still inside "Project Structure" / "Modules"), and add
@@ -281,7 +280,6 @@
     _Jython22*.java,  
     _FreeMarkerPageContext2.java,  
     FreeMarkerJspFactory2.java,  
-    Java8*.java  
 
 - You may do "Build" / "Build project" (Ctrl+F9) to see if everything compiles now.
     
diff --git a/build.xml b/build.xml
index 6c499af..905411f 100644
--- a/build.xml
+++ b/build.xml
@@ -19,14 +19,13 @@
 -->
 
 <project basedir="." default="jar" name="freemarker"
-  xmlns:ivy="antlib:org.apache.ivy.ant"
-  xmlns:javacc="http://javacc.dev.java.net/"
-  xmlns:docgen="http://freemarker.org/docgen"
-  xmlns:bnd="http://www.aqute.biz/bnd"
-  xmlns:rat="antlib:org.apache.rat.anttasks"
-  xmlns:u="http://freemarker.org/util"
-  xmlns:if="ant:if"
-  xmlns:unless="ant:unless"
+         xmlns:ivy="antlib:org.apache.ivy.ant"
+         xmlns:javacc="http://javacc.dev.java.net/"
+         xmlns:docgen="http://freemarker.org/docgen"
+         xmlns:bnd="http://www.aqute.biz/bnd"
+         xmlns:rat="antlib:org.apache.rat.anttasks"
+         xmlns:u="http://freemarker.org/util"
+         xmlns:unless="ant:unless"
 >
 
   <!-- ================================================================== -->
@@ -59,7 +58,7 @@
   </condition>
   <available property="atLeastJDK8" classname="java.util.function.Predicate"/>
 
-  <!-- When boot.classpath.j2se* is missing, these will be the defaults: -->
+  <!-- When boot.classpath.j* is missing, these will be the defaults: -->
   <!-- Note: Target "dist" doesn't allow using these. -->
   <property name="boot.classpath.j2se1.7" value="${sun.boot.class.path}" />
   <property name="boot.classpath.j2se1.8" value="${sun.boot.class.path}" />
@@ -265,10 +264,12 @@
       includes="freemarker/core/_Java8Impl.java"
     />
 
-    <rmic
-      base="build/classes" includes="freemarker/debug/impl/Rmi*Impl.class"
-      classpathref="ivy.dep"
-      verify="yes" stubversion="1.2"
+    <ivy:cachepath conf="build.base" pathid="ivy.dep" />
+    <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
+           debug="on" optimize="off" release="16" encoding="utf-8"
+           includeantruntime="false"
+           classpathref="ivy.dep"
+           includes="freemarker/core/_Java16Impl.java"
     />
 
     <ivy:cachepath conf="build.jsp2.0" pathid="ivy.dep.jsp2.0" />
@@ -336,11 +337,6 @@
         freemarker/ext/jython/_Jython25VersionAdapter.java"
     />
 
-    <rmic base="build/classes" classpathref="ivy.dep"
-      includes="build/src-main-java-filtered/freemarker/debug/impl/Rmi*Impl.class"
-      verify="yes" stubversion="1.2"
-    />
-
     <copy toDir="build/classes">
       <fileset dir="src/main/resources"
         excludes="
@@ -364,11 +360,10 @@
 
     <ivy:cachepath conf="build.test" pathid="ivy.dep.build.test" />
     <javac srcdir="src/test/java" destdir="build/test-classes" deprecation="off"
-      debug="on" optimize="off" target="1.8" source="1.8" encoding="utf-8"
+      debug="on" optimize="off" release="16" encoding="utf-8"
       includeantruntime="false"
       classpath="build/classes"
       classpathref="ivy.dep.build.test"
-      bootclasspath="${boot.classpath.j2se1.8}"
     />
     <copy toDir="build/test-classes">
       <fileset dir="src/test/resources"
diff --git a/osgi.bnd b/osgi.bnd
index 3cf11c1..9345879 100644
--- a/osgi.bnd
+++ b/osgi.bnd
@@ -49,10 +49,10 @@
 # This is needed for "a.class.from.another.Bundle"?new() to work.
 DynamicImport-Package: *
 
-# The required minimum is 1.7, but we utilize versions up to 1.8 if available.
+# The required minimum is 1.7, but we utilize versions up to 16 if available.
 # See also: http://wiki.eclipse.org/Execution_Environments, "Compiling
 # against more than is required"
-Bundle-RequiredExecutionEnvironment: JavaSE-1.8, JavaSE-1.7
+Bundle-RequiredExecutionEnvironment: JavaSE-16, JavaSE-1.7
 
 # Non-OSGi meta:
 Main-Class: freemarker.core.CommandLine
diff --git a/src/main/java/freemarker/core/_Java16.java b/src/main/java/freemarker/core/_Java16.java
new file mode 100644
index 0000000..84af6cd
--- /dev/null
+++ b/src/main/java/freemarker/core/_Java16.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.core;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+
+/**
+ * Used internally only, might change without notice!
+ * Used for accessing functionality that's only present in Java 8 or later.
+ */
+public interface _Java16 {
+
+    boolean isRecord(Class<?> cl);
+
+    Set<Method> getComponentAccessors(Class<?> recordClass);
+
+}
diff --git a/src/main/java/freemarker/core/_Java16Impl.java b/src/main/java/freemarker/core/_Java16Impl.java
new file mode 100644
index 0000000..ba1ba87
--- /dev/null
+++ b/src/main/java/freemarker/core/_Java16Impl.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.core;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.RecordComponent;
+import java.util.IdentityHashMap;
+import java.util.Set;
+
+/**
+ * Used internally only, might change without notice!
+ * Used for accessing functionality that's only present in Java 16 or later.
+ */
+// Compile this against Java 16
+@SuppressWarnings("Since15") // For IntelliJ inspection
+public class _Java16Impl implements _Java16 {
+
+    public static final _Java16 INSTANCE = new _Java16Impl();
+
+    private _Java16Impl() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public boolean isRecord(Class<?> cl) {
+        return cl.isRecord();
+    }
+
+    @Override
+    public Set<Method> getComponentAccessors(Class<?> recordType)  {
+        RecordComponent[] recordComponents = recordType.getRecordComponents();
+        if (recordComponents == null) {
+            throw new IllegalArgumentException("Argument must be a record type");
+        }
+        IdentityHashMap<Method, Void> methods = new IdentityHashMap<>(recordComponents.length);
+        for (RecordComponent recordComponent : recordComponents) {
+            methods.put(recordComponent.getAccessor(), null);
+        }
+        return methods.keySet();
+    }
+}
diff --git a/src/main/java/freemarker/core/_JavaVersions.java b/src/main/java/freemarker/core/_JavaVersions.java
index 5670c36..9023b64 100644
--- a/src/main/java/freemarker/core/_JavaVersions.java
+++ b/src/main/java/freemarker/core/_JavaVersions.java
@@ -31,28 +31,8 @@
         // Not meant to be instantiated
     }
 
-    private static final boolean IS_AT_LEAST_8;
-    static {
-        boolean result = false;
-        String vStr = SecurityUtilities.getSystemProperty("java.version", null);
-        if (vStr != null) {
-            try {
-                Version v = new Version(vStr);
-                result = v.getMajor() == 1 && v.getMinor() >= 8 || v.getMajor() > 1;
-            } catch (Exception e) {
-                // Ignore
-            }
-        } else {
-            try {
-                Class.forName("java.time.Instant");
-                result = true;
-            } catch (Exception e) {
-                // Ignore
-            }
-        }
-        IS_AT_LEAST_8 = result;
-    }
-    
+    private static final boolean IS_AT_LEAST_8 = isAtLeast(8, "java.time.Instant");
+
     /**
      * {@code null} if Java 8 is not available, otherwise the object through with the Java 8 operations are available.
      */
@@ -75,5 +55,51 @@
         }
         JAVA_8 = java8;
     }
-    
+
+    private static final boolean IS_AT_LEAST_16 = isAtLeast(16, "java.net.UnixDomainSocketAddress");
+
+    /**
+     * {@code null} if Java 8 is not available, otherwise the object through with the Java 8 operations are available.
+     */
+    static public final _Java16 JAVA_16;
+    static {
+        _Java16 java16;
+        if (IS_AT_LEAST_16) {
+            try {
+                java16 = (_Java16) Class.forName("freemarker.core._Java16Impl").getField("INSTANCE").get(null);
+            } catch (Exception e) {
+                try {
+                    Logger.getLogger("freemarker.runtime").error("Failed to access Java 16 functionality", e);
+                } catch (Exception e2) {
+                    // Suppressed
+                }
+                java16 = null;
+            }
+        } else {
+            java16 = null;
+        }
+        JAVA_16 = java16;
+    }
+
+    private static boolean isAtLeast(int minimumMinorVersion, String proofClassPresence) {
+        boolean result = false;
+        String vStr = SecurityUtilities.getSystemProperty("java.version", null);
+        if (vStr != null) {
+            try {
+                Version v = new Version(vStr);
+                result = v.getMajor() == 1 && v.getMinor() >= minimumMinorVersion || v.getMajor() > 1;
+            } catch (Exception e) {
+                // Ignore
+            }
+        } else {
+            try {
+                Class.forName(proofClassPresence);
+                result = true;
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+        return result;
+    }
+
 }
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index d9edddb..e8f98da 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20,10 +20,7 @@
 <book conformance="docgen" version="5.0" xml:lang="en"
       xmlns="http://docbook.org/ns/docbook"
       xmlns:xlink="http://www.w3.org/1999/xlink"
-      xmlns:ns5="http://www.w3.org/1998/Math/MathML"
-      xmlns:ns4="http://www.w3.org/2000/svg"
-      xmlns:ns3="http://www.w3.org/1999/xhtml"
-      xmlns:ns="http://docbook.org/ns/docbook">
+>
   <info>
     <title>Apache FreeMarker Manual</title>
 
@@ -16105,8 +16102,9 @@
                   setting</link> is less than 2.3.21: Gives what
                   <literal>java.text.DecimalFormat</literal> does with US
                   locale, which are <literal>∞</literal>,
-                  <literal>-∞</literal>, and <literal>�</literal> (U+FFFD,
-                  replacement character).</para>
+                  <literal>-∞</literal>, and <literal>NaN</literal> (before
+                  Java 11 was <literal>�</literal>, i.e., U+FFFD, replacement
+                  character).</para>
                 </listitem>
               </itemizedlist>
             </listitem>
@@ -30088,6 +30086,21 @@
               <literal>LookAheadSuccess</literal> with stack trace instance
               for each <literal>FMParser</literal> instance.</para>
             </listitem>
+
+            <listitem>
+              <para>We don't generate RMIC stub classes for
+              <literal>freemarker.debug.impl.Rmi*Impl</literal> classes
+              anymore. Almost nobody uses that API, but if you do, you
+              certainly already rely on dynamic stubs anyway. (The
+              <literal>rmic</literal> tool used to generate the stub classes
+              was removed from the JDK, starting with JDK 15.)</para>
+            </listitem>
+
+            <listitem>
+              <para>To build FreeMarker itself (not for just using it), now
+              you need JDK 16 (or later). This doesn't influence the minimum
+              Java version requirement for using FreeMarker.</para>
+            </listitem>
           </itemizedlist>
         </section>
       </section>