JVM内存模型
在JVM中,内存空间主要分为两大块,每一个大块中,又分为多个小块。
- 线程共享区
- 堆内存
- 方法区(直接内存)
- 线程私有区
- 栈内存
- 本地方法栈
- 程序计数器
线程共享区
堆内存
何时分配?
java程序启动时自动分配
堆内存中存放了什么内容?
存放对象实例(new的对象),所有的对象实例及数据都在堆内存中分配内存。
在jdk1.7后,常量和静态变量也存放在堆内存中。
如何设置堆内存?
可以通过参数【-Xms】设置起始内存,默认为电脑内存大小 / 64
可以通过参数【-Xmx】设置最大内存,默认为电脑内存大小 / 4
堆内存划分
为了高效的GC回收,jvm将堆内存分为三个区域,其中年轻代和老年代的默认比例为1:2,可以通过参数【-XX:NewRatio】设置
- 年轻代
- 老年代
- 元空间(jdk1.7前为永久代)
年轻代
年轻代的内存分为两个大区,Survivor区又分为两个小区,三个区的默认比例为【8 : 1 : 1】,可通过参数【-XX:SurvivorRatio】设置,具体结构如下
- eden区(80%)
- Survivor区
- From区(10%):
- To区(10%)
eden区
新创建的对象会分配到eden区中
Survivor-From区(S0区)
存放第一次ygc后,从eden区转移过来的对象
Survivor-To区(S1区)
存放第二次ygc后,从eden区和from转移过来的对象,并将from区和to区进行转换。
每次转换后,会保证To区为空区域,用于保存下一次转移的对象
为什么Survivor区要分为两个区?
- 为了避免内存碎片的产生
- 可以保证有一个区域永远为空,用来接收新转移的对象
- survivor区会保留未晋升且未回收的对象,如果只有一个区,那就会出现多个不连续的内存地址,当eden将大对象转移到survivor区时,会出现没有足够大的内存进行分配,最终直接晋升到老年代中,造成老年代的资源浪费
年轻代的对象如何晋升老年代?
- 当survivor区中的对象年龄到达阈值(默认值15)
- 当to区被填满时,所有对象晋升至老年代
为什么默认阈值设置为15?
在jvm的对象头信息中,记录年龄的字段占用4个字节,换算为二进制后,最大值为1111,即15
老年代
老年代中存放从年轻代晋升过来的对象,不会轻易进行回收。
为了避免年轻代的三个区之间发生大量的内存拷贝,一些大对象也可以直接进入老年代。
什么是大对象?
- 数组
- 长字符串
- 可以通过参数【-XX:PretenureSizeThreshold】设置大对象的阈值
元空间
元空间通过本地内存实现,可以看作方法区的一种实现,元空间中存放编译后的class文件。
可以通过参数【-XX:MetaspaceSize】设置元空间大小,默认为20.75MB
方法区
方法区在jdk1.8以前称为永久代,存放类型及编译后的代码缓存,在jdk1.7以前,还存放常量和静态变量。
线程私有区
栈内存
栈空间存放局部变量、操作数栈、动态链接、方法出口地址,它是一种快速有效的分配方式,可以通过参数【-Xss】设置栈空间大小
栈没有GC问题,它的生命周期和线程的生命周期绑定。
工作流程
方法执行前入栈,方法执行结束后出栈
本地方法栈
native方法存放在本地方法栈中,由C语言实现
程序计数器
运行速度最快的区域,记录线程指令。
如果现在执行的是java方法,计数器记录jvm字节码的指令地址。
如果是native方法,则记录为undefined
总结
栈是运行单位,解决程序的运行问题
堆是存储单位,解决数据存储问题