Java 23 and GraalVM for JDK 23 Released - JVM Weekly vol. 100
Today, we have a double occasion! But first, the duties (after all, we have the new JDK 23), and then the pleasures (my little celebration).
1. JDK 23 Released!
Let’s start with the official post from Oracle… and the live stream.
I also have something from myself. Frank Delporte from Foojay.io invited me to his podcast, where, together with Simon Ritter, Deputy CTO from Azul, I had the chance to talk about the new features in JDK 23. So, if you want to check out our discussion, feel free to watch the video below 😃
And now, it's time for the JEPs
Stable
467: Markdown Documentation Comments
JEP 467 introduces the ability to write JavaDoc documentation comments using Markdown, in addition to the existing HTML format and JavaDoc tags.
What is the problem?: Current JavaDoc documentation comments use HTML and specific JavaDoc tags, which are difficult to write and read. HTML tends to "clutter" the content, discouraging developers from creating well-formatted documentation. Additionally, JavaDoc documentation tags are less known and often require consultation with JavaDoc’s own documentation (which, when you think about it, is somewhat meta).
Solution: JEP 467 introduces support for Markdown in documentation comments, enabling more concise and readable documentation formatting. Markdown is currently a standard among markup languages used by developers, popularized by platforms like GitHub, so the syntax is widely used and known (fun fact - all editions of JVM Weekly are originally written in Markdown). This allows developers to write documentation faster and with less effort, while still being able to use HTML and JavaDoc tags where necessary (many popular Markdown parsers are compatible with HTML inserts).
Code Example:
Traditional JavaDoc comment:
/**
* Returns a <b>hash code</b> value for the object.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Must consistently return the same integer for the same object,
* provided no information used in {@code equals} comparisons is modified.
* <li>If two objects are equal according to the {@link #equals(Object)} method,
* calling {@code hashCode} on each must produce the same result.
* <li>It is not required that unequal objects produce distinct integers,
* but this improves the performance of hash tables.
* </ul>
*
* @implSpec
* The {@code hashCode} method defined by class {@code Object} returns distinct
* integers for distinct objects as far as is practical.
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
The same Markdown comment:
/// Returns a **hash code** value for the object.
///
/// The general contract of `hashCode` is:
///
/// - Must consistently return the same integer for the same object,
/// provided no information used in `equals` comparisons is modified.
/// - If two objects are equal according to the [equals][#equals(Object)] method,
/// calling `hashCode` on each must produce the same result.
/// - It is not required that unequal objects produce distinct integers,
/// but this improves the performance of hash tables.
///
/// @implSpec
/// The `hashCode` method defined by class `Object` returns distinct
/// integers for distinct objects as far as is practical.
///
/// @return a hash code value for this object.
/// @see java.lang.Object#equals(java.lang.Object)
/// @see java.lang.System#identityHashCode
471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
JEP 471 aims to deprecate memory access methods in the sun.misc.Unsafe
class with the intent of removing them in future JDK releases. This proposal encourages developers to migrate to supported alternatives, allowing applications to smoothly transition to newer JDK versions.
What is the problem?: Memory access methods from sun.misc.Unsafe
are dangerous and can lead to undefined behavior, including JVM crashes. Although not intended for wide use, they have become popular among developers seeking higher performance and power than what standard APIs offer. Lack of safety checks before using them can lead to bugs and application crashes.
Solution: JEP 471 deprecates memory access methods in sun.misc.Unsafe
, encouraging developers to transition to safe and efficient alternatives: VarHandle
(JEP 193) and MemorySegment
(JEP 454). The proposal anticipates gradually removing access to sun.misc.Unsafe
methods in several phases, starting with compilation warnings, runtime warnings, and eventually the actual removal of the methods. New command-line options, such as --sun-misc-unsafe-memory-access={allow|warn|debug|deny}
, have been added to enable developers to test and evaluate the impact of deprecating memory access methods.
Code Example:
Example code using sun.misc.Unsafe
:
class Foo {
private static final Unsafe UNSAFE = ...;
private static final long X_OFFSET;
static {
try {
X_OFFSET = UNSAFE.objectFieldOffset(Foo.class.getDeclaredField("x"));
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryToDoubleAtomically() {
int oldValue = x;
return UNSAFE.compareAndSwapInt(this, X_OFFSET, oldValue, oldValue * 2);
}
}
Example code using VarHandle
:
class Foo {
private static final VarHandle X_VH;
static {
try {
X_VH = MethodHandles.lookup().findVarHandle(Foo.class, "x", int.class);
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryAtomicallyDoubleX() {
int oldValue = x;
return X_VH.compareAndSet(this, oldValue, oldValue * 2);
}
}
474: ZGC: Generational Mode by Default
JEP 474 changes the default operating mode of the Z Garbage Collector (ZGC) to generational mode. The current non-generational mode will be marked as deprecated and planned for removal in future JDK releases.
What is the problem?: Maintaining both generational and non-generational modes of ZGC slows down the development of new features. The generational mode of ZGC is considered a better solution for most use cases, rendering the non-generational mode redundant.
How does the proposal solve this problem?: The proposal changes the default value of the ZGenerational
option to true
, thus enabling the generational mode of ZGC by default. After these changes, running the JVM with the -XX:+UseZGC
option will default to the generational mode of ZGC. The non-generational mode will be marked as deprecated, with plans for its eventual removal in the future.
Preview
455: Primitive Types in Patterns, instanceof, and switch (Preview)
JEP 455 introduces pattern matching for primitive types in instanceof
and switch
, allowing the use of these types in nested contexts and at the top-level.
What is the problem?: Current limitations on primitive types in instanceof
and switch
make it difficult to write uniform and expressive code based on them. For example, switch
does not support primitive type patterns, and instanceof
does not support safe casting to primitive types, leading to inconsistent and potentially complicated code.
Solution: JEP 455 enables pattern matching of primitive types in instanceof
and switch
, allowing for safe and expressive casting. This eliminates the need for manual, potentially unsafe casts and allows for more concise code.
Code Example:
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i;
}
if (i instanceof byte b) {
... // use b as byte
}
466: Class-File API (Second Preview)
JEP 466 proposes a standard API for parsing, generating, and transforming Java class files. This is the second preview of the API, which has been improved based on experiences and feedback from the first preview introduced in JEP 457.
What is the problem?: Currently, there are many libraries for handling Java class files, each with its pros and cons. The more rapid evolution of the class file format (due to e.g. ongoing evolution of Valhalla) means these libraries are not always up-to-date with the latest JDK versions, leading to compatibility issues and making it difficult to introduce new features in the JDK.
Solution: JEP 466 introduces a standard API for handling class files that evolves with the class file format. This API offers immutability for class file elements, a tree structure mirroring the class file hierarchy, and user-driven navigation for efficient parsing. It features lazy parsing for better performance, integrates stream and materialized views, and supports transformations through flat mapping operations on streams of elements. By hiding complex implementation details like the constant pool and stack maps, it simplifies interactions and enhances developer productivity.
The Class-File API has a broad scope and must generate classes according to the Java Virtual Machine specification, requiring significant quality and compatibility testing. As usage of ASM in the JDK is replaced by the Class-File API, results will be compared to detect regressions, and extensive performance tests will be conducted.
What changes have been made since the last preview?
Simplified the
CodeBuilder
class by removing medium-level methods that duplicated low-level methods or were rarely used.Enhanced the
ClassSignature
class to more accurately model generic signatures of superclasses and superinterfaces.
Alternatives: An alternative considered was incorporating ASM into the JDK and taking responsibility for its further maintenance, but this was deemed inappropriate. ASM is an old, legacy codebase difficult to evolve, and the design priorities guiding its architecture may not fit today's realities.
Code Example
Parsing class files using patterns:
CodeModel code = ...
Set<ClassDesc> deps = new HashSet<>();
for (CodeElement e : code) {
switch (e) {
case FieldInstruction f -> deps.add(f.owner());
case InvokeInstruction i -> deps.add(i.owner());
// ... and so on for instanceof, cast, etc ...
}
}
Generating class files using builders:
ClassBuilder classBuilder = ...;
classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,
methodBuilder -> methodBuilder.withCode(codeBuilder -> {
Label label1 = codeBuilder.newLabel();
Label label2 = codeBuilder.newLabel();
codeBuilder.iload(1)
.ifeq(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int))
.goto_(label2)
.labelBinding(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))
.labelBinding(label2)
.return_();
});
Transforming class files:
ClassFile cf = ClassFile.of();
ClassModel classModel = cf.parse(bytes);
byte[] newBytes = cf.transform(classModel, (classBuilder, ce) -> {
if (ce instanceof MethodModel mm) {
classBuilder.transformMethod(mm, (methodBuilder, me)-> {
if (me instanceof CodeModel cm) {
methodBuilder.transformCode(cm, (codeBuilder, e) -> {
switch (e) {
case InvokeInstruction i
when i.owner().asInternalName().equals("Foo") ->
codeBuilder.invoke(i.opcode(), ClassDesc.of("Bar"),
i.name().stringValue(),
i.typeSymbol(), i.isInterface());
default -> codeBuilder.with(e);
}
});
} else {
methodBuilder.with(me);
}
});
} else {
classBuilder.with(ce);
}
});
473: Stream Gatherers (Second Preview)
JEP 473 proposes enhancing the Stream API to support custom intermediate operations, allowing for more flexible stream processing.
What is the problem?: The current Stream API offers a fixed set of intermediate operations that do not always enable the execution of more complex tasks. The inability to define custom intermediate operations limits the flexibility and expressiveness of the code, especially for tasks requiring specific data transformation logic.
Solution: JEP 473 introduces a new intermediate operation Stream::gather(Gatherer)
, which allows stream elements to be processed using user-defined gatherers. A gatherer is an instance of the java.util.stream.Gatherer
interface that represents the transformation of stream elements and enables manipulation of streams of infinite size.
What changes have been made since the last preview?: No changes were introduced compared to the previous preview (JEP 461). The goal is to gather additional feedback and experience using this API.
Code Example:
Grouping elements into fixed windows:
var result = Stream.iterate(0, i -> i + 1)
.gather(Gatherers.windowFixed(3))
.limit(2)
.toList();
// result ==> [[0, 1, 2], [3, 4, 5]]
Defining a custom gatherer:
record WindowFixed<TR>(int windowSize)
implements Gatherer<TR, ArrayList<TR>, List<TR>> {
public WindowFixed {
if (windowSize < 1) {
throw new IllegalArgumentException("window size must be positive");
}
}
@Override
public Supplier<ArrayList<TR>> initializer() {
return () -> new ArrayList<>(windowSize);
}
@Override
public Integrator<ArrayList<TR>, TR, List<TR>> integrator() {
return Gatherer.Integrator.ofGreedy((window, element, downstream) -> {
window.add(element);
if (window.size() < windowSize) {
return true;
}
var result = new ArrayList<TR>(window);
window.clear();
return downstream.push(result);
});
}
@Override
public BiConsumer<ArrayList<TR>, Downstream<? super List<TR>>> finisher() {
return (window, downstream) -> {
if (!downstream.isRejecting() && !window.isEmpty()) {
downstream.push(new ArrayList<TR>(window));
window.clear();
}
};
}
}
476: Module Import Declarations (Preview)
JEP 476 introduces the ability to import all packages exported by a module using a new import module
declaration. This facilitates the reuse of modular libraries without requiring developers to modularize their own code.
What is the problem?: Currently, when using modules, developers must manually import many packages, which can be time-consuming and complicated, especially for beginners. The inability to simply import entire modules leads to redundancy and complexity in the code.
How does the proposal solve this problem?: JEP 476 introduces a new import module
declaration, which allows importing all public classes and interfaces from packages exported by a module and modules it transitively requires. This allows developers to more easily and quickly access needed resources, simplifying code and reducing the number of import declarations.
However, there are some drawbacks - using multiple import module
declarations may lead to name conflicts that will be detected only at compile time. Resolving these conflicts may require adding individual type import declarations, which can be cumbersome and difficult to maintain.
Code Example:
Importing an entire module:
import module java.base;
String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
Stream.of(fruits)
.collect(Collectors.toMap(
s -> s.toUpperCase().substring(0,1),
Function.identity()));
Resolving name conflicts:
import module java.base;
import module java.sql;
import java.sql.Date;
Date d = ... // Ok! Date jest rozpoznane jako java.sql.Date
477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
JEP 477 aims to simplify writing first programs in Java by allowing developers to create programs without the need to understand advanced language constructs designed for large programs. This feature introduces the ability to declare implicit classes and instance main methods, allowing for more concise writing of small programs.
What is the problem?: Currently, novice programmers must understand many complex language constructs, such as classes, packages, modules, access, and static modifiers, before they can write a simple program. This can discourage new users and make learning difficult.
Solution: JEP 477 simplifies creating programs by introducing:
Instance main methods: Main methods do not need to be static, public, or take a
String[]
parameter.Implicit classes: Allows writing methods and fields at the top level, automatically included in a hidden class.
Automatic import of I/O methods: Methods for simple text input/output are automatically imported.
Automatic import of the java.base module: All public classes and interfaces of packages exported by the java.base module are automatically imported.
Code Example:
Simple "Hello, World!" program in an implicit class:
void main() {
println("Hello, World!");
}
Interactive program:
void main() {
String name = readln("Please enter your name: ");
print("Pleased to meet you, ");
println(name);
}
480: Structured Concurrency (Third Preview)
JEP 480 introduces an API for structured concurrency, simplifying concurrent programming by treating groups of related tasks as a single "unit of work". Structured concurrency improves error handling and simplifies task cancellation while providing observability of concurrent code.
What is the problem?: The current approach to concurrency, based on ExecutorService and Future, allows for concurrency patterns that are not bound by any logical scope, leading to problems with thread lifecycle management, error handling, and cancellation propagation. This makes the code harder to understand, debug, and maintain.
How does the proposal solve this problem?: JEP 480 introduces an API for structured concurrency, providing hierarchical relationships between tasks and their subtasks, similar to the call stack in standard code. StructuredTaskScope
, the main class of the API, allows developers to group related tasks, manage them as a single unit, and automatically propagate cancellation and handle errors. It also improves observability - tools of this kind can display the task hierarchy, making it easier to diagnose problems.
Code Example:
Using StructuredTaskScope in the handle method:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Dołącz do obu podzadań
.throwIfFailed(); // Propaguj błędy
// Here both subtasks have completed successfully, so compose their results
return new Response(user.get(), order.get());
}
}
481: Scoped Values (Third Preview)
JEP 481 introduces scoped values, which allow a method to share immutable data both with calls within a thread and with child threads.
What is the problem?: Thread-local variables (ThreadLocal) are difficult to manage and suffer from mutability, unbounded lifetime, and costly inheritance. These problems become even more apparent when using a large number of virtual threads.
How does the proposal solve this problem?: JEP 481 introduces scoped values as a safe and efficient way to share data between methods within the same thread and with child threads. They are immutable, and their limited lifetime makes them available only for a specific time during thread execution, simplifying management and improving performance. They can also be inherited by child threads created by StructuredTaskScope
.
Scoped Values are easy to use, offering a simple way to pass data between methods, and they provide transparency, as the shared data's lifetime is clearly visible in the code structure. They are easier to understand than thread-local variables and have lower memory and time costs, especially when used together with virtual threads (JEP 444) and structured concurrency (JEP 480).
Example of using scoped values in a web framework:
class Framework {
private final static ScopedValue<FrameworkContext> CONTEXT
= ScopedValue.newInstance();
void serve(Request request, Response response) {
var context = createContext(request);
ScopedValue.runWhere(CONTEXT, context,
() -> Application.handle(request, response));
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get();
var db = getDBConnection(context);
db.readKey(key);
}
}
In this example, CONTEXT
is a scoped value set in the serve
method and available in the readKey
method.
482: Flexible Constructor Bodies (Second Preview)
JEP 482 introduces the ability to place statements in constructors before calling another constructor (super(..)
or this(..)
) in the Java language. These statements cannot refer to the created instance but can initialize its fields.
What is the problem?: In the Java language, a constructor must begin with a call to another constructor (super(..)
or this(..)
). This limitation prevents placing initialization code before calling the superclass constructor, which can lead to issues with overriding methods and initializing fields.
Solution: JEP 482 introduces changes to the constructor grammar, allowing for statements to be placed before calling the constructor. This code can initialize fields but cannot refer to the created instance. This makes the class more reliable when methods are overridden.
Example of argument verification in the constructor before calling the superclass constructor:
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(long value) {
if (value <= 0) throw new IllegalArgumentException(..);
super(value);
}
}
In this example, the value
argument is verified before calling the superclass constructor BigInteger
.
Changes since the previous Preview: In the second preview (the first one was called JEP 447: Statements before super(...) ) a significant change has been introduced that allows constructors to initialize fields before invoking another constructor (super(..) or this(..)). This new capability ensures that a constructor in a subclass can initialize its fields before a superclass constructor executes, preventing the superclass constructor from encountering default field values (such as 0, false, or null) in the subclass.
This improvement addresses a common issue where the superclass constructor calls methods that are overridden in the subclass and use uninitialized fields, leading to potential bugs and errors.
Incubation
469: Vector API (Eighth Incubator)
JEP 469 extends the API that enables expressing vector computations, compiled to native vector instructions on supported CPU architectures, offering performance superior to equivalent scalar computations.
What issue do they address?: The concept of vector computations, which allows operations on multiple data simultaneously, was difficult to express in Java. As a result, the environment relied on the HotSpot auto-vectorization algorithm, limiting its practical usefulness and performance.
Solution: The Vector API in Java allows for creating complex vector algorithms with better predictability and reliability. It leverages the existing HotSpot auto-vectorizer, offering users a more predictable model.
Changes since the last incubator: The API was reintroduced for incubation in JDK 22, with minor improvements from JDK 21, such as bug fixes and performance enhancements. The completion of Project Valhalla is expected before introducing the Preview version.
2. No String Templates in JDK 23: Back to the Drawing Board
And since we've already talked about what's new in the upcoming JDK, it's time to remind ourselves of what won't be included.
Since the deprecation of the Security Manager, there hasn't been a more controversial JEP than the various iterations of String Templates. Each proposal for the new API has faced significant pushback. Notably, the first two Preview versions failed to bring the community to a consensus, and the third Preview, initially announced as the final version, faced substantial criticism from the community. As a result, discussions on the Amber project mailing list since March culminated in an unprecedented decision: the withdrawal of the third Preview and a complete reevaluation of the functionality. Consequently, String Templates will be removed from JDK 23, necessitating a refactor of code in projects that dared to use the test version.
For a refresher on the proposed syntax, here's an example from the JEP:
Thanks for reading JVM Weekly! Subscribe for free to receive new posts and support my work.
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
If you want more details, Nicolai Parlog discussed the issues faced by the specification creators in detail in the new Inside Java Newscast episode, What Happened to Java's String Templates
The design of string templates aimed to facilitate not just string concatenation but also the safe embedding of variables in structured languages like SQL, HTML, and JSON. However, the use of \{...}
instead of the more familiar $
symbol and the special syntax for invoking processors received criticism. Additionally, the need for string processors (STR.
in the example) for performance was eventually deemed unnecessary, upending the original concept. The mailing list discussions indicated that the extensive rework required could not be completed within the remaining 12 weeks before the JDK 23 release, leading to the decision to give it more time.
Before contesting this decision, remember that String Templates never exited the test phase. The Preview pathway, introduced in JEP 12, aims to gather community feedback and ensure features are fully refined before final release, allowing developers to experiment with new language features, APIs, or tools in real-world conditions. This iterative process helps ensure that features are robust and meet the needs of real projects when they become part of the JDK.
However, features in the Preview version may not make it to the final Java release. If a feature receives negative feedback, encounters serious technical issues, or fails to meet community expectations, it can be changed, delayed, or completely removed. Although this has rarely happened before, it seems it's time to revise those expectations.
The first withdrawal of a JEP from the Preview pathway certainly adds flavor to the testing process for new functionalities. It will be interesting to observe how this move affects the perception of such features. Many people, including myself, experimented with String Templates, not expecting such a radical turn of events. Although there was always a possibility that something like this could happen, the two previous previews suggested that the creators were confident. It seemed that incubation was the phase where functionalities might never be implemented. But it's better now than Java living forever with a feature that doesn't meet community standards, giving us a chance for a better-accepted variant in the future.
3. GraalVM for JDK 23 released as well!
Since we've almost wrapped up the topic of JDK 23, it's time for GraalVM for JDK 23. There are plenty of interesting things here as well.
Here you can find official release stream:
As stated in announcement post - as always written by GraalVM Developer Advocate’s Alina Yurenko - in the new version of GraalVM for JDK 23, besides support for new JEPs from JDK 23, one of the main features is the introduction of a mark & compact GC for the old generation of the Serial GC, aiming to reduce peak memory usage compared to the copying GC, which can use up to 2x the current heap size when all objects survive. It utilizes object headers to store new locations of contiguous surviving object sequences rather than adding new fields, thus minimizing overall memory overhead. The implementation relies on chunk card tables to temporarily store indices for new locations, allowing precise updates of object references. While performance compared to the copying GC is variable, it provides room for optimization in both memory and CPU usage.
A lot of significant improvements were made, especially regarding Native Image. Similar to other compilers, GraalVM users can control the optimization level using the -O
option. As stated in the documentation, by default, -O2
is used, which aims for a good trade-off between performance, file size, and build time. GraalVM for JDK 23 introduces a new optimization level -Os
, which allows executable files to be reduced by up to 35%, a great advantage for building smaller, more efficient applications (at the cost of reduced performance during compilation).
Additionally, GraalVM also introduces a new feature for generating a Software Bill of Materials (SBOM) directly in the native image or as a resource on the classpath, making it easier to analyze supply chain security. SBOM is a structured record of all the components, libraries, modules, and dependencies contained in a given software, allowing for better risk management and the identification of potential security vulnerabilities. This helps organizations monitor all the components used in their applications, which is particularly important given the increasing number of cyberattacks targeting software supply chains. You can enable this with the flag --enable-sbom=classpath
, allowing the use of standard tools like Spring Actuator to inspect project dependencies.
But I've saved the most exciting surprise for last. In Oracle JDK 23 (and, as far as I understand... only in it?), the Oracle GraalVM JIT compiler (Graal JIT) was introduced as one of the JIT options available to JDK users. Thanks to this, all Oracle JDK users can take advantage of code optimization techniques that were previously only available in Oracle GraalVM. The integration of Graal JIT offers new possibilities that can help developers and administrators optimize application performance. Although Graal JIT in Oracle JDK is treated as an experimental feature, it is fully supported commercially by Oracle and Oracle Cloud Infrastructure. This change is quite... surprising—to say the least, it's like a second attempt at the subject. Graal JIT was already added and removed from JDK. Moreover, it appears that this is not related to the Galahad project, whose goal is to integrate JIT with OpenJDK. I must admit, I’m eagerly waiting for more context, and I suspect we'll revisit this topic.
And most importantly... the new project mascot. I really liked the previous bunny, but I've already fallen in love with its successor. I’m looking forward to the name and hope that by Devoxx in Belgium, there will be some stickers 😃.
Bonus: JVM Weekly Celebrates a Major Milestone with issue 100!!
Now for a bit of personal news 😉
I've been with you for over two years now (trivia: I was inspired to start by
and his , which I highly recommend), and I'm incredibly happy I began this little project. It’s very satisfying to see how everything (sometimes faster, sometimes slower, but steadily upward!) is growing.What I’m most proud of—thanks to JVM Weekly, I've met so many wonderful people from the community who would have probably been unreachable to me otherwise, and I’ve learned fantastic things from them—I can't list everyone here, but special thanks go to
and the team at Foojay.io! Additionally, for anyone who hasn’t tried—believe me, nothing teaches regularity like a weekly newsletter. Suddenly, it turns out that you can write a paragraph of text during any ten-minute break, and the best ideas for memes, jokes, phrases, and paragraphs come in the bathtub or when you're about to fall asleep.I don't plan on announcing any major changes for our (we’re having a flood in Poland, so I don't feel entirely comfortable celebrating) anniversary, except for some minor cleanups. From today, I’ve finally linked the domain jvm-weekly.com to Substack (after 100 editions, it was about time, right?) (as of publication, it may still be propagating). Additionally, in place of the green, impersonal symbol, I wanted to show the world a new logo with my beloved Duke in red glasses, which you can now see in several publication spaces 😃.
I know Duke doesn’t look the most modern in the world, but I’ve always dreamed of it being part of the blog.
PS: Maybe one day there’ll be a variant with the new GraalVM mascot—I love it!
One thing I still haven’t managed to achieve is a return to Polish editions. These were published for a long time (even before the English editions), but I still haven't found a way to incorporate them into the current workflow in a way that they’re not "dead on arrival." But I promise, I'll keep thinking about it and will definitely return to the subject (or maybe Substack will finally release support for bilingual publications).
If you know anyone who would benefit from becoming a subscriber—feel free to share/reshare! I know that in 2024, friends don't share newsletters with friends (we all have way too much information), but maybe someone will be interested 😉
I hope that in less than two years, we'll be celebrating edition #200. I still love this project and am incredibly glad I decided to start it.
Seeing all these pictures in your articles, you must be quite the narcissist.