文章目录
栈、堆、方法区的交互关系
从内存结构上
看运行时数据区包含本地方法栈、程序计数器、虚拟机栈、堆和方法区
从线程共享与否
的角度来看运行时数据区的划分
从最简单的代码角度出发,当前声明的变量是Student类型的student,把整个Student类的结构加载到方法区
,把变量student放到虚拟机栈中
,new的对象放到Java堆中
在虚拟机栈局部变量表中存放的是各个变量,其中reference区域就相当于图中的student变量
,引用类型reference指向了堆空间中对象的实例数据
,在堆的对象实例数据中有一个到对象类型数据的指针
,这个指针指向了方法区中对象类型的数据
。
方法区的理解
方法区是可供各个线程共享的运行时内存区域。方法区与传统语言中的编译代码存储区或者操作系统进程的正文段的作用非常类似,它存储了每一个类的结构信息
,例如,运行时常量池(Runtime Constant Pool)
、字段和方法数据、构造函数和普通方法的字节码内容
,还包括一些在类、实例、接口初始化时用到的特殊方法
。
方法区的基本理解
对于方法区的理解我们要注意以下几个方面。
(1)方法区(Method Area)与堆一样,是各个线程共享的内存区域。
(2)方法区在JVM启动的时候被创建,并且它实际的物理内存空间和虚拟机堆区一样都可以是不连续的。
(3)方法区的大小跟堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类
。如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误,
如java.lang.OutOfMemoryError:PermGen space
或者java.lang.OutOfMemoryError:Metaspace
。
以下情况都可能导致方法区发生OOM异常:加载大量的第三方jar包
、Tomcat部署的工程过多(30~50个)
或者大量动态地生成反射类
。关闭JVM就会释放这个区域的内存。
JDK中方法区的变化
在JDK 7及以前,习惯上把方法区称为永久代。但是JDK 8移除了永久代。JDK 8之后称为元空间。
不过元空间与永久代最大的区别在于元空间不在虚拟机设置的内存中,而是在本地内存
。
方法区的内部结构
Java源代码编译之后生成class文件,经过类加载器把class文件中的内容加载到JVM运行时数据区。class文件中的一部分信息加载到方法区,比如类class、接口interface、枚举enum、注解annotation以及运行时常量池等类型信息。
类变量和常量
static修饰的成员变量为类变量或者静态变量,静态变量和类关联在一起,随着类的加载而加载。类变量被类的所有实例共享,即使没有类实例时也可以访问它。
在JDK 7之前类变量也是方法区的一部分,JDK 7及以后的JDK类变量放在了堆空间
。此外,使用final修饰的成员变量表示常量
,使用static final修饰的成员变量称为静态常量
,静态常量和静态变量的区别是静态常量在编译期就已经为其赋值
。
静态常量和静态变量的区别
public class MethodAreaTest {
public static int count = 1;
public static final int number = 2;
public static void main(String[] args) {
}
}
javap -verbose MethodAreaTest
可以发现被声明为static的类变量count在编译时期并没有做赋值处理,而对于声明为“static final”的常量处理方法则不同,每个全局常量在编译的时候就会被赋值。
常量池
方法区内部包含了运行时常量池。class文件中有个constant pool,翻译过来就是常量池。当class文件被加载到内存中之后,方法区中会存放class文件的constant pool相关信息,这时候就成为了运行时常量池。
可以把常量池看作一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
。
运行时常量池
上面我们讲了class文件中的常量池,接下来我们再讲一下什么是运行时常量池。运行时常量池(Runtime Constant Pool)是方法区的一部分。
虚拟机加载类或接口后,就会创建对应的运行时常量池。JVM为每个已加载的类型(类或接口)都维护一个常量池。
运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
方法区的演进细节
HotSpot虚拟机中方法区的变化
JDK 6中方法区的内容如图所示。
JDK 7中方法区的内容如图所示。可以发现相对JDK 6来说,字符串常量池(StringTable)位置发生了变化。为什么要对字符串常量池的位置进行调整呢?因为永久代的回收效率很低,在Full GC的时候才会触发,而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致字符串常量池回收效率不高。我们程序中一定会有大量的字符串被创建,而很多字符串往往不需要永久保存,那么回收效率低的话,就会导致永久代内存严重不足。如果将字符串放到堆里,内存就能及时回收利用
。
在JDK 7中,字符串常量不再在Java堆的永久代中分配,而是和应用程序创建的其他对象一样,在Java堆的主要部分(称为新生代和老年代)中分配。
JDK 8中方法区的内容如图所示,这个时候方法区的实现元空间不再占用JVM内存,而是把元空间放到了本地内存
。
永久代为什么被元空间替换
JDK 7之前的版本中,HotSpot虚拟机将类型信息、内部字符串和类静态变量存储在永久代中
,垃圾收集器也会对该区域进行垃圾回收。JDK 7将HotSpot虚拟机中永久代内部字符串和类静态变量数据移动到Java堆中
,但是依然存在永久代
。
随着Java 8的到来,HotSpot虚拟机中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,原因有以下两点。
(1)为永久代设置空间大小是很难确定的。
在某些场景下,如果动态加载类过多,容易产生永久代的OOM。比如某个集成了很多框架的Web工程中,因为功能繁多,在运行过程中要不断动态加载很多类,可能出现如下致命错误。
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
(2)将元数据从永久代剥离出来放到元空间中,不仅实现了对元数据的无缝管理,而且因为元空间大小仅受本地内存限制,也简化了Full GC,并且可以在GC不暂停的情况下并发地释放元数据。
方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型信息。
先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用
。
字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量。
类和接口的全限定名。
字段的名称和描述符。
方法的名称和描述符。
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量与回收Java堆中的对象非常类似。判定一个常量是否“废弃”还是相对简单的,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件。
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。