更多精彩,访问可乐可乐可的博客https://blog.youkuaiyun.com/weixin_44494373
Java内存区域的那些事
楼房之地基,知晓内存区域,才能知晓JVM
Java的内存区域的管理是“全自动”的,每一个刚刚见识的人都会觉得神奇
Java的自动管理内存,使得我们不需要操作释放内存的代码,不容易出现内存泄漏与溢出的现象。
但是我们不得不提防,一旦出现了内存问题,知晓虚拟机的工作模式,才能排出错误。
Java的内存区域有几个时间点,产生了比较重要的变动,分别是JDK1.7,Java8
- 在JDK1.7前,字符串存放在方法区之中
- 在JDK1.7后字符串被放在了堆
- 在Java8,取消了方法区,改用了直接使用直接内存的的元空间
Java内存区域
Java8之前的运行时数据区域
这张图很重要,要熟记
Java虚拟机在执行Java程序时,将其所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁时间,有的区域,随着虚拟机进程的启动而存在,有的依赖用户线程的启动和结束。
1、程序计数器
程序计数器(Program Counter Register)
一块较小内存空间,是当前线程所执行的字节码行号指示器。字节码解释器工作时通过改变计数器达到语句的切换功能。
“线程私有”,各线程都拥有,互相独立,互不影响。
PC计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
tips
若为
Java方法
,计数器记录的是正在执行的虚拟机字节码指令的地址
。若为
Native方法
,这个计算器值为空
。Native方法为Java的本地方法,Java依赖了很多本地其他语言的代码,这些代码将使用Native注册在Java虚拟机中,供Java使用
2、Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks)
描述Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame,方法运行时的基础数据结构),存放局部变量表(编译期可知的基础数据类型)、操作数栈、动态链接、方法出口等等信息
线程私有,生命周期与线程相同。
空间以Slot为单位,64位数据占两个。
两个异常:
- StackOverflowError:线程请求深度(方法调用,算术运算会使用)大于虚拟机所允许的深度(编译时指定)。
- OutOfMemoryError:虚拟机栈在动态扩展时,无法请求到足够的内存
3、本地方法栈
本地方法栈(Native Method Stack)
类似虚拟机栈,为虚拟机使用到的Native方法服务
两个异常:
- StackOverflowError:线程请求深度(方法调用,算术运算会使用)大于虚拟机所允许的深度(编译时指定)。
- OutOfMemoryError:虚拟机栈在动态扩展时,无法请求到足够的内存
4、Java堆
Java堆(Java Heap)
Java虚拟机所管理的最大块,垃圾收集器管理的主要区域,故也称“GC堆”
存放对象实例,几乎所有的对象实例都在这里分配内存
被所有线程共享
异常:
- OutOfMemoryError:堆中没有内存完成实例分配,并且堆无法再扩展
从垃圾回收的角度,Java堆可以细分为:老年代和新生代。
Java堆可能划分出多个线程私有的分配缓冲区。
Java堆可能在不连续的内存空间,但是逻辑上是连续的
5、方法区
方法区(Method Area)
用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器(JLT,将字节码直接翻译为机器代码,加速执行)编译后的代码等数据
被所有线程共享
Java虚拟机规范将其描述为堆的一个逻辑部分,但有一个别名“非堆”,被多数人称为“永久代”
关于字符串常量池,在JDK1.6中,字符串在方法区拥有对象,在JDK1.7中只有引用,在jdk8中,永久代(方法区)被元空间取代了。
https://juejin.im/post/5c160420518825235a05301e
6、运行时常量池
运行时常量池(Runtime Constant Pool),方法区的一部分,
用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
除了Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
动态性:运行期间也能将新变量放入常量池。(如**String类的intren()**方法)
7、直接内存
直接内存(Direct Memory)
虚拟机外的内存
NIO:JDK1.4中新引入,引入一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以直接使用Native函数库分配堆外内存,然后通过一个存储在Java堆中DirectByteBuffer对象作为这块内存的引用进行操作。
可能的异常:OutOfMemoryError
Java8以后的内存区域
在Java8,取消了方法区,使用元空间顶替了原来方法区的功能,元空间使用直接内存,效率更高。
原有的老年代新生代在堆内进行划分。
新的内存如何进行垃圾回收
看图,我们能得到,新生代被分为了三个区域:一个较大的Eden和两个较小的Survivor
Survivor中总是有一个是空的,当发生GC时,使用标记-复制算法将Eden与一个正在使用的survivor中的对象一起复制到空闲的Survivor中。
默认Eden与Survivor大小比例为8:1 (8+1+1=10)
若Survivor不足以放存活的对象,则会使用其他空间(一般是老年代)做分配担保,
其中,使用动态年龄计算的方法,将对象一级一级的升级
Eden->From Survivor0 ->To Survivor1 -> OldMemory
对象如何被添加到老年代
- 当一次新生代垃圾回收Minor GC过后,若对象仍然存活,则进入To Survivor,并年龄+1
- To Survivor成为新的From Survivor
- 当From Survivor中的对象超过了年龄阈值则进行晋升
- 若To Survivor在GC后,填入被移动的对象,成为新的From前,被填满,则整体移动到老年代
动态年龄计算的方法
每次新生代垃圾回收,都会将存活对象年龄加一
若累计的某个年龄大小超过了Survivor区的一半,则取这个年龄与MaxTenuringThreshold中更小的一个值,作为晋升的年龄阈值。