Aviator Quick Overview
Li Wei
Aviator Quick Overview
Basic Introduction
AviatorEvaluator is a high‑performance expression evaluation engine in the Java ecosystem (open‑source project, GitHub: google/aviator). Its core function is to compile dynamic expressions written as strings (such as mathematical formulas or business rules) into executable code and compute the result quickly at runtime.
Main Uses
Aviator is mainly used in scenarios that require dynamic calculation. Common applications include:
- Rule engine: dynamically parse business rules (e.g., risk‑control rules, promotion rules). Example:
"age > 18 && score >= 600"(determine whether a user meets loan eligibility). - Dynamic configuration: turn configuration items from hard‑coded values into dynamic expressions (e.g., traffic thresholds, permission control). Example:
"requestCount > maxThreshold * 1.5"(check if request volume exceeds the limit). - Mathematical/logic computation: supports complex math operations (trigonometric functions, exponentials, bitwise ops) and logical judgments. Example:
"sin(pi/4) + log10(100)"(calculate sine value + logarithm). - Custom script extensions: provide lightweight scripting capability for applications (as an alternative to heavier solutions like Groovy or JavaScript).
Implementation Principle
Aviator’s core workflow can be divided into four steps: lexical analysis → syntactic analysis → code generation → execution, with code generation being the key to its high performance.
1. Lexical Analysis
The input expression string is split into meaningful tokens (variables, operators, functions, constants).
Example: the expression "a + b * (c - 10)" is tokenized into [a, +, b, *, (, c, -, 10, )].
Aviator defines token patterns with regular expressions (for variable names, numbers, operators, etc.) and matches them one by one using a scanner.
2. Syntactic Analysis
From the token sequence, an abstract syntax tree (AST) is built, or the expression is transformed into a reverse‑Polish notation (postfix expression).
- AST: a tree‑structured representation of the expression’s syntax (e.g., root node
+, left childa, right child*). - Reverse‑Polish notation: converts an infix expression (e.g.,
a + b * c) into postfix form (e.g.,a b c * +) for direct stack‑based evaluation.
Aviator uses a recursive‑descent parser, supporting operator precedence (e.g.,*higher than+) and parentheses.
3. Code Generation
The AST or RPN is turned into Java bytecode (.class files), which is the core of Aviator’s speed.
- Tool: uses ASM (a Java bytecode manipulation framework) to generate bytecode dynamically, instead of relying on reflection or interpretation.
- Optimizations: the generated code performs constant folding (e.g.,
2 + 3directly computed as5), variable caching (reducing lookup overhead), etc.
Example: the expression"a + b * 2"results in a class that extendsAviatorCompiledExpression, whoseevaluatemethod contains the bytecode for the calculation logic.
4. Execution
- Class loading: the generated bytecode class is loaded via a custom class loader (e.g.,
AviatorClassLoader). - Instantiation: an instance of that class is created (usually a singleton because the expression logic is fixed).
- Computation: the instance’s
evaluatemethod is invoked with variable bindings (e.g.,Map) to obtain the result.
Why Does Aviator “Keep Creating Class Loaders”?
This is a side effect of Aviator’s code‑generation strategy. The main reasons are:
1. Need for Dynamic Bytecode Generation
To achieve high‑performance computation, Aviator compiles each expression into Java bytecode (instead of interpreting it). Every distinct expression generates a unique Java class (the class name includes a random suffix such as Expr_123456) to guarantee uniqueness and avoid name clashes.
2. Use of Custom Class Loaders
Aviator loads the generated bytecode classes with a custom class loader (e.g., AviatorClassLoader). According to Java’s class‑loading mechanism:
- The same fully‑qualified class loaded by different class loaders is treated as different classes (parent‑delegation model).
- To isolate classes generated for different expressions, Aviator typically creates a new class‑loader instance for each expression (or uses separate loaders).
3. Class Loaders Are Not Reclaimed Promptly
A Java class can be unloaded only when three conditions are met:
- All instances of the class have been garbage‑collected.
- The class’s
Classobject has no references. - The class loader that loaded the class (
ClassLoader) has no references.
Classes generated by Aviator are usually not explicitly unloaded (expressions may be reused), and the custom class loader holds references to the generated classes, while those classes hold references back to the loader (e.g., via static fields). This creates a circular reference, preventing the loader from being garbage‑collected, leading to continuous accumulation and eventually a memory leak (PermGen/Metaspace overflow).
4. Limits of the Caching Mechanism
Aviator offers a compilation cache (enabled via AviatorEvaluator.compile(expression, true)) that can store compiled results for identical expressions, avoiding repeated class and loader creation. However:
- If expressions change dynamically (each time a new string), the cache cannot help, and class loaders are still created frequently.
- Even with caching enabled, different expressions still generate distinct classes and loaders.
How to Mitigate Frequent Class‑Loader Creation
- Enable compilation cache: for repeatedly used expressions, cache the compiled result with
compile(expression, true)to avoid generating new classes. - Reuse class loaders: design a custom loader that can load multiple generated classes instead of one loader per expression, reducing the total number of loaders.
- Limit expression complexity: avoid overly complex or deeply nested expressions, which reduces the size and count of generated classes.
- Upgrade the JVM: JDK 8+ replaces PermGen with Metaspace, alleviating permanent‑generation overflow (though it does not solve class‑loader leaks).
Summary
AviatorEvaluator is a high‑performance expression evaluation engine for Java that achieves speed through dynamic bytecode generation. Its “continuous creation of class loaders” stems from the dynamic class‑generation strategy and the use of custom class loaders, which prevents timely garbage collection. The key to solving the issue is caching compiled results and optimizing class‑loader management.
References
- Aviator official documentation: Aviator Documentation
- GitHub repository: google/aviator
- ASM framework: ASM Documentation
Originally written by Li Wei (李唯_) and published in Chinese on 后端技术栈全书 (Full-Stack Backend Engineering). Translated and adapted for DriftSeas with permission.