1 成员变量、局部变量、类变量存储在内存的什么地方?
类变量(静态成员变量) 方法区
类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁在java7之前把静态变量存放于方法区,在java7时存放在堆中
成员变量 堆
成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
局部变量 栈
局部变量是定义在类的方法中的变量
在所在方法被调用时放入虚拟机栈的栈帧中,栈顶是正在执行的方法,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
1.1 案例分析
public class StaticObjTest {
static class Test{
// 静态变量
// 一个java.lang.Class类型的对象实例引用了此变量
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();
}
}
跟据代码分析处内存分布:
首先main方法入栈(Main是主方法先执行),执行 Test test = new StaticObjTest.Test(); 引用了外部类StaticObjTest的内部类Test,所以会先加载Test的静态变量Staticobj(存在方法区),在堆中加载了ObjectHolder的实例staticObj,并分配了内存地址0x001.
然后会执行 new StaticObjTest.Test ,在堆中实例化了一个Test类的对象并分配了内存地址0x002
调用test实例的foo方法,foo方法入栈,foo方法实例化了ObjectHolder的对象localObj存在堆中,分配了内存地址0x003。
最后加载了ObjectHolder的实例对象instanceObj,分配了堆中的内存地址0x004.
2. 方法区、永久代、HotSpot
HotSpot是一种主流的JAVA虚拟机实现它由Oracle开发和维护。方法区和永久代则是HotSpot虚拟机中的两个相关概念。
-
HotSpot虚拟机:HotSpot是一种流行的Java虚拟机实现,常用于使用Oracle JDK或OpenJDK。它是Java平台上广泛使用的虚拟机,具有高性能、动态编译和优化的特性。
-
方法区(Method Area):方法区是Java虚拟机的一部分,用于存储已加载的类信息、常量池、静态变量和编译后的代码等。它是各个线程共享的内存区域,与堆和栈不同。在HotSpot虚拟机中,方法区通常被称为元空间(Metaspace),它解决了早期版本中永久代的一些问题。
-
永久代(Permanent Generation):永久代是方法区的一种实现方式,在早期版本的HotSpot虚拟机中使用。它是方法区的代表,主要用于存放类的元数据、运行时常量池、静态变量和字符串常量等。然而,永久代大小固定且难以调整,容易导致内存溢出和泄漏问题。
总结来说,HotSpot是一种Java虚拟机的实现,而方法区和永久代是HotSpot虚拟机中的两个相关概念。方法区用于存储已加载的类信息、常量池、静态变量和编译后的代码等,而永久代是方法区的一种实现方式,在早期版本中使用。目前,HotSpot虚拟机通常使用元空间来代替方法区和永久代,以提供更灵活和可扩展的内存管理方式。
方法区可以理解为接口,而元空间和永久代是接口的实现 jdk8以前永久代由jvm管理,而jdk8元空间由直接内存管理,并且取代了永久代。
3. 为什么调整字符串常量池和静态变量的位置?
、JDK7将字符串常量池放到了堆空间中,因为永久代的回收效率很低,只有在FULL GC时才会触发,而FULL GC只有在老年代的空间不足、永久代不足时才会触发,这就导致了字符串常量池回收效率不高,而在我们开发中会有大浪的字符串被创建,回收效率低会导致永久代内存不足,
将字符串常量池放到堆里,能及时回收内存!
堆 --> 新生代 and 老年代
经历过15次gc进入老年代(young GC) 老年代满发生FULL GC
方法区->永久代
4. 为什么用元空间替换永久代
因为现在的程序Jar包过于庞大,而永久代是存放于jvm内存中,所以为永久代设置最大空间大小比较难以确定,但是元空间是直接使用本地内存的,所以不用担心永久代内存移除情况
永久代的调优也比较困难
5.JDK1.8元空间会产生内存溢出么?在什么情况下会产生内存溢出?
会产生内存溢出,在Java 8及之后的版本中,使用元空间(Metaspace)代替了永久代(Permanent Generation)。元空间相比永久代有更大的灵活性和可扩展性,可以动态分配更多的内存。但是,元空间并非无限制的,也有可能发生溢出。
以下是一些可能导致元空间溢出的情况:
-
类数量过多:如果应用程序动态加载了大量的类或生成了大量的匿名类,会增加元空间的负担,导致空间不足而溢出。
-
字符串常量过多:字符串常量也存储在元空间中。如果应用程序使用了大量的字符串常量,特别是较长的字符串常量,会占用较多的空间,导致元空间溢出。
-
大量动态生成的代码:某些框架或库在运行时可能会生成大量的动态代理类或动态字节码,这些动态生成的代码会存储在元空间中,如果产生了过多的动态代码,就会造成元空间的溢出。
-
未正确配置元空间大小:元空间的大小默认是不受限制的,它会根据应用程序的需求进行动态分配。但是,如果没有正确设置元空间的限制,或者设置了过小的限制,也可能导致元空间溢出。
当元空间溢出时,通常会抛出OutOfMemoryError: Metaspace
异常。为避免元空间溢出,可以采取以下措施:
-
增加元空间的大小:通过设置JVM参数
-XX:MaxMetaspaceSize
来增加元空间的最大大小。 -
优化代码和配置:减少动态加载类、避免过多的字符串常量,合理使用动态代理等技术,优化代码以减少元空间的负担。
-
监控和调优:使用监控工具来监视元空间的使用情况,及时发现并解决潜在的溢出问题。
6. JVM8的内存结构
6.1 程序计数器
java在执行字节码的时候的行号指示器,是线程私有的 记录了字节码的行号指示器。
每个线程都有自己的程序计数器
6.2 虚拟机栈
虚拟机栈是线程私有的,跟随线程生灭。在方法被执行的时候在虚拟机栈中创建一个栈帧,方法执行时入栈,执行完后出栈,栈帧包含:
局部变量表:存储着方法里的java基本数据类型以及对象的引用
操作数栈
动态链接
方法返回地址
虚拟机栈可能会抛出两种异常:
- 如果线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出StackOverFlowError即栈溢(指针指不到的情况)
- 如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出
StackOverError栈溢出原因:
无限递归循环调用
执行了大量方法,导致线程栈空间耗尽
方法内声明了海量的局部变量
6.3 Java堆(Java Heap)
java堆是JVM内存中最大的一块,由所有线程共享, 是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于java虚拟机的发展,堆中也多了许多东西,现在主要有:
- 对象实例
-
- 类初始化生成的对象
- 基本数据类型的数组也是对象实例
- 字符串常量池
-
- 字符串常量池原本存放于方法区,从jdk7开始放置于堆中。
- 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
- 静态变量 (堆)
-
- 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
- 线程分配缓冲区(Thread Local Allocation Buffer)
-
- 线程私有,但是不影响java堆的共性
- 增加线程分配缓冲区是为了提升对象分配时的效率
java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设定),如果堆无法扩展或者无法分配内存时也会报OOM。
6.4 方法区(Method Area)
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
(1)类型(类class、接口interface、枚举enum、注解annotation)信息:
对每个加载的 类型 (类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang. Object,都没有父类)
- 这个类型的修饰符( public, abstract,final的某个子集)
- 这个类型实现接口的一个有序列表。
(2) 域(Field)信息:
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。域的相关信息包括如下内容:
- 域名称
- 域类型
- 域修饰符(public,private,protected,static,final, volatile,transient的某个子集)
(3) 方法(Method)信息:
JVM必须在方法区中保存类型的所有方法的相关信息以及方法的声明顺序。方法的相关信息包括:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表(abstract和 native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
(4)静态变量(non-final的)
静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
补充说明:被声明为final的静态变量的处理方法则不同,被static和final修饰的变量也称为全局变量,每个全局常量在编译的时候就会被赋值
6.5 小结
6.5.1 方法区:
类型信息:已加载的 类、接口、枚举、注解、即时编译器编译后的代码缓存等
类的方法信息
运行时常量池:比常量池包含更多的信息,例如编译时无法确定的常量、运行时生成的String对象
静态变量:所有类的静态变量都存在方法区,无论是否实例化,在类的加载初期初始化,贯穿整个运行期间。
6.5.2 堆:
对象实例:类初始化生成的对象 基本数据类型的数组也是对象实例
字符串常量池:7之后开始放于堆中,之前在方法区
静态变量:7之后迁移至堆
线程分配缓冲区