运行时数据区
Java的内存分成那几部分?
- 线程不共享
- 程序计数器:每个线程会通过程序计数器记录当前将要执行的字节码指令的地址。程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
- Java虚拟机栈 和 本地方法栈:虚拟机栈采用栈的数据结构来管理方法调用中的基本数据(局部变量表、操作数栈、帧数据(动态链接、方法出口、异常表的引用)等),每一个方法的调用使用一个栈帧来保存。有可能出现内存溢出,少见,递归可能出现stack overflow错误。
- 线程共享
- 堆:堆中存放的是创建出来的对象,最容易产生内存溢出的位置。和垃圾回收机制有关系。
- 方法区:方法区中存储每个类的基本信息(元信息)、运行时常量池、即时编译器编译后的代码缓存等数据。7之前同时还保存了字符串常量池,也会出现内存溢出。(异常表)
- 直接内存:主要是NIO使用,由操作系统直接管理,不属于JVM内存。
Java内存中哪些部分会内存溢出?
除了程序计数器,其它都会内存溢出。因为程序计数器存放的是下一条指令的内存地址。对于Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果是本地方法,则不记录,程序计数器的值为undefined。
程序计数器的大小并不是固定的,也不直接等同于CPU的位数。它的大小足以支持记录当前执行的指令位置,这通常与具体的JVM实现有关,并不直接反映物理CPU的位数。相反,它更多地与JVM内部的指令处理机制相关。
内存溢出指的是内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存时因空间不足而失败,虚拟机一般会抛出指定的错误。
堆:溢出之后会抛出OutOfMemoryError,并提示是Java heap Space导致的。
栈:溢出之后会抛出StackOverflowError。
方法区:溢出之后会抛出OutOfMemoryError,JDK7及之前提示永久代,JDK8及之后提示元空间。通过bytebuddy创建一个新的类
直接内存:溢出之后会抛出OutOfMemoryError。
JDK不同版本在内存结构上的区别是什么?
- JDK6:堆(方法区(字符串常量池))。方法区称为永久代。
- JDK7:堆(方法区、字符串常量池)。方法区称为永久代。
- JDK8:堆(字符串常量池)、方法区。方法区称为元空间,使用系统内存。
运行时常量池一直在方法区中。
字符串常量池从方法区移动到堆的原因
- 垃圾回收优化:字符串常量池的回收逻辑和对象的回收逻辑类似,内存不足的情况下,如果字符串常量池中的常量不被使用就可以被回收;方法区中的类的元信息回收逻辑更复杂一些。移动到堆之后,就可以利用对象的垃圾回收器,对字符串常量池进行回收。
- 让方法区大小更可控:一般在项目中,类的元信息不会占用特别大的空间,所以会给方法区设置一个比较小的上限。如果字符串常量池在方法区中,会让方法区的空间大小变得不可控。
- intern方法的优化:JDK6版本中intern () 方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中。JDK7及之后版本中由于字符串常量池在堆上,就可以进行优化:字符串保存在堆上,把字符串的引用放入字符串常量池,减少了复制的操作。
StringTable练习
false,true
JDK6,字符串常量池在方法区中,intern时,会在字符串常量池新创建一个。 (java是自带的,直接返回)false,false
JDK7,字符串常量池在堆上,当intern时,堆上存在的话就创建一个引用。 (java是自带的,直接返回)true,false
后续JDK版本中,如果Java虚拟机不需要使用java字符串,那么字符串常量池中就不会存放java。打印结果有可能会出现两个true。
静态变量存储在哪里?
- JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。
- JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。具体源码可参考虚拟机源码:BytecodeInterpreter针对putstatic指令的处理。