SUMMARY

SRE and IT Operations play a critical role in ensuring reliable, high-performance applications. Yet, SREs (Site Reliability Engineers) often face ‘thrown-over-the-wall’ code deployments to operate without having insights into the code-level features.

In my previous article (“Is your Java Observability tool Lambda Expressions aware?”), I delved into one such code-level feature: Java lambda expressions which replace anonymous inner classes. The latter can negatively impact your Java application’s performance.

The choice of lambda vs anonymous classes is not just some intricate code-level design feature. It has a direct bearing on application performance and can impact Service Level Indicators (SLIs) negatively, which is what SREs are jointly responsible for.

How do SREs know that there are better code-level alternatives? SREs need an X-Ray into code so they can swim upstream and influence code-level design decisions. APM and observability tools can provide this visibility.

My article struck a chord with many SREs, prompting numerous questions seeking more details on Java lambda expressions.

A few questions that I’ll answer today, after a brief recap:

  • What is a Java Lambda Expression and what are their benefits?
  • Will you improve performance if you use Lambda Expressions (rather than anonymous classes)?
  • Why are some APM tools blind to Java Lambda Expressions? (hint: they are hard to instrument for tracing and monitoring)
  • Takeaway for SREs: Swim upstream, shift-left and influence AppDev

What is a Java lambda expression?

Java lambda expressions provide a concise way to encapsulate business logic. You can represent an anonymous block of code that can either be an expression or a series of statements, ultimately returning a result. The structure of a lambda expression is as follows:

(argument list) -> { body }

The syntax includes an argument list, an arrow token (->), and a body. I’ll walk you through some code examples in the next section which will make this clear.

This construct enables the creation of anonymous functions or methods without explicitly declared names. You can succinctly define behavior that can be passed as arguments to methods, similar to the concept of anonymous classes. Lambda expressions facilitate the expression of code in a compact and expressive manner, enhancing the readability and flexibility of Java code.

The term “lambda” is derived from lambda calculus which symbolizes function abstraction.Lambda expressions were introduced in Java 8 in the year 2014. They allow for a more functional programming style enabling streamlined and expressive code.

Java lambda illustrated with examples

Let’s look at some sample code to get more familiar with lambda expressions.

Task: Calculate Interest for a Loan

InterestCalculator loanInterestCalculator = (principal, rate, time) -> principal * rate * time / 100;

Task: Apply Discount to an Online Order

OrderDiscountProcessor discountProcessor = (totalAmount) -> totalAmount – (totalAmount * 0.1);

What you are seeing in the above examples is the following:

  • They are anonymous: They don’t have an explicit name like a method would normally have. That’s good – less code to write and debug.
  • They are functions: A lambda isn’t associated with a particular class (unlike a method is). But like a method, a lambda has a list of parameters, a body, a return type, and a possible list of exceptions that can be thrown.
  • Passed around—A lambda expression can be passed as argument to a method or stored in a variable.
  • Concise—You don’t need to write a lot of boilerplate like you do for Java anonymous classes.

You might come across the expression “passing code”. What does that mean?

When we talk about “passing code,” we are typically referring to the ability to provide a block of executable code (a function or behavior) as an argument to another piece of code. This is often used in the context of passing behavior to a method or function.

Why should you care about lambda expressions?

Before Java lambda expressions were introduced, passing code using anonymous classes was tedious and verbose. Lambdas fix this problem – you can pass code in a concise way.

Lambda expressions promote functional programming design patterns. You no longer need to write clumsy code using anonymous classes to benefit from behavior parameterization.

An alternative to anonymous classes

Let’s first talk about anonymous classes.

In Java, you can create classes that don’t have a name. These are just like local classes (classes defined in a block) that you may already be familiar with. However, the key difference is that anonymous classes don’t have a name.

Anonymous classes enable you to declare and instantiate a class simultaneously, providing a way to create ad hoc implementations on the fly. This approach simplifies the code by combining class declaration and instantiation in a single step, making it convenient for scenarios where a temporary, unnamed class is needed.

The Oracle documentation makes it very clear:

Anonymous Classes, shows you how to implement a base class without giving it a name. Although this is often more concise than a named class, for classes with only one method, even an anonymous class seems a bit excessive and cumbersome. Lambda expressions let you express instances of single-method classes more compactly.

Later in this article, we’ll go through the specific benefits of Java lambda expressions. For now, here’s a list of things to keep in mind on “why Java lambdas”:

  • Java lambdas are a replacement for anonymous classes. You don’t need to explicitly declare a method, as seen in traditional anonymous classes.
  • They are shortcuts for writing short, simple methods. Java lambdas are great at making your code shorter and easier to understand.
  • They work especially well with a programming style called functional programming, where you can do some cool things like passing functions as if they were regular data.
  • Java lambdas seamlessly integrate with Java streams, enabling concise and expressive data processing. Stream pipelines can operate sequentially or in parallel. Lambdas allow for efficient parallel processing of collections, harnessing the full potential of multicore CPUs. This combination enhances performance by distributing computations, making Java development more scalable and responsive.

Anonymous classes also have a bunch of performance disadvantages

Anonymous classes also come with undesirable characteristics that can impact your application’s performance.

  • Anonymous classes can prolong JVM start-up times due to the additional bytecode generation and class loading complexities they introduce.
  • Increased JAR file size bloat due to generation of many class files. This hinders deployment efficiency and increases JAR download times.
  • Anonymous classes can waste precious metaspace in the JVM, impacting memory usage and potentially leading to performance issues.
  • Anonymous classes are also code cache inefficient, as their dynamic nature can result in less effective utilization of the code cache, negatively affecting runtime performance. Anonymous classes impact JVM start-up times

Lambda expressions solve many of the above performance benefits, which we’ll see below.

Benefits of Lambda Expressions illustrated with code examples

Benefit #1: Concise and better clarity => more developer productivity

Let’s understand with an example. You are writing an e-commerce application that needs to calculate the total price of products exceeding $200. You can do it the old way or the modern way.

Verbose traditional code using Anonymous classes Concise and modern equivalent using Java lambda expressions
double totalPrice = 0.0;

for (Product product : products) {

if (new PriceChecker() {

@Override

public boolean test(Product product) {

return product.getPrice() > 200.0;

}

}.test(product)) {

totalPrice += new PriceExtractor() {

@Override

public double apply(Product product) {

return product.getPrice();

}

}.apply(product);

}

}

// Helper interfaces (anonymous classes)

interface PriceChecker {

boolean test(Product product);

}

interface PriceExtractor {

double apply(Product product);

}
double totalPrice = 0.0;

for (Product product : products) {

if (product.getPrice() > 200.0) {

totalPrice += product.getPrice();

}

}

See the brevity and conciseness in the side-by-side comparison above? Lambda expressions reduce boilerplate code, resulting in concise (and readable syntax). Developers can achieve the same functionality with fewer lines of code. The conciseness is achieved because Lambda expressions provide an expressive syntax that allows developers to focus on the core logic without unnecessary ceremony.

Benefit #2: Better readability =>  better collaboration across AppDev and SRE (and everyone else)

Lambda expressions, when used judiciously, enhance the structural clarity of code. Developers can easily discern the logic encapsulated within lambda expressions. This helps in aiding bug identification and resolution in the future.

As an example, the old-style code in the above example has more lines for conditional checks. As the business logic becomes more complex, this increased complexity may make the code harder to understand. This is true especially for developers who are new to the codebase or reviewing the code after some time.

Lambda expressions that are clear and comprehensible help in avoiding technical debt. Code that is easy to understand is less prone to introducing complexities, reducing the likelihood of accumulating technical debt in projects.

At the same time, developers should prioritize readability over brevity. If a Java lambda expression becomes too complex, it may hinder understanding rather than enhancing it.

Benefit #3: Encourages functional programming => flexible and modular code

The lambda expressions example shown gets the job done with less boilerplate code. Pass a lambda expression to filter products based on the condition product.getPrice() > 200.0, replacing the traditional for loop and explicit conditional statements.

Although I have used a contrived example, real-world business logic could be far more complex, involving intricate conditions and multiple steps, where the conciseness of lambda expressions becomes even more advantageous.SREs and application developers need to be on the lookout for utilizing concise constructs, such as lambda expressions in Java 8. Positively influence your developer communities to express complex business operations in a more compact and understandable manner.

Benefit #4: Improved start-up performance => faster user experience

Anonymous classes impact start-up times of JVMs. When you use anonymous classes, the compiler generates a new class file for each one. These files typically have names like ClassName$1 , where ClassName is the name of the class containing the anonymous class, followed by a dollar sign and a number.

Here’s why anonymous classes impact start-up times:

  • Each class file must be loaded and verified before it can be used.
  • Loading involves operations like disk I/O and decompressing the JAR file, which can be costly
  • This process can be resource-intensive, impacting the startup performance of the application.

As a result, the more anonymous inner classes you have, the longer it may take for your application to start up, potentially leading to a less responsive user experience. Java lambda expressions result in lesser number of classes and therefore aid in faster JVM start up times.

Benefit #5: Lean JAR file sizes => faster download times, cost savings

The Java compiler generates multiple classes during compilation time. These classes contribute to JAR file bloat, leading to larger JAR file sizes. A JAR (Java Archive) file is a compressed file format that bundles Java class files, metadata, and resources into a single package. It allows developers to conveniently distribute and deploy Java applications or libraries, simplifying the management and sharing of Java code.

Bloated JAR files have several consequences:

  • Quicker download times: Smaller JAR files download faster over networks. This is crucial especially in scenarios with limited bandwidth.
  • Better deployment cycles: Compact JAR files are more efficient for deployment. They consume less disk space, facilitating quicker distribution and deployment processes, which is crucial in large-scale software distribution.
  • Consumes less storage space: Especially important for resource-constrained devices.
  • Cost Savings in bandwidth: Reduced JAR file sizes result in lower bandwidth consumption. This is true for cloud-based deployments where data transfer costs are a consideration.

In Function as a Service (FaaS) environments such as AWS Lambda (not to be confused with Java lambda), lean JAR files are essential. Compact JARs facilitate faster deployment, reduce startup times, and improve the overall efficiency of serverless functions.

This is crucial in FaaS, where cold-starts are a concern. Quick execution and resource optimization are key for responsive and cost-effective serverless applications.

Benefit #6: Memory efficient (metaspace) => optimized memory

What is metaspace?

Metaspace is a memory area in the JVM that serves as a storage location for metadata related to class definitions. Metaspace is responsible for storing information such as class names, method signatures, and constant pool data, essential for the runtime execution of Java programs. Metaspace replaced the Permanent Generation (PermGen) in Java 8.

Unlike PermGen, metaspace can dynamically adjust its size, avoiding common issues like OutOfMemoryErrors. Nevertheless, the instantiation of multiple anonymous classes can still contribute to increased metaspace usage.

As SRE, you need the ability to see historical trends of metaspace utilization to detect anomalies.
The above is an example from eG Enterprise JVM observability suite.

The bottomline is that Lambda expressions take up less room in metaspace as compared to anonymous classes.

The benefit of using lambda expressions also extends to heap memory. You get quicker and more efficient GC (garbage collection) and lesser GC pause times.

This is because a smaller number of objects are created or instantiated in memory as compared to anonymous classes. Therefore, less garbage to reclaim from heap memory.

Benefit #7: Code cache efficient => stable applications

What is code cache and what bad things happen when it becomes full?

Code cache stores compiled native code generated by the Just-In-Time (JIT) compiler, enhancing execution speed. By writing efficient and concise code, developers can maximize code cache utilization, leading to quicker execution and improved overall application responsiveness.

Bad things happen when the code cache becomes full in the Java Virtual Machine (JVM):

  • Compiler Disabling: The JVM stops compiling additional bytecode into native code, leading to a significant performance degradation.
  • Warning Messages: You’ll start to see warning messages such as “CodeCache is full… The compiler has been disabled.”
  • Application Performance Degradation: The application’s performance degrades as it relies solely on interpreted bytecode, which is typically slower than native code.
  • Application Crashes: In extreme cases, if the code cache-related issues are not addressed, the application might eventually crash due to the inability to allocate memory for compiling and storing native code.

Since the code inside each anonymous class is compiled to machine code by the JVM, it would be stored inside a code cache. More code = more code cache bloat.

Visualizing historical trends in code cache utilization is vital for anomaly detection.
The above screenshot is also from eG Enterprise JVM observability suite.

Again, lambda expressions are simply more code cache efficient when compared to anonymous classes.

Why are Java lambda expressions hard to instrument for tracing and monitoring?

APM and observability tools instrument Java applications by hooking into the JVM at the time of bytecode class loading. For observability instrumentation engineers, understanding the class loading  process is key.

Reason #1: Needs advanced instrumentation techniques

When it comes to “normal” Java classes vs Java lambda expressions, there’s a big difference in the class loading process.

In normal class loading, the bytecode is loaded via the “transform” method. However, for Java lambda expressions, the Java bytecode is loaded via the “bootstrap” method. This adds complexity to the instrumentation process.

The APM instrumentation engineer often must figure out how to use specialized techniques to instrument the bootstrap process.

The Lambda bootstrap method uses the InnerClassLambdaMetafactory to generate an inner class for the lambda at runtime. Observability tools must intercept this method and capture the method argument “classBytes” & modify it using advanced techniques such as ASM to check whether that bytecode matches with configured functional interface names.

Reason #2: Java lambda expressions bytecode vary from one JVM vendor to another

Each version of the JVM employs a different implementation of the bootstrap method. This also adds to the complexity of instrumenting the Java application.

Here are some examples of the additional complexity:

Java version 9 to 14

jdk.internal.misc.Unsafe. defineAnonymousClass(Class, byte[] , Object)

Java version 15 to 20

java.lang.invoke.MethodHandles$Lookup.defineHiddenClass(byte[], boolean, ClassOption…);

On Java version 21

java.lang.invoke.MethodHandles$Lookup.makeHiddenClassDefiner(String, byte[], Set, ClassFileDumper);

Beware blind spots in your APM and observability tools

What happens when your APM tool lacks Java lambda instrumentation and visibility, but your Java application indeed uses lambda expressions?

You could end up with incomplete traces.  If a Lambda makes external calls like JMS or third-party calls, missing instrumentation leads to gaps in distributed traces. This results in blindness to slowdowns in dependencies, creating black boxes that hinder root-cause analysis.

You might also see un-instrumented time blocks in code-level call graphs that indicate parts not covered by your observability tool.

Blind spots in your observability tooling can cripple your ability to troubleshoot slowness and errors.

As an SRE and IT Ops, select APM and observability tools with care

As an SRE, opt for tools that employ sophisticated instrumentation tailored for Java lambda expressions. One such example is eG Enterprise which gives you full-stack visibility into Java applications that use lambda expressions inside the code.

Unlike legacy monitoring tools that often necessitate code changes for debugging or troubleshooting data, modern tools, such as eG Enterprise, are Java Lambda Expression-aware. In my recent article on monitoring Java Lambda Expressions, I highlighted the challenge: many SRE and IT Ops tools lack diagnostic capabilities for this powerful code construct.

All this means that legacy monitoring tools often require changes to code or the way code is built to be made to obtain debug or troubleshooting data. And of course, for the SRE or IT Ops team this isn’t SREs are often simply handed third-party apps or apps developed by a siloed AppDev teams. Changing application code in order to gain observability insight is not feasible in many cases. Automatic observability tools such as eG Enterprise solve this problem out of the box.

It’s essential for SRE and IT Ops teams to prioritize tools that understand and support modern code constructs to simplify troubleshooting in contemporary applications. For further details and insights, visit Is your Java Observability tool Lambda Expressions aware?.

Ignoring this consideration could lead to significant challenges in effectively monitoring and troubleshooting modern applications, particularly those leveraging Java Lambda Expressions.

What’s the takeaway for SREs and IT Ops teams?

I’ve posted some of this information on LinkedIn and discussed how SREs are often too abstracted away from language features which have an impact on performance. By the time SREs get to know about it, it’s usually too late in the game. One SRE commented:

“How do you go about discussing this with the dev team, considering that SREs are not directly responsible for the application design and code development? Isn’t this more of a task for performance engineers”

As an SRE, having the tools to swim upstream (or shift-left) and influence AppDev teams to use performance efficient language features would be utopia. With eG Enterprise you will at least have the tools to troubleshoot efficiently and prove when the responsibility is in the AppDev team’s court.

However, it will also give a few SREs (perhaps those with previous coding experience or in less siloed organizations) the tools to open a collaborative conversation, perhaps along the lines:

“Hey folks, we saw that our Permgen and codecache is seeing some inefficiencies. In looking through the traces in observability, we are thinking we could do better with using the new Java language features like lambda. We can help pitch in with some before-vs-after load test benchmarks if you folks are game!”

The key takeaway for Site Reliability Engineers (SREs) and IT Ops teams is the critical importance of being attuned to language features, such as Java Lambdas, that can significantly impact performance.

Often, SREs may be abstracted from these nuances until it’s too late. Engaging with the development team becomes crucial, even if SREs aren’t directly responsible for application design and code development.

This collaboration ensures that performance considerations are addressed early in the development process. While performance engineers may lead specific efforts, fostering communication between SREs and developers allows for a proactive approach to identifying and resolving potential issues related to Java Lambdas. Understanding and discussing these language features can lead to more efficient, reliable, and performant applications in the long run.

For this collaboration to happen, SREs and IT Ops must choose APM and observability vendors that are modern Java ready.

eG Enterprise is an Observability solution for Modern IT. Monitor digital workspaces,
web applications, SaaS services, cloud and containers from a single pane of glass.

Learn More

About the Author

Arun is Head of Products, Container & Cloud Performance Monitoring at eG Innovations. Over a 20+ year career, Arun has worked in roles including development, architecture and ops across multiple verticals such as banking, e-commerce and telco. An early adopter of APM products since the mid 2000s, his focus has predominantly been on performance tuning and monitoring of large-scale distributed applications.