当前位置: 首页 > 后端技术 > Java

终于明白了Java8的内存结构,不再纠结于方法区和常量池!!

时间:2023-04-01 15:43:48 Java

Java8内存结构图虚拟机内存和本地内存的区别Java虚拟机在执行过程中会将托管内存分配到不同的区域。这些区域称为虚拟机内存。同时,也没有对虚拟机的直接管理。虚拟机的物理内存也有一定程度的使用。被使用但不在虚拟机内存数据区的内存称为本地内存。两种内存有一定的区别:JVM内存是由虚拟机内存大小这个参数控制的。当size超过参数设置的size时会报OOM。本地内存不受虚拟机内存参数限制,只受物理内存容量限制。虽然不受参数限制,但是如果内存使用超过了物理内存的大小,同样也会报OOMjavaruntimedataareajava虚拟机在执行过程中会将托管内存划分到不同的区域,一些会随着线程产生和消失,有的会随着java进程产生和消失,根据《Java虚拟机规范》的规定,运行时数据区分为以下几个区域:程序计数器(ProgramCounterRegister)的程序计数器是当前线程执行的字节码的行号指示器。通过改变计数器的值,选择下一行指令,并通过它们来实现跳转、循环、恢复线程等功能。在任何时候,一个处理器核心只能运行一个线程。多线程是通过依次切换线程和分配时间来完成的。这需要一个标志来记住每个线程执行的位置。这里需要程序计数器。.因此,程序计数器是线程私有的,每个线程都有自己的程序计数器。虚拟机栈(JVMStacks)虚拟机栈是线程私有的,会随着线程一起生死。虚拟机栈描述了线程中方法的内存模型:每个方法执行时,都会在虚拟机栈中同步创建一个栈帧(stackframe)。每个堆栈帧包含以下局部变量表。局部变量表存放java基本数据类型(byte/boolean/char/int/long/double/float/short)和方法中的对象引用(注意:这里的基本数据类型是指方法中的局部变量)操作数栈动态连接方法返回地址在方法执行时入栈,执行完出栈。可能会抛出两种异常:如果线程请求的栈深度大于虚拟机指定的栈深度,则会抛出StackOverFlowError,即栈溢出。如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时,就会抛出OutOfMemoryError,即OOM内存溢出。MethodStacks)本地方法栈和虚拟机栈的作用类似,都会抛出OutOfMemoryError和StackOverFlowError,都是线程私有的。主要区别是虚拟机栈执行的是java方法。本地方法栈执行本地方法。(什么是Native方法?)Java堆(JavaHeap)java堆是JVM内存中最大的一块,所有线程共享,是垃圾收集器管理的一块内存区域。它主要存储对象实例。当然由于java虚拟机的发展,堆里面的东西多了很多,现在主要有:对象实例类初始化生成的对象基本数据类型的数组也是objectinstancestringconstantpool字符串常量池本来是存放在方法区的,jdk7开始把它放到了堆中。字符串常量池存储的是对字符串对象的直接引用,而不是直接存储的对象。它是一个字符串表静态变量。静态变量是静态修饰的变量。在jdk7中,从方法区迁移到堆中的线程分配缓冲区(ThreadLocalAllocationBuffer)是线程私有的,但不影响java堆的通用性。增加线程分配缓冲区是为了提高对象分配的效率。java堆可以是固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设置),如果堆不能扩展或者内存不能分配,也会报OOM。方法区(MethodArea)方法区绝对是网上所有关于java内存结构的文章争论的焦点,因为方法区的实现在java8.0有了重大革新。下面我们来讨论一下:方法区是所有线程共享的内存。java8以前是放在JVM内存中,由永久代实现,受JVM内存大小参数限制,java8去掉了永久代的内容,方法区由元空间(MetaSpace)实现,并且直接放在本地内存中,不受JVM参数限制(当然如果物理内存满了,方法区也会报OOM),将原本放在方法区的字符串常量池和静态变量转移到Java堆。方法区与其他区域的区别在于,方法区的内容在编译时和类加载完成后略有不同,但总体上分为两部分:类元信息(Klass)类元信息在类中compilation放到方法区,里面放着类的基本信息,包括类的版本,字段,方法,接口,常量池表(ConstantPoolTable)。常量池表(ConstantPoolTable)存储类在编译过程中产生的字面量。,Symbolicreferences(什么是字面量?什么是符号引用?),这些信息会在类加载后解析到运行时常量池中。运行时常量池(RuntimeConstantPool)主要存放的是类加载后解析的字面量和符号引用,但不仅这些运行时常量池是动态的,可以添加数据,更常用的是intern()方法的字符串类。直接内存位于本地内存中,不属于JVM内存。但是它在物理内存耗尽的时候也会报OOM,所以我也来说说。jdk1.4增加了NIO(NewInput/Output)类,引入了一种新的基于通道(channels)和缓冲区(buffers)的IO方法。可以使用native函数直接分配堆外内存,然后存储java堆中的DirectByteBuffer对象作为对这块内存的引用进行操作,在某些场景下可以大大提高IO性能,避免java之间来回复制数据堆和本机堆。常见问题解答什么是Native方法?由于java是高级语言,距离硬件底层比较远,有时无法操作底层资源。所以java加入了native关键字,被native关键字修饰的方法可以用其他语言重写。这样,我们就可以写一个native方法,用C重写,来操作底层资源。当然,使用native方式会导致系统的可移植性不高,这点需要注意。成员变量、局部变量、类变量存放在内存的什么位置?类变量类变量用静态修饰符修饰,在方法外定义。随着java进程的生成和销毁,静态变量在java8之前是存放在方法区,在java8中是存放在堆中。成员变量定义在类的Variables中,但没有被static修饰符修饰,随着类实例的生成和销毁,它们是类实例的一部分。因为是实例的一部分,所以在初始化类的时候,直接从运行时常量池中取引用或者值,这就和初始化的对象一起放到堆的局部变量中了。局部变量是在类的方法中定义的变量。调用方法时,将其放入虚拟机栈的栈帧中。方法执行完后,从虚拟机栈中弹出,所以存放在虚拟机栈中final修饰的常量存放在哪里呢?final关键字不影响在内存中的位置,具体位置请参考上一题。类常量池、运行时常量池、字符串常量池是什么关系?有什么不同?类常量池和运行时常量池都存放在方法区,字符串常量池在jdk7中已经从方法区迁移到java堆中。在类编译过程中,类的元信息会被放入方法区。部分类元信息是类常量池,主要存放字面量和符号引用,部分字面量为文本字符。加载类时将字面量和符号引用解析为直接引用,存入运行时常量池;对于文本字符,在解析时会查找字符串常量池,找出文本字符对应的字符串对象的直接引用,直接将引用存入运行时常量池;字符串常量池存储对字符串对象的引用,而不是字符串本身。什么是文字?什么是符号引用?编译期间无法引用文字Java代码。字面量是编译时数据的表示:inta=1;//这个1是字面量Stringb="iloveu";//iloveu是字面量符号引用。由于在编译过程中不知道每个类的地址,因为这个类可能还没有被加载,所以如果你在一个类中引用另一个类,那么你就无从知道它的内存地址,那怎么办,我们只能把他的类名作为一个符号引用,在类加载完成后,通过这个符号引用来获取他的内存地址。例子:我在com.demo.Solution类中引用了com.test.Quest,那么我将com.test.Quest保存为类常量池的符号引用,类加载完成后,将这个引用取到方法中area找到这个类的内存地址。