Java和C++的区别,体现在自动内存分配
和垃圾收集技术
。
JVM在执行Java程序时,会将它管理的内存分为若干个不同的数据区域。
这些区域有各自的作用范围以及生命周期:
- 线程私有的区域,随着用户线程的启动和结束而建立和销毁。
- 线程共享的区域,随着虚拟机进程的启动而一直存在,JVM结束后销毁。
1、程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,可以看做当前线程执行的字节码的行号指示器
。是线程私有
的。
JVM的字节码解释器工作时,就是通过改变这个计数器的值,来选取下一条需要执行的字节码指令。它是程序控制流的指示器。
1、为什么PC是线程私有的
首先,这个东西必须存在,因为肯定要记录下一条执行的指令是什么。
如果把这个东西做成线程共享的,那它的含义就是,当前正在执行的这个线程,下一条要执行的指令的地址
那么如果要发生线程上下文切换,新的线程必须告诉JVM,它的下一条指令的地址,同时,被切换走的那个线程也要保存它的下一条指令的地址
因此,为了线程切换后能恢复到上次执行的位置
,就必须有线程私有的程序计数器。
2、PC的值
- 如果线程执行的是一个Java方法,PC记录的是正在执行的虚拟机字节码指令的地址。
如果线程执行的是一个Native方法,PC的值为空
。
PC这块内存区域,是唯一一个没有规定任何 OutOfMemoryError 情况的区域,永远不会发生内存溢出。
3、PC的作用
它的效果就是保存了下一条要执行的字节码指令地址,但是有两个作用:
- 在本线程内:JVM的字节码解释器通过程序计数器来读取指令,所以PC可以
进行程序的流程控制
,比如顺序、循环、分支 - 在多线程的情况下:程序计数器用于
记录每个线程执行的位置
,从而当线程被切换回来的时候能够得知该运行哪一条指令
2、虚拟机栈
虚拟机栈(VM Stack)也是线程私有的。
虚拟机栈描述的是Java方法执行的线程内存模型:
每个方法被执行时,JVM都会同步创建一个栈帧(Stack Frame)
,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
。- 每个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表
通常说的,JVM中的“栈内存”,指的就是此处的虚拟机栈,或专指虚拟机栈中局部变量表部分。
局部变量表存放了编译期可知的局部变量的内容,包括基本类型和对象引用
、returnAddress类型(指向一条字节码指令的地址)
局部变量表所需的内存空间在编译期间完成分配。局部变量表的基本存储单位是“局部变量槽(Slot)”。
进入一个方法时,这个方法需要多大的局部变量空间是完全确定的(槽数确定),在方法运行期间不会改变局部变量表的大小。
这个内存区域存在两类异常:
-
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度
-
OutOfMemoryError:线程申请栈空间失败,内存不够。
(HotSpot不支持栈容量动态扩展,只要线程申请栈空间成功就不会在运行过程中发生OOM)
3、本地方法栈
作用和虚拟机栈类似,不过虚拟机栈是为Java方法服务的,而本地方法栈是为Native方法服务的。
这部分没有强制规定,HotSpot虚拟机直接把虚拟机栈和本地方法栈合二为一了
。
4、堆
堆(Heap)是JVM管理内存中最大的一块,被所有线程共享,在虚拟机启动时创建。
堆的唯一目的就是存放对象实例,大多数对象实例都存在这里
。
堆是垃圾收集器管理的内存区域,所以也被叫做“GC堆”(垃圾堆)。
堆占用的内存空间是可以扩展的,用这两个参数设定:
-Xmx // 设置最大内存 -Xms // 设置最小内存
如果堆的内存不足(堆上没有空间进行实例分配,且无法往大扩展),就会抛出 OutOfMemoryError 异常。
1、堆内存是怎么细分的
堆中可以细分为新生代和老年代
,其中新生代又分为Eden区、From Survivor区、To Survivor区,默认比例是8:1:1,可以调整。
5、方法区
方法区(Method Area)也是线程共享的。
它用于存储已经被虚拟机加载的类型信息、常量、类的静态变量、JIT 即时编译器编译后的代码缓存等数据
。
方法区属于堆的一个逻辑部分,但要和堆区分开理解。
1、方法区的具体实现
并未规定具体实现方式,可以由虚拟机自行实现。
在JDK 8以前,HotSpot选择使用“永久代”实现方法区
,这样可以让垃圾回收器也管理这部分内存,不用专门为这个空间写一个垃圾回收策略。但这样也有弊端,使得更容易发生内存溢出,因为永久代有内存上限
。
在JDK 6时,已经放弃使用永久代,逐步改为使用“本地内存”来实现方法区。
在JDK 7,已经把原本放在永久代的字符串常量池、静态变量等移出
到了JDK 8,完全废弃了永久代的概念,把内容全部移到了“元空间”中。
垃圾回收行为在方法区比较少见。但如果方法区内存不足,也会抛出 OutOfMemoryError 异常。
比如频繁使用动态代理创建出一堆类型,就会占据方法区内存
2、方法区和永久代的关系
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。
永久代就是HtoSpot对方法区的一种实现,其他虚拟机中是没有永久代的。
3、为什么要舍弃永久代,使用元空间
- 永久代有内存上限,由JVM管理,无法进行调整,容易发生内存溢出。
元空间在直接内存中
,直接内存是受本机可用内存的影响,相对更不容易内存溢出 - 元空间是方法区的实现,里面存放了类的元数据。元空间更大,那么能加载的类就更多了
- 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有⼀个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代
6、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分
。
Class文件中有“常量池表(Constant Pool Table)”,用于存放编译器生成的各种字面量与符号引用
,在类加载完成后,这部分内容会存放在方法区的运行时常量池中。
运行时常量池具备动态性。Java语言并不要求常量一定只有编译期才能产生,即并不是只有Class文件中常量池表的内容能进入方法区的运行时常量池,运行期间也可以将新的常量放入常量池,比如String类的intern()方法。
运行时常量池受到方法区内存的限制,如果无法申请到足够的内存,也会抛出 OutOfMemoryError 异常。
存放的位置
- 在JDK1.7前,运行时常量池+字符串常量池是存放在方法区的永久代中
- 在JDK1.7中,字符串常量池从方法区移到堆中,运行时常量池还保留在方法区中
- 在JDK1.8中,使用元空间实现方法区,此时字符串常量池依然保留在堆中,运行时常量池依然保留在方法区中,但此时的方法区处于元空间
7、直接内存
JDK1.8之后,方法区放在了元空间中,元空间就在直接内存中。
直接内存(Direct Memory)不是JVM运行时数据区的一部分,但也被频繁使用,也可能导致OOM。
在JDK 1.4中加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式。
它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆内的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能提高性能,避免了在Java堆和Native堆中来回复制数据。
直接内存受到主机总内存(物理内存、分页文件)的限制,如果设置得比实际内存更大,在动态扩展时就会发生内存不足,从而到最后OOM。
8、总结
JVM将内存主要分为了两大部分:堆和栈。其中堆是线程共享的,而栈是线程私有的。
- 堆:用于存储对象实例
- 栈:服务于每个线程方法的执行。
- 每个方法执行时,JVM会同步创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 一个方法的执行过程,对应着一个栈帧的入栈和出栈过程。
栈分成了两部分:
- 虚拟机栈:面向Java方法
- 本地方法栈:面向Native方法
要记录下一条执行的指令位置,并且控制线程的切换,需要设计一个线程私有的程序计数器。
- 程序计数器:
- 负责存放当前线程的字节码执行位置。字节码解释器根据PC的值来选取字节码指令去执行。
- 有两个作用:进行程序流程控制、保存上次执行到的字节码指令,线程切换回来后接着执行,所以它是线程私有的
HotSpot虚拟机在堆上额外开辟了一块内存,作为方法区。
- 方法区:
- JDK1.8之前在堆上的永久代中,JDK1.8之后在直接内存的元空间中。
用于存储已经被类加载的类、常量、类的静态变量、JIT 即时编译器的代码缓存
。
为了对常量进行优化,在方法区中开辟了运行时常量池。
- 运行时常量池:
是方法区的一部分,存放编译器生成的各种字面量与符号引用
。