In object-oriented programming languages, such as Java and C#, static attributes and methods establish class-level members independent of particular object instances and linked to the class as a whole. This article offers a basic overview of this concept, covering the associated runtime memory allocation and impact on program execution.
Introduction
Objects are essentially instances of classes that come to life at runtime, thanks to the new
operator. Each object carries its own set of instance variables, representing the object's unique attributes or state. These instance variables are exclusive to each object, and any changes made to one object's attributes will not affect the attributes of other objects belonging to the same class.
On the other hand, the concept of static, manifested through the static
keyword, is used within a program to qualify either attributes or methods related to the class itself rather than being tied to individual object instances. As a result, static
members exist independent of whether objects of that class have been instantiated.
In a previous article, we explored the details of memory management within a runtime environment, e.g., the JVM or the .NET CLR, specifically focusing on how the stack and heap handle different data types, such as value types and reference types, during method execution.
To gain a deeper understanding of why using static
members when writing programs, it is worth exploring additional aspects of memory management related to the storage of program code and the information, i.e., metadata, that the runtime requires to create instances of classes.
Memory Management: Class Metadata, Method code and Static Attributes
Java and C# programs are compiled into an intermediate binary representation known as bytecode (or CIL code) and stored in specific .class
(or assembly) files. This bytecode includes a binary version of the program’s logic with associated classes and methods.
As we know, object-oriented programs are based on objects of various classes that interact via calling methods. A previous post explained that during program execution, data for value types and reference types variables are allocated either on the stack or heap memory. However, that article did not discuss where classes’ metadata and method code reside in memory once loaded from the .class
or assembly files.
Current implementations of runtime environments like the JVM and the .NET CLR store each class definition needed for a program in specific data structures. These structures contain common class metadata that the runtime uses to create objects of that class.
From a logical perspective, these data structures are similar, but various runtime environments implement and store them in memory in a slightly different way. In the latest versions of the JVM, class metadata and method bytecode are stored in a Method Area within Metaspace. In the .NET CLR, each class definition is stored in a Method Table on the Loader Heaps, with references (pointers) to the locations of the CIL code within the assemblies. The rest of the section explores such differences in more detail.
Java Virtual Machine (JVM)
When a Java program is compiled, it is converted into bytecode and stored in .class
files. This bytecode represents the program’s logic and instructions in binary form, including the code definitions of all the classes and methods.
At runtime, the JVM loads the program classes’ bytecode from the .class
files. The figure below shows that each class metadata and methods’ bytecode are placed into a dedicated Method Area structure. Unlike other data associated with the program, such as value types and reference types data, the Method Area is not stored in the stack or heap memory. Instead, after Java version 8, it is created in a dedicated native memory location called Metaspace:
Before Java 8, class Method Area structures were stored in a fixed-size heap memory called the Permanent Generation (PermGen). This often led to OutOfMemoryError issues when the PermGen space was exhausted, especially in applications that dynamically loaded many classes. On the other hand, Metaspace uses native memory, which is allocated outside the JVM heap. This allows Metaspace to grow dynamically based on the application’s needs, significantly reducing the risk of running out of memory for class metadata.
.NET Common Language Runtime (CLR)
In the .NET ecosystem, programs are compiled into Common Intermediate Language (CIL) code and stored in assembly files. This CIL code represents the program's logic and instructions, including the code definitions of all the classes and methods.
As the figure below shows, when the CLR executes a program, different memory heaps are allocated in addition to the Managed Heap discussed in the context of data storage. These are known as the Loader Heaps. Unlike the Managed Heap, the Loader Heaps are not touched by garbage collection and serve as a repository for information that remains constant throughout the entire lifecycle of a program.
Among the Loader Heaps, the HighFrequencyHeap stores a set of the above information accessed frequently during a program execution, such as a MethodTable for each class defined within that program.
Storage of Static Attributes in Memory
As mentioned at the beginning of this article, static
attributes are shared among all instances of a class and exist regardless of whether objects of that class have been instantiated. Therefore, they find their place within those data structures connected to the class rather than specific object instances, such as the Method Area or Method Table. Their storage location within these structures outside the garbage collected heap emphasises the class-level nature of static
attributes and their lasting presence throughout a program's execution. The definition of static
attributes in Java and their memory allocation in the Metaspace is described in the following section.
Defining and Using Static Attributes
Let's illustrate the memory allocation process of static
attributes with the example shown in the following Java code, which we used also in previous posts:
class BankAccount
{
private String number;
private double balance;
private static int accountsCreated = 0;
public BankAccount(string num, double bal)
{
number = num;
balance = bal;
accountsCreated++;
}
public int getAccountsCreated()
{
return accountsCreated;
}
// all other BankAccount methods: Deposit, Withdraw, etc.
}
public class Program
{
public static void main(String[] args)
{
BankAccount account1, account2;
account1 = new BankAccount("A0123", 500.5);
account2 = new BankAccount("BD324", 100.0);
System.out.println(account1.getAccountsCreated()); // 2
System.out.println(account2.getAccountsCreated()); // 2
}
}
When the above program is started and the first instruction of the main
is executed, the Method Area of the BankAccount
class is created inside the Metaspace (or in the HighFrequencyHeap of the Loader Heaps for a C# .NET program). As mentioned, the Method Area includes metadata required to create object instances of that class. It also includes the bytecode for the class’ methods and the corresponding JIT-compiled machine code generated as those methods are invoked.
It can be noticed that the above BankAccount
class defines a static
attribute accountsCreated
in addition to the usual instance variables number
and balance
.
Therefore, as the figure below shows, space for this accountsCreated
class member will also be allocated in the Method Area in Metaspace, even before the creation on the heap of the two objects referenced by account1
and account2
.
Inside the constructor of the class, the content of the static
attribute accountsCreated
is incremented every time a new BankAccount
is allocated on the heap. Because accountsCreated
is a static
attribute that is shared among all the BankAccount
object instances, all these object instances will reference the same memory location of the Method Area where this attribute is stored. As a result, accountsCreated
can be used as a counter that keeps track of the number of BankAccount
objects created since the beginning of the program execution.
As a result, when at the end of the main
method, the method getAccountsCreated
is called on each of the object instances referenced by account1
and account2
, the return
value of those invocations will be identical. Again, this is due to the content of the static
accountsCreated
attribute being shared among those instances and not specific to each.
Defining and Using Static Methods
Like static
attributes, static
methods are behaviours inherently linked to the class where they are defined, which can be invoked without requiring an object instance of that class. static
methods do not operate on instance variables (attributes) of an object because they are not bound to a specific object instance and do not have access to an object's state.
As a result, they can only access static
attributes and methods of the same class or static
members exposed by other classes. They can, of course, operate on value types and reference types received via their parameters and on their locally defined variables.
Utility Methods
The first common usage of static
methods is to implement behaviours that do not depend on the state of individual objects but rather perform operations on parameters or other static
members. An example is shown in the following code snippet:
public class CalcManager
{
public static bool isEven(int n)
{
return n % 2 == 0;
}
public static int cube(int n)
{
return n * n * n;
}
public static double add(double[] x)
{
double sum = 0.0;
for (double e : x)
sum += e;
return sum;
}
}
class Program
{
public static void Main()
{
int number = 3;
double[] values = { 0.4, 3.5, 7.8, 0.5 };
System.out.println(CalcManager.isEven(number));
System.out.println(CalcManager.add(values));
}
}
The CalcManager
class does not include any declaration of attributes. Therefore, objects created from this class would not have any state associated. The class also defines three methods—isEven
, cube
, and add
—that perform operations solely based on their parameters and return
values with the calculated results. Because these methods do not access instance variables (attributes), they can be declared as static
. Such methods are often referred to as utility methods because they provide utility functions that are not tied to the object state.
When a class defines static
methods, these methods can be invoked by other classes directly by referring to the class name followed by .
and by the method name. This is shown in the above code snippet for the Program
class when the static methods isEven
and add
are called.
static
methods can also be declared with the static
keyword. This indicates that no object instances of that class can ever be created. Since Java does not have a similar mechanism, a class can be made non-instantiable by making it final
and defining a private
default constructor.Methods that Access Other Static Members
Another common use of static
methods is to implement behaviours operating on other static
attributes and methods of the same class or static
members exposed by other classes. The method GetAccountsCreated
in the code snippet of the previous section on static attributes could be an example of this scenario.
As discussed earlier, when that method is invoked in the main
on separate object instances, the result of those invocations is identical. This suggests that the behaviour is related to the class BankAccount
rather than specific object instances. Therefore, the definition of the getAccountsCreated
method can become static
, as shown in the following code snippet:
class BankAccount
{
private String number;
private double balance;
private static int accountsCreated = 0;
public BankAccount(string num, double bal)
{
number = num;
balance = bal;
accountsCreated++;
}
public static int getAccountsCreated()
{
return accountsCreated;
}
// all other BankAccount methods: Deposit, Withdraw, etc.
}
class Program
{
public static void main()
{
BankAccount.getAccountsCreated(); // 0
BankAccount account1, account2;
account1 = new BankAccount("A0123", 500.5);
account2 = new BankAccount("BD324", 100.0);
BankAccount.getAccountsCreated(); // 2
}
}
Because the method is now associated with the class and not a specific object, it can be invoked via the class name .
the method identifier, i.e., BankAccount.getAccountsCreated()
, even when no BankAccount
objects have been allocated on the heap at the beginning of the main
.
It should be noted that because the method is now attached to a class and not to a particular object instance, it will not have access to instance fields. No specific object reference is passed implicitly to the method when it is invoked; therefore, the this
keyword cannot be used inside the method's body to access instance variables and methods.
On the other hand, if the static
getAccountsCreated
method had reference type variables passed via its parameters or declared and instantiated within its body, attributes of objects referenced through those variables would be accessible.
Conclusions
Static is a concept that applies to both attributes and methods. In object-oriented languages like Java and C# .NET, static attributes and static methods are associated with a class rather than specific object instances. Static attributes are shared among all instances of the class and exist independently of whether objects of that class are instantiated.
Static methods are behaviours attached to a class and can be called without referencing an object instance. They do not operate on instance variables but can use other static members of the class or those exposed by other classes. Static methods are commonly used for utility functions that perform operations without relying on object state.