内存模型结构概述
JVM 内存模型结构包括方法区(Metaspace)、堆(Heap)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)和程序计数器(Program Counter)。下面是对这些部分的简要描述:
- 方法区(Metaspace):用于存储类的结构信息、静态变量、常量、即时编译器编译后的代码等数据。在 JDK 8 之前,方法区被实现为永久代(Permanent Generation),而在 JDK 8 及以后的版本中被改为使用元空间(Metaspace)来实现。
- 堆(Heap):用于存储对象实例和数组等动态分配的内存。堆是 JVM 内存管理中最大的一块,也是垃圾回收的重点区域。在堆中,可以细分为新生代(包括伊甸园区和幸存者区)和老年代。
- 虚拟机栈(VM Stack):每个线程在创建时都会被分配一个虚拟机栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调用时,会在虚拟机栈中创建一个栈帧,用于存储方法的运行时数据。
- 本地方法栈(Native Method Stack):和虚拟机栈类似,但是用于执行 native 方法(即由本地语言(如 C、C++)实现的方法)时使用。
- 程序计数器(Program Counter):存储当前线程正在执行的字节码指令地址或返回地址。每个线程都有自己独立的程序计数器,因此它是线程私有的内存区域。
这些内存区域共同组成了 JVM 的内存模型结构,每个区域都有其特定的作用和内存分配规则。深入理解 JVM 内存模型结构有助于开发人员更好地理解 Java 内存管理、垃圾回收等方面的工作原理,从而优化程序性能、避免内存泄漏等问题。
内存模型结构详解
栈内存:用于存储局部变量,当数据使用完,所占空间会自动释放。
堆内存:数组和对象,通过new建立的实例都存放在堆内存中。
方法区:静态成员、构造函数、常量池、线程池
本地方法区:windows系统占用
寄存器:用来暂时存放参与运算的数据和运算结果
程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程 私有。
Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。
Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。
Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),
回收目标主要是常量池的回收和类型的卸载,各线程共享
线程私有区域【程序计数器、虚拟机栈、本地方法区】
线程共享区域【JAVA 堆、方法区】、直接内存
Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代。
默认的设置下,当对象的年龄达到 15 岁的时候,也就是躲过 15 次 Gc 的时候,他就会转移到老年代中去躲过 15 次 GC 之后进入老年代。
1、JVM中堆空间可以分成三个大区,新生代、老年代、永久代
2、新生代可以划分为三个区,Eden区,两个Survivor区,在HotSpot虚拟机Eden和Survivor的大小比例为8:1
哪些对象会被存放到老年代
- 新生代对象每次经历一次minor gc,年龄会加1,当达到年龄阈值(默认为15岁)会直接进入老年代;
- 大对象直接进⼊老年代;
- 新生代复制算法需要⼀个survivor区进行轮换备份,如果出现⼤量对象 在minor gc后仍然存活的情况时,就需要老年代进行分配担保,让survivor无法容纳的对象直接进入老年代;
- 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的⼀半,年龄大于或等于该年龄的对象就可以直接进入年老代。
什么时候对象进入老年代
1.伊甸园区放不下的对象,直接晋升到老年代。
2.进入Survivor区的一批对象,超过Survivor一半,将大于等于该批对象的移至老年代。
3.长期存活的对象,年龄超过阈值的。(无法改变的)
3是无法避免的,但是1,2我们可以避免。
解决1得办法就是:开发中尽量少new大对象。
解决2的办法就是:不让一批对象超过Survivor一半,把Survivor调大
内存分配有哪些原则
对象优先分配在 Eden
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
空间分配担保
Java 8的内存分代改进
从永久代到元空间,在小范围自动扩展永生代避免溢出
深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象
JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
为什么分年老代和新生代
1)新生代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from和to)。
2)老年代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
什么要分为 Eden 和 Survivor?为什么要设置两个 Survivor区
1.如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
2.Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
3.设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor
GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满
了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor spaceS1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。
如果没有 Survivor,Eden 区每进行一次 Minor GC,存活对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代内存空间远大于新生代,进行一次 Full GC 耗时间比 Minor GC 长得多,所以需要分为 Eden 和 Survivor。Survivor 存在意义,就减少被送到老年代对象,进而减少 Full GC 发生,
Survivor 预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活对象,才会被送到老年代。
设置两个 Survivor 区最大好处就解决了碎片化,刚刚新对象在 Eden 中,经历一次 Minor GC,Eden 中存活对象就会被移动到第一块 survivor spaceS0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0中存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为这种复制算法保证了 S1 中来自S0 和 Eden 两部分✁存活对象占用连续内存空间,避免了碎片化发生)
ava 堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
当 Eden 区空间满了, Java 虚拟机会触发一次 Minor GC,以收集新生代垃圾,存活下来对象,则会转移到 Survivor 区。大对象(需要大量连续内存空间✁ Java 对象,如那种很长字符串)直进入老年态;
如果对象在 Eden 出生,并经过第一次 Minor GC 后仍然存活,并且被 Survivor容纳话,年龄设为 1,每熬过一次 Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活对象进入老年态。
老年代满了而无法容纳更多对象,Minor GC 之后通常就会进行 Full GC,FullGC 清理整个内存堆 – 包括年轻代和年老代。Major GC 发生在老年代 GC,清理老年区,经常会伴随至少一次 Minor GC,比 Minor GC 慢 10 倍以上。
Eden和Survivor的比例分配
默认比例8:1
大部分对象都是朝生夕死。
复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。