Details
-
JEP
-
Status: Draft
-
P3
-
Resolution: Unresolved
-
None
-
None
-
None
-
Feature
-
Open
-
SE
-
-
XL
-
XL
-
401
Description
Summary
Support null-restricted storage of value objects in fields and
array components. These variables are initialized to an initial instance of
the class and reject attempts to write a null
value; they can be optimized
with compact, flattened object encodings.
This is a preview language and VM feature.
Goals
This JEP introduces the language, JVM, and reflection API features necessary to allow value classes to express support for null-restricted storage, and to allow individual fields and arrays to opt in to null-restricted behaviors.
The HotSpot implementation then optimizes these fields and arrays by storing
value objects directly in flattened storage, without any object headers,
indirections, or null
flags.
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 encodenull
, the inlined layout of a value object typically requires some additional bits. For example, a variable storing anint
can fit in 32 bits, but for a value class with a singleint
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 an
appropriately-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 when users of the class make use of an appropriate type.
Description
The features described below are preview features, enabled with the
--enable-preview
compile-time and runtime flags.
Initial instances
A concrete value class may declare that it allows implicit creation of an initial instance at run time. This means that the initial instance may be created without any code execution or other additional cooperation from the value class.
To be specific, the initial 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.
An implicit constructor, if declared, takes the place of the default no-argument constructor, and when explicitly invoked can be thought of as having a body that simply sets all instance fields to their default values. It's not possible to separately declare an explicit no-argument constructor.
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 initial 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, and the class may not declare instance initializers.
The default
keyword can be used in conjunction with the name of an
implicitly-constructible value class to access the initial instance of the
class.
Range zero = Range.default;
assert range.start == 0;
assert range.end == 0;
The initial instance can also be accessed by invoking the implicit constructor.
(Note that these expressions have different binary compatibility behavior: a
Foo.default
expression asserts that the class Foo
has an initial instance,
and if not will cause a LinkageError
; meanwhile, a new Foo()
expression
simply invokes a constructor with no arguments, ignoring any implementation
details.)
For many value classes, the initial instance would violate the class's
invariants (for example, a String
-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 initial instance.
Null-restricted types
A null-restricted type is expressed with the name of a value class followed by
the !
symbol. The named value class must have an implicit constructor.
A variable with a null-restricted type prevents attempts to set the variable to
null
. References can be freely converted to and from null-restricted types,
but a conversion to a null-restricted type requires a check for null
, and
throws a NullPointerException
if it is found (compare the behavior of unboxing
conversion).
When a null-restricted field or array is created, it is implicitly initialized to the given class's initial 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
LooselyConsistentValue
interface:
value class Point implements LooselyConsistentValue {
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 LooselyConsistentValue
(directly or
indirectly) must be a value class and must declare an implicit constructor.
Users of a LooselyConsistentValue
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(0.0, 1.0) };
new Thread(() -> ps[0] = new Point(2.0, 3.0)).start();
Point p = ps[0]; // may be (2.0, 1.0), among other possibilities
Some implicitly-constructible value classes 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 implement the LooselyConsistentValue
interface.
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, implementing LooselyConsistentValue
may be necessary to enable flattening of these null-restricted fields and array
components.
When flattened, 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
An implicitly-constructible concrete value class 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.
The constant pool is able to express type restrictions, which represent
dynamic checks to be enforced on variables, in addition to their declared type.
In particular, a type restriction may assert that a value is a non-null instance
of an ACC_DEFAULT
value class. (Details TBD.)
Arrays may be created, via anewarray
, with a type restriction rather than a
CONSTANT_Class
. This restriction enhances the array store check performed by
the array (via aastore
). A null restriction indicates that the array is to be
initialized to the given class's initial instance.
A field declaration may similarly include an attribute that points to a type
restriction. The type restriction is dynamically checked on writes to the field
(putfield
, putstatic
). Just like arrays, a null restriction on a field
indicates that the field is to be initialized to the given class's initial
instance.
A type restriction applied to a field is resolved and validated during
preparation (or at another point before the first access of the field), ensuring
that, e.g., the class named by a null restriction has ACC_DEFAULT
set.
If the value of a field is implicitly set to a value class's initial instance,
the named value class must be initialized before the field can be read.
Like anewarray
, the checkcast
and instanceof
instructions are enhanced to
support type restrictions.
Java language compilation
If a field is declared or an array is created with a null-restricted type, a type restriction is applied to that field or array in the class file.
Other null-restricted types (such as the type of a local variable) are enforced,
as necessary, with checkcast
instructions.
A default
expression compiles to either aconst_init
(if possible), a
reflective call, or an array allocation/read combination (details TBD).
Core reflection
Null-restricted types are not represented as instances of java.lang.Class
,
and cannot appear in class literals. The java.lang.reflect.Type
API does
encode null-restricted types.
A separate TypeRestriction
API represents any type restrictions that may
appear in class
files (designed in anticipation of additional kinds of
restrictions in the future).
Arrays support a new reflective query to get the type restriction enforced by
the array (perhaps a method of java.lang.reflect.Array
?). The
Array.newInstance
method is overloaded to support array creation with a type
restriction.
java.lang.reflect.Field
also supports reflecting the type restriction, if any,
applied to the field.
java.lang.Class
is enhanced with hasInitialInstance
and getInitialInstance
preview API methods.
The result of Class.getDeclaredConstructors
, etc., includes the zero-arg
constructor represented by an implicit constructor, and invoking that
Constructor
returns an initial instance.
Other API & tool support
javax.lang.model
supports implicit constructors.
The javadoc
tool advertises the presence of an implicit constructor in class
documentation.
java.lang.constant
is enhanced to express type restrictions.
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 initial instance whenever a read operation occurs. In contrast, null-restricted arrays are eagerly filled with an initial 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 instance encoding is typically 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 initial 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 LooselyConsistentValue
interface.
Dependencies
This JEP depends on Value Objects (Preview), which establishes the semantics of identity-free objects and applies many JVM optimizations.
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.
More general support for nullness features will be explored in a future JEP.
Attachments
Issue Links
- relates to
-
CODETOOLS-7903012 Add support for Primitive Objects(JEP-401) to asmtools
-
- In Progress
-
-
JDK-8269096 Add java.util.Objects.newIdentity method
-
- Resolved
-
-
JDK-8260606 Update Valhalla core-libs naming for methods related to primitive classes
-
- Resolved
-
-
JDK-8277163 Value Objects (Preview)
-
- Draft
-