Differences between Java 8 and Java 11+

Java 8 has reached end of support on January 31, 2024. Your existing Java 8 applications will continue to run and receive traffic. However, App Engine might block re-deployment of applications that use runtimes after their end of support date. We recommend that you migrate to the latest supported version of Java by using the guidelines in this page.

Migrating to the Java 11+ runtimes, also known as the second-generation Java runtimes, allow you to use up-to-date language features and build apps that are more portable, with idiomatic code.

Understanding your migration options

To reduce runtime migration effort and complexity, the App Engine standard environment lets you access many of legacy bundled services and APIs, such as Memcache, in the second-generation Java runtimes. Your Java app can call the bundled services APIs through the App Engine API JAR, and access most of the same capabilities as on the Java 8 runtime.

You also have the option to use Google Cloud products that offer similar functionality as the legacy bundled services. These Google Cloud products provide idiomatic Cloud Client Libraries for Java. For the bundled services that are not available as separate products in Google Cloud, such as image processing, search, and messaging, you can use third-party providers or other workarounds.

To learn more about migrating to unbundled services, see Migrating from bundled services.

There are some differences in how you perform the runtime migration, based on whether you choose to use legacy bundled services:

Migrating to Java 11+ runtimes with bundled services Migrating to Java 11+ runtimes without bundled services
Access bundled services using the App Engine APIs JAR. Optionally, use recommended Google Cloud products or third-party services.

Use appengine-web.xml and web.xml for app configuration.

You may also need to configure additional YAML files depending on the features your app uses.

Use app.yaml for app configuration.

You may also need to configure additional YAML files depending on the features your app uses.

Apps are deployed via Jetty. Use WAR format to package your app. Apps are deployed using your own server. Use JAR format to package your app. To learn more about converting your existing WAR file to an executable JAR, see Re-packaging a WAR file.

Overview of the migration process

The following lists some changes you may have to make to your existing App Engine Java 8 app and your deployment process in order to use the second-generation Java runtimes:

Key differences between the Java 8 and Java 11+ runtimes

The following is a summary of the differences between the Java 8 and Java 11+ runtimes on the App Engine standard environment:

Java 8 runtime Java 11+ runtimes
Server deployment Server deployed for you via Jetty If your app does not use the legacy bundled services, you have to deploy a server yourself.1
App Engine legacy bundled services Provided Provided
Ability to use Cloud Client Libraries for Java Yes Yes
Language extension and system library support Yes Yes
External network access Yes Yes
Files system access Read/write access to /tmp Read/write access to /tmp
Language runtime Modified for App Engine Unmodified, open-source runtime
Isolation mechanism gVisor-based container sandbox gVisor-based container sandbox
Testing with local development server Supported Supported
Thread safety configuration Can be specified in the appengine-web.xml file. Can't be specified in the configuration files. All apps are presumed to be thread safe.3
Logging Uses a java.util.logging.
ConsoleHandler, which writes to
stderr and flushes the stream
after each record.
Standard Cloud Logging 2
DataNucleus plugin 2.x support Supported Not supported 4

Notes:

  1. If your app is not using the legacy bundled services, the second-generation Java runtimes can execute any Java framework as long as you package a web server that is configured to respond to HTTP requests on the port specified by the PORT environment variable (recommended) or on port 8080. For example, the second-generation Java runtimes can run a Spring Boot Uber JAR as is. For more examples, see the Framework flexibility section.

    If your app is using the legacy bundled services, App Engine deploys it using Jetty in the same way as in the Java 8 runtime.

  2. Logging in the second-generation Java runtimes follows the logging standard in Cloud Logging. In the second-generation Java runtimes, app logs are no longer bundled with the request logs but are separated in different records. To learn more about reading and writing logs in the second-generation Java runtimes, see the logging guide.

  3. To configure a non-threadsafe app in the second-generation Java runtime, similar to setting <threadsafe>false</threadsafe> in Java 8, set max concurrency to 1 in either your app.yaml file, or appengine-web.xml file if using the legacy bundled services.

  4. Google doesn't support the DataNucleus library in the second-generation runtimes. Newer versions of DataNucleus are backwards incompatible with the versions used in Java 8. To access Datastore, we recommend that you use Datastore mode client library or Java Objectify (version 6 or later) solution. Objectify is an open source API for Datastore that provides a higher-level of abstraction.

Memory usage differences

Second-generation runtimes see a higher baseline of memory usage compared to first-generation runtimes. This is due to multiple factors, such as different base image versions, and differences in how the two generations calculate memory usage.

Second-generation runtimes calculate instance memory usage as the sum of what an application process uses, and the number of application files dynamically cached in memory. To avoid memory-intensive applications from experiencing instance shutdowns due to exceeding memory limits, upgrade to a larger instance class with more memory.

CPU usage differences

Second-generation runtimes can see a higher baseline of CPU usage upon instance cold-start. Depending on an application's scaling configuration, this might have unintended side effects, such as, a higher instance count than anticipated if an application is configured to scale based on CPU utilization. To avoid this issue, review and test application scaling configurations to ensure the number of instances are acceptable.

Request header differences

First-generation runtimes allow request headers with underscores (e.g. X-Test-Foo_bar) to be forwarded to the application. Second-generation runtimes introduces Nginx into the host architecture. As a result of this change, second-generation runtimes are configured to automatically remove headers with underscores (_). To prevent application issues, avoid using underscores in application request headers.

Framework flexibility

The second-generation Java runtimes don't include any web-serving framework unless you are using the legacy bundled services. This means that you can use a framework other than a servlet-based framework. If you are using the legacy bundled services, the second-generation Java runtimes provide the Jetty web-serving framework.

There are hello world samples using popular Java web frameworks on the Google Cloud GitHub repository:

Migrating XML to YAML file formats

gcloud CLI does not support the following file formats:

  • cron.xml
  • datastore-index.xml
  • dispatch.xml
  • queue.xml

The following examples demonstrate how to migrate your xml files to yaml files.

Migrating your files automatically

To migrate your xml files automatically:

  1. You must have gcloud CLI version 226.0.0 or later. To update to the latest version:

    gcloud components update
    
  2. For each file you'd like to migrate, specify one of the following subcommands (cron-xml-to-yaml, datastore-indexes-xml-to-yaml, dispatch-xml-to-yaml, queue-xml-to-yaml) and file name:

    gcloud beta app migrate-config queue-xml-to-yaml MY-QUEUE-XML-FILE.xml
    
  3. Manually double-check the converted file before deploying to production.

    For a successful sample xml to yaml file conversion, see the Migrating your files manually tabs.

Migrating your files manually

To manually migrate your xml files to yaml files:

cron.yaml

Create a cron.yaml file with a cron object containing a list of objects, each with fields that correspond to each of the <cron> tag attributes in your cron.xml file, as shown below.

Converted cron.yaml file:

cron:
- url: '/recache'
  schedule: 'every 2 minutes'
  description: 'Repopulate the cache every 2 minutes'
- url: '/weeklyreport'
  schedule: 'every monday 08:30'
  target: 'version-2'
  timezone: 'America/New_York'
  description: 'Mail out a weekly report'

Original cron.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
  <cron>
    <url>/recache</url>
    <description>Repopulate the cache every 2 minutes</description>
    <schedule>every 2 minutes</schedule>
  </cron>
  <cron>
    <url>/weeklyreport</url>
    <description>Mail out a weekly report</description>
    <schedule>every monday 08:30</schedule>
    <timezone>America/New_York</timezone>
    <target>version-2</target>
  </cron>
</cronentries>

For more information, see the cron.yaml reference documentation.

dispatch.yaml

Create a dispatch.yaml file with a dispatch object containing a list of objects, each with fields that correspond to each of the <dispatch> tag attributes in your dispatch.xml file, as shown below.

Converted dispatch.yaml file:

dispatch:
- url: '*/favicon.ico'
  module: default
- url: 'simple-sample.uc.r.appspot.com/'
  module: default
- url: '*/mobile/*'
  module: mobile-frontend

Original dispatch.xml file

<?xml version="1.0" encoding="UTF-8"?>
<dispatch-entries>
  <dispatch>
      <url>*/favicon.ico</url>
      <module>default</module>
  </dispatch>
  <dispatch>
      <url>simple-sample.uc.r.appspot.com/</url>
      <module>default</module>
  </dispatch>
  <dispatch>
      <url>*/mobile/*</url>
      <module>mobile-frontend</module>
  </dispatch>
</dispatch-entries>

For more information, see the dispatch.yaml reference documentation

index.yaml

Create an index.yaml file with an indexes object containing a list of objects, each with fields that correspond to each of the <datastore-index> tag attributes in your datastore-indexes.xml file, as shown below.

Converted index.yaml file:

indexes:
- ancestor: false
  kind: Employee
  properties:
  - direction: asc
    name: lastName
  - direction: desc
    name: hireDate
- ancestor: false
  kind: Project
  properties:
  - direction: asc
    name: dueDate
  - direction: desc
    name: cost

Original datastore-index.xml file:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes
 autoGenerate="true">
   <datastore-index kind="Employee" ancestor="false">
       <property name="lastName" direction="asc" />
       <property name="hireDate" direction="desc" />
   </datastore-index>
   <datastore-index kind="Project" ancestor="false">
       <property name="dueDate" direction="asc" />
       <property name="cost" direction="desc" />
   </datastore-index>
</datastore-indexes>

For more information, see the index.yaml reference documentation.

queue.yaml

Create a queue.yaml file with a queue object containing a list of objects, each with fields that correspond to each of the <queue> tag attributes in your queue.xml file, as shown below.

Converted queue.yaml file:

queue:
- name: fooqueue
  mode: push
  rate: 1/s
  retry_parameters:
    task_retry_limit: 7
    task_age_limit: 2d
- name: barqueue
  mode: push
  rate: 1/s
  retry_parameters:
    min_backoff_seconds: 10
    max_backoff_seconds: 200
    max_doublings: 0

Original queue.xml file:

<queue-entries>
  <queue>
    <name>fooqueue</name>
    <rate>1/s</rate>
    <retry-parameters>
      <task-retry-limit>7</task-retry-limit>
      <task-age-limit>2d</task-age-limit>
    </retry-parameters>
  </queue>
  <queue>
    <name>barqueue</name>
    <rate>1/s</rate>
    <retry-parameters>
      <min-backoff-seconds>10</min-backoff-seconds>
      <max-backoff-seconds>200</max-backoff-seconds>
      <max-doublings>0</max-doublings>
    </retry-parameters>
  </queue>
<queue-entries>