(1)成员变量:
(2)局部变量
public class StaticObjTest {
static class Test{
// 静态变量
static ObjectHolder staticObj = new ObjectHolder();
// 实例变量
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
// 局部变量
ObjectHolder localObj = new ObjectHolder()();
System.out.println("done");
}
}
private static class ObjectHolder{
}
public static void main(String[] args) {
Test test = new StaticObjTest.Test();
test.foo();
}
}
版本 | 演进细节 |
---|---|
JDK6及之前 | 方法区的实现为永久代,静态变量存放在永久代中,字符串常量池(StringTable)位于运行时常量池中。 |
JDK7 | 方法区的实现为永久代,但已经逐步“去永久代”,静态变量、字符串常量池移除,保存在堆中 |
JDK8 | 方法区的实现为本地内存的元空间,字符串常量池、静态变量仍在堆中 |
在 HotSpot JVM(Java Virtual Machine)中,方法区(Method Area)是用于存储类的元数据信息、常量池、字段、方法字节码等数据的区域。在早期版本的 HotSpot JVM 中,方法区是永久代(Permanent Generation)的一部分,用于存储类的元数据信息,包括类的结构、常量池、字段、方法等。
然而,随着 Java 8 版本的发布,HotSpot JVM 对内存管理做了重大调整,永久代被彻底移除,并由元空间(MetaSpace)取而代之。元空间是方法区的替代方案,用于存储类的元数据信息和符号引用,并且不再有永久代的大小限制,而是使用本地内存来管理。
元空间的特点:
总的来说,HotSpot JVM 在 Java 8 版本中通过引入元空间替代永久代,从而改善了内存管理的问题,提高了 Java 程序的稳定性和性能。这也是为什么在 Java 8 及之后的版本中不再需要调整永久代大小,并且可以通过配置参数来调整元数据区的大小。
JDK7中将字符串常量池放到了堆空间中:因为永久代的回收效率很低,在Full GC时才会触发,而Full GC在老年代的空间不足、永久代不足时才会触发,这就导致字符串常量池回收效率不高,而回收效率低会导致永久代内存不足。将字符串常量池放到堆里,能及时回收内存。
在 Java 8 版本之后,由于永久代被移除,静态变量通常存储在元空间,而不是传统的永久代。调整静态变量的位置可以避免永久代可能引发的内存泄漏和溢出问题,并提供更好的内存管理和优化。
以下是一些使用元空间替换永久代的原因:
避免内存泄漏和溢出
永久代在某些情况下可能会引发内存泄漏或内存溢出问题。由于永久代的大小是有限制的,当类的元数据信息过多时,可能导致永久代空间不足,从而引发内存溢出问题。元空间使用本地内存进行管理,可以避免这些问题,并根据应用的需要动态调整空间大小。
灵活的内存管理
元空间的大小可以根据应用的需要进行动态调整,不再受永久代大小的限制。这样可以更好地适应不同应用的内存需求,提高了内存管理的灵活性。
减少内存碎片
永久代使用的是堆内存,而元空间使用本地内存,减少了在堆上分配内存和释放内存的开销,从而减少了内存碎片。
提高性能
元空间的内存管理机制相对于永久代更加高效,提高了类加载和卸载的性能。元空间使用本地内存来管理类的元数据信息,减少了垃圾回收对类元数据的影响,提高了类加载和卸载的效率。
对永久代进行调优是很困难的
(1)解析
在JDK 1.8中,永久代被移除,取而代之的是元空间。元空间使用本地内存进行管理,并且其大小不再受永久代大小的限制。因此,JDK 1.8中的元空间不会产生传统意义上的永久代内存溢出。
虽然元空间不会产生永久代内存溢出,但仍然可能产生内存溢出。例如,如果加载大量的类和资源,或者进行大量的对象创建,都可能导致本地内存耗尽,从而引发本地内存溢出(OutOfMemoryError: Metaspace)问题。
解决办法:增加 Metaspace 的大小
-XX:MaxMetaspaceSize=512m
(2)代码演示
模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间是会超过Metaspace指定的空间大小的
java -XX:+PrintFlagsInitial
在IDEA中新建SpringBoot项目
测试代码:
public class MetaspaceDemo {
static class OOM{}
public static void main(String[] args) {
int i = 0;//模拟计数多少次以后发生异常
try {
while (true){
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("=================多少次后发生异常:"+i);
e.printStackTrace();
}
}
}
执行后查看内存会发现一直在增加,停止运行后设置元空间(Metaspace)的初始大小和最大大小。参数如下:
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
运行结果:
cglib默认开启了UserCache生成的代理类都会复用原先产生在缓存中的类,所以至始至终都只有一个代理类,所以不会产生内存溢出。手动关闭它,enhancer.setUseCache(false)
(1)方法区(Metaspace):用于存储类的元数据信息、常量池、字段、方法等。JVM 8中,取代了永久代(Permanent Generation),称为元空间。元空间使用本地内存进行管理,并且大小不再受永久代大小的限制。
(2)堆(Heap):用于存储对象实例。堆是Java程序运行时所需内存的主要部分。在JVM启动时通过-Xmx和-Xms参数来设置堆的最大大小和初始大小。
(3)Java栈(Java Stack):用于存储线程的方法调用和局部变量。每个线程都有自己的Java栈,用于存储方法调用的栈帧,以及线程局部变量和操作数栈。Java栈的大小可以通过-Xss参数设置。
(4)本地方法栈(Native Stack):用于存储JNI(Java Native Interface)方法的调用和本地方法的栈帧。
(5)程序计数器(Program Counter Register):记录了线程正在执行的字节码指令地址。
(6)直接内存(Direct Memory):JVM使用直接内存来支持NIO(New I/O)库,直接内存不是JVM运行时数据区的一部分,但也是JVM内存结构的一部分。
(7)虚拟机栈(JVM Stacks):虚拟机栈是线程私有的,随线程生灭。虚拟机栈描述的是线程中的方法的内存模型,每个方法被执行的时候,都会在虚拟机栈中同步创建一个栈帧(stack frame),方法被执行时入栈,执行完后出栈。
在JVM 8中,永久代被移除,取而代之的是元空间。元空间使用本地内存进行管理,避免了永久代可能引发的内存泄漏和溢出问题。这也是为什么在JVM 8及之后版本不再需要调整永久代大小,并且可以通过配置元空间大小来优化内存管理。
(1)方法区
JVM规范只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。在JVM规范中,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。由于方法区是JVM的规范,所以所有的虚拟机都必须遵守。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)。
(2)永久代
永久代在JVM启动时就会被固定分配一块内存空间,大小由-XpermSize参数指定,无法动态调整。
永久代的大小限制可能导致内存溢出问题,特别是在大量动态生成类的场景下。
永久代是 HotSpot 虚拟机基于 JVM 规范对方法区的一个落地实现,且永久代是 HotSpot 虚拟机特有的概念,而如 JRockit(Oracle)、J9(IBM) 虚拟机有方法区 ,但是没有永久代。
Metaspace(元空间)是 JDK8及之后, HotSpot 虚拟机对方法区新的实现。
(1)堆栈配置相关
(2)垃圾收集器相关