目录 Java 内存区域 JVM 垃圾回收 类加载器与类加载器 HotSpot 虚拟机对象实现 一、Java 内存区域 程序计数器 当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 虚拟机栈 除Native方法外,所有的方法都是通过栈来实现的。方法调用的数据需要通过栈进行传递,每次方法调用,都会有一个对应的栈帧被压入栈中,每次方法调用结束,都会有一个栈帧被弹出。栈的生命周期也和线程相同。 局部变量表:存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)。 操作数栈:作为方法调用的中转站,用于存放方法执行过程中的临时操作数和中间计算结果。由于JVM的无寄存器设计,操作数栈成为核心的计算区域。例如: 动态链接:指向运行时常量池中的方法引用(Method References)。用于在方法中调用其他方法时,将Class文件常量池中指向方法的符号引用(因为编译期无法知道实际内存地址)转化为其在内存地址中的直接引用。 程序运行中栈可能会出现两种错误: StackOverFlowError:栈的内存大小不允许动态扩展时,线程请求栈的深度超过当前虚拟机栈最大深度(如函数调用陷入无限循环)。 OutOfMemoryError:栈的内存大小允许动态扩展时,虚拟机在动态扩展栈时无法申请到足够的内存空间。 本地方法栈 与虚拟机栈类似,区别在于其为Native方法服务,HotSpot中与虚拟机栈合二为一。 堆 在JVM启动时创建,是由所有线程共享的一块最大的内存区域。堆的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 JDK1.7后默认开启逃逸分析,如果某些方法中的对象引用没有被返回或未被外面使用(即未逃逸),那么对象可以直接在栈上分配内存。 堆也称为GC堆,是垃圾收集器管理的主要区域。收集器基本都采用分代垃圾收集算法,因此堆可以细分为:新生代、老年代、永久代,JDK8后永久代被元空间取代。新生代还能划分为Eden、Survivor。详见内存分配与回收原则。 程序运行中堆最容易出现 OutOfMemoryError 错误,例如: java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当JVM花费过多的时间进行GC,却只能回收很少的堆空间时。 java.lang.OutOfMemoryError: Java heap space:堆内存不足以存放新创建的对象。(受制于配置的最大堆内存 -Xmx 和物理内存大小。) 方法区 方法区是JVM运行时线程共享的一块逻辑区域(抽象概念),在不同的虚拟机上,方法区的实现(如永久代、元空间)是不同的。当JVM要使用一个类时,它会读取并解析Class文件获取相关信息(类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等),再将信息存入到方法区。 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) ? 永久代受制于JVM设置的固定大小 -XX:MaxPermSize ,启动后无法调整,加载大量类时容易出现 java.lang.OutOfMemoryError: PermGen space 错误;而元空间使用本地内存,加载多少个类的元数据只受制于本机实际可用内存,溢出的概率更小。(元空间溢出时会出现 java.lang.OutOfMemoryError: MetaSpace 错误) -XX:MaxMetaspaceSize:设置最大元空间大小,默认值为unlimited。 -XX:MetaspaceSize:设置元空间的初始大小,如果未指定,则根据运行时的应用程序需求动态调整。 永久代与堆共享垃圾回收器,增加了GC的复杂度,且回收效率低。 JDK8以前,HotSpot通过永久代存储类的元数据,而JRockit中没有类似的概念,因此在两者合并后,就没有必要额外设置一个永久代了。 运行时常量池 Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池表(Constant Pool Table)。常量池表会在类加载后存放到方法区或元空间的运行时常量池中,内存受制于方法区或元空间。 字面量:程序中显式写出的值,如数字、字符、字符串等。 符号引用:对类、字段、方法、接口方法等的引用。解析阶段就是JVM把常量池中的符号引用替换成直接引用的过程。 字符串常量池 字符串常量池是JVM为了提