Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8251554

JEP 401: Flattened Heap Layouts for Value Objects (Preview)

    XMLWordPrintable

Details

    • JEP
    • Status: Draft
    • P3
    • Resolution: Unresolved
    • None
    • None
    • None
    • Feature
    • Open
    • SE
    • valhalla dash dev at openjdk dot java dot net
    • XL
    • XL
    • 401

    Description

      Summary

      Allow value classes to express certain properties that enable flattened heap layouts for their instances. This is a preview language and VM feature.

      Goals

      This JEP introduces two optional properties of value classes that relax guarantees about explicit instance creation in exchange for flattened heap memory storage.

      Authors of value classes can declare that the class:

      • Permits implicit creation of a default instance as the initial value of a field or array (similar to the default value of a primitive type)

      • Allows racing threads to perform non-atomic updates, potentially creating new accidental instances by intermixing field values (similar to the permitted treatment of types long and double)

      JVM enhancements enable instances of classes with these properties to be flattened compactly in appropriately-typed fields and arrays.

      Non-Goals

      The storage behavior of primitive values has provided inspiration for this JEP, but primitive types remain distinct from value class types and do not interact with these features. Enhancements to the treatment of primitive types will be explored in Enhanced Primitive Boxing.

      Future enhancements to the JVM are anticipated to support inlining of value objects within generic APIs. For now, generic APIs work with erased types and heap-allocated objects, as usual.

      Existing value-based classes in the standard libraries will not be affected by this JEP. Once the features of this JEP become final, they can be applied to classes in the standard libraries as a separate task.

      Motivation

      Value classes give up their instances' object identity in exchange for better performance. Specifically, the lack of identity allows for inlined object encodings—instances directly encoded as sequences of field values, avoiding any overhead from object headers, indirections, or heap allocation. These classes provide a compromise between the abstraction power of identity classes and the memory efficiency of primitives.

      However, value classes do not fully close the performance gap between objects and primitives when their instances are stored in fields or arrays on the heap. There are two significant constraints:

      • A variable of a value class type is initially set to null, providing initialization safety: in the event the programmer forgets to initialize a variable, they get a predictable failure (NullPointerException) rather than interpreting uninitialized memory as an implicitly-created object. In order to encode null, the inlined layout of a value object typically requires some additional bits. For example, a variable storing an int can fit in 32 bits, but for a value class with a single int field, a variable of that class type could use up to 64 bits.

      • A variable of a value class type must be modified atomically in order to respect encapsulation of its state: partially-updated memory, if observable, could implicitly create a new object, intermixing field values from the originally-stored object and its replacement. But inlined object layouts are often too large for efficient atomic modification—the overhead to guarantee atomicity exceeds typical benefits of inlining.

      Primitives do not have these constraints: a primitive-typed field is implicitly initialized to a zero value (or the equivalent) on creation, rather than null; and large primitive variables, of types long or double, are allowed to be non-atomically updated (see JLS 17.7).

      Some value classes are sufficiently primitive-like that they could support similar behaviors.

      For example, just as 0 is a reasonable initial value for an int-typed field, the point (0,0) may be a reasonable initial value for a Point!-typed field. As for primitive-typed fields, JVMs could be given permission to create this zero Point object implicitly by simply allocating a field of an appropriate type.

      Similarly, some value classes are like long and double, able to interpret values implicitly created by non-atomic reads and writes. These classes could reasonably give JVMs permission to make field and array updates without guaranteeing atomicity, shifting responsibility to their users for managing concurrency and handling any bugs arising from races.

      This JEP provides value class authors with the ability to opt out of some guarantees provided by the explicit object creation process, and in exchange get better-performing field and array storage.

      Description

      The features described below are preview features, enabled with the --enable-preview compile-time and runtime flags.

      Default instances

      A concrete value class may declare that it allows implicit creation of a default instance at run time. This means that the default instance may be created without any code execution or other additional cooperation from the value class.

      To be specific, the default instance of a class is an object that has all of its instance fields set to their default values—the initial state of a fresh class instance before any instance initialization or constructor code has executed.

      A concrete value class opts in to this behavior by declaring an implicit constructor:

      value class Range {
          int start;
          int end;
      
          public implicit Range();
      
          public Range(int start, int end) {
              if (start > end) throw new IllegalArgumentException();
              this.start = start;
              this.end = end;
          }
      }

      The implicit constructor must be public.

      This is strawman syntax, subject to change.

      Value classes that declare an implicit constructor are subject to additional restrictions:

      • The class must not be an inner class with an enclosing instance.

      • The class may not declare another zero-argument constructor.

      • The default instance of the class must not contain itself. That is, no instance field of the class may have a null-restricted type (discussed below) that directly or indirectly depends on the declaring class.

      • (Possibly) To avoid confusion, no instance field of the class may have an initializer expression.

      The default keyword can be used in conjunction with the name of a value class with an implicit constructor to access the default instance of the class.

      Range zero = Range.default;
      assert range.start == 0;
      assert range.end == 0;

      (Possibly) the default instance can also be accessed by invoking the implicit constructor.

      For many value classes, the default instance would violate the class's invariants (for example, a reference-typed field of the value class might be expected to be initialized to something other than null). In that circumstance, it may not be appropriate for the class to declare an implicit constructor. This feature is designed for the subset of value classes that can comfortably operate on their default instance.

      Use of null-restricted types

      A variable with a null-restricted type prevents attempts to set the variable to null. Details of this behavior are described in the referenced JEP.

      The details of general-purpose null-restricted types are still under development. The most relevant feature for this JEP is that the type of a variable or method return may use a ! suffix to indicate that it does not store null. This is enforced with runtime checks.

      When a null-restricted field or array of a value class type is created, if the class has a default instance, the field or array is implicitly initialized to that instance.

      class Cursor {
          private Point! position;
      
          public Cursor() {
          }
      
          public Cursor(Point! position) {
              this.position = position;
          }
      
          static void test() {
              Cursor c = new Cursor();
              assert c.position == Point.default;
              c = new Cursor(null); // NullPointerException
          }
      }

      Additionally, if an array was allocated with the class's null-restricted type, it will dynamically check for nulls at run time, even when viewed through an unrestricted compile-time type.

      Object[] objs = new Point![10];
      assert objs[2] == Point.default;
      objs[2] = null; // NullPointerException

      Non-atomic updates

      A value class with an implicit constructor may also declare that it tolerates implicit creation of instances via non-atomic field and array updates. This means that, in a race condition, new class instances may be accidentally created by intermixing field values from other instances, without any code execution or other additional cooperation from the value class.

      A value class opts in to allowing this behavior by implementing the NonAtomic interface:

      value class Point implements NonAtomic {
          double x;
          double y;
      
          public implicit Point();
      
          public Point(double x, double y) {
              this.x = x;
              this.y = y;
          }
      }

      This is strawman syntax, subject to change.

      A concrete class that implements NonAtomic (directly or indirectly) must be a value class and must declare an implicit constructor.

      Users of a NonAtomic value class are responsible for maintaining the integrity of their data, and can avoid unwanted instance creation by limiting access to a single thread, enforcing a synchronization protocol, or declaring a field volatile. Otherwise, unexpected instances may be created:

      Point[] ps = new Point[]{ new Point(0.0, 1.0) }; 
      new Thread(() -> ps[0] = new Point(1.0, 0.0)).start(); 
      Point p = ps[0]; // may be (1.0, 1.0), among other possibilities 

      Some value classes with an implicit constructor have complex integrity constraints for non-zero field values (for example, the start index of a Range, declared above, must not exceed the end index). In that circumstance, it may not be appropriate for the class to declare its constructor non-atomic. This feature is designed for the subset of value classes that can comfortably operate on arbitrary combinations of field values.

      Performance model

      As described in the Value Objects JEP, the typical treatment of a standard value class is for local variables, method parameters, and expression results to use inline encodings, while fields and array components do not.

      Adding an implicit constructor to such a class improves the likelihood that fields and array components of null-restricted types can be inlined as well, using a compact flattened encoding.

      If the class has multiple instance fields, declaring the class NonAtomic may be necessary to enable inlining of these null-restricted fields and array components.

      When inlined, a null-restricted class type should have a heap storage footprint and execution time (when fully optimized) comparable to the primitive types. For example, a Point!, given the class declaration above, can be expected to directly occupy 128 bits in fields and array components, and to avoid any allocation in stack computations. A field access simply references the first or second 64 bits. There are no additional pointers.

      Notably, null-restricted uses of a value class with an implicit constructor and a single instance field can be expected to have minimal overhead compared to operating on a value of the field's type directly.

      However, JVMs are ultimately free to encode class instances however they see fit. Some classes may be considered too large to represent inline. Certain JVM components, in particular those that are less performance-tuned, may prefer to interact with instances as heap-allocated objects. An encoding might carry with it a cached heap pointer to reduce the overhead of future allocations. Etc.

      class file representation & interpretation

      A concrete value class with an implicit constructor encodes the constructor's properties via an ImplicitCreation attribute. The attribute includes bits to store ACC_DEFAULT and ACC_NON_ATOMIC flags.

      At class load time, an error occurs if an ACC_DEFAULT class has an illegal circularity in its instance field types.

      A field declaration may have an empty NullRestricted attribute to indicate that it does not store null and is implicitly initialized to the default instance of the field's type. The named class is loaded and validated during preparation (or at another point before the first access of the field), ensuring that it has ACC_DEFAULT set. Writes to the field (putfield, putstatic) will check for null, throwing a NullPointerException if found.

      Null-restricted arrays are created reflectively, as described in the "Core Reflection" section, below. The aastore instruction performs null checks, throwing a NullPointerException if the array is null-restricted and the value to write is null.

      Direct creation of null-restricted arrays may be supported by bytecode in the future.

      The class Foo must be initialized whenever another class with a NullRestricted field of type Foo is created.

      Java language compilation

      If a field's type is null-restricted and names a value class with an implicit constructor, the NullRestricted attribute is applied.

      An array creation with an appropriate element type compiles to a reflective array creation method invocation.

      A default expression compiles to either aconst_init (if possible), a reflective call, or an array allocation/read combination (details TBD).

      Core reflection

      A new kind of java.lang.Class instance represents a null-restricted runtime type. There is no class literal in the Java language for referencing one of these objects directly.

      Only value classes with implicit constructors have these special Class objects modeling null-restricted types. Null-restricted types of other classes are erased and have no Class object representation.

      The Class.getComponentType method returns a null-restricted type from a null-restricted array. Other methods of Class do something sensible when invoked on null-restricted types, with a bias towards throwing or otherwise signaling to the caller that they shouldn't be using the object in this way.

      The Object.getClass method may return an array type with a null-restricted component type.

      The Field.getType method does not return a null-restricted type; rather, it returns the type of the field as provided by the field's descriptor. A preview API method, Field.getRestrictedType, can be invoked to get the null-restricted type of a null-restricted field (and otherwise get the field's type as declared by the descriptor).

      The following preview API methods are declared in java.lang.Class:

      • isNullRestrictedType: identify special null-restricted value class types—only exist for value classes with default instances

      • asNullRestrictedType & asClassType: conversions between null-restricted and standard class types (only work on appropriate value classes)

      • hasDefaultInstance & getDefaultInstance: for value classes that have a default instance (not "default value", because this is a class operation, not a type operation—it works on Point.class)

      The Array.newInstance methods support null-restricted types encoded as Class objects. To create a null-restricted array, an appropriate Class can be passed to one of these methods:

      Point[] ps1 = new Point![10];
      // or
      var type = Point.class.asNullRestrictedType();
      Point[] ps2 = (Point[]) Array.newInstance(type, 10);

      (Possibly) the result of Class.getDeclaredConstructors, etc., includes the zero-arg constructor represented by an implicit constructor, and invoking that Constructor returns a default instance.

      Other API & tool support

      javax.lang.model supports implicit constructors.

      The javadoc tool includes the implicit constructor in class documentation.

      java.lang.constant and java.lang.invoke may need special support for null-restricted fields and arrays.

      HotSpot implementation

      This section describes implementation details of this release of the HotSpot virtual machine, for the information of OpenJDK engineers. These details are subject to change in future releases and should not be assumed by users of HotSpot or other JVMs.

      In HotSpot, values of fields and arrays with null-restricted types are encoded as follows:

      • Value classes with field layouts exceeding a size threshold, that do not declare an implicit constructor, or that require atomic updates are always encoded as regular heap objects. Fields marked volatile always store regular heap objects.

        In this case, null-restricted fields initially store null, but this value is detected and lazily replaced by the class's default instance whenever a read operation occurs. In contrast, null-restricted arrays are eagerly filled with a default instance pointer at array creation time.

      • Otherwise, value objects are encoded in null-restricted fields and arrays as a flattened sequence of field values. Array components may be padded to achieve good alignment.

        In this case, the initial default instance encoding is achieved by setting all bits to 0 when the field or array is allocated. Reads and writes must adapt between the use site encoding and the field encoding, at times copying from heap storage on writes and allocating new heap storage on reads. Array accesses may need to dynamically check a flag to determine whether the underlying array is flattened.

      Alternatives

      Making use of primitive types, rather than declaring value classes, will often produce a program with equivalent or slightly better performance. However, this approach gives up the valuable abstractions provided by classes. It's easy to, say, interpret a double with the wrong units, pass an out-of-range int to a library method, or fail to keep two boolean flags together in the right order.

      Value classes provide useful performance benefits without needing implicit constructors and null-restricted storage. And with additional innovation in JVM implementation techniques and hardware capabilities, the performance costs of null encodings and atomic updates may shrink further. However, the limitations outlined in the "Motivation" section are pretty fundamental. For example, a value class type wrapping a single long field and supporting the full range of long values for that field can never be encoded in fewer than 65 bits. This JEP gives programmers who need fine-grained control a more reliable performance model for heap storage.

      We considered many different approaches to the object model and type system before settling on a model in which compact flattened heap storage is simply a JVM optimization for a null-restricted reference type. This strategy avoids the conceptual overhead that comes from generalizing the existing model for primitive types. Developers already understand objects and classes, and null-restricted types are a simple language enhancement that is useful as a general-purpose feature.

      Risks and Assumptions

      There are security risks involved in allowing instance creation outside of constructors, via default instances and non-atomic reads and writes. Developers will need to understand the implications, and recognize when it would be unsafe to declare an implicit constructor or implement the NonAtomic interface.

      Dependencies

      This JEP depends on Value Objects (Preview), which establishes the semantics of identity-free objects and applies many JVM optimizations.

      This JEP also depends on Null-Restricted and Nullable Types (Preview), which introduces null-restricted types and defines their runtime behavior.

      Building on this JEP, JEP 402: Enhanced Primitive Boxing (Preview) refactors the primitive wrapper classes as value classes with implicit constructors.

      In the future, JVM class and method specialization (JEP 218, with revisions) will allow generic classes and methods to specialize field and array layouts when parameterized by null-restricted value class types.

      Attachments

        Issue Links

          Activity

            People

              dlsmith Dan Smith
              dlsmith Dan Smith
              Dan Smith Dan Smith
              Brian Goetz
              Votes:
              0 Vote for this issue
              Watchers:
              23 Start watching this issue

              Dates

                Created:
                Updated: