jvm-内存模型
java代码经过编译后形成class(字节码)文件,jvm将class文件加载到内存运行。那么jvm是如何为类的实例分配内存的呢?这时候就需要了解jvm的内存模型
从图片可以看出,jvm内存大体分为四部分
- 堆内存 Heap
- 方法区 PermGen
- 栈内存 Stack
- 程序计数器 Program Counter (图片没有反映出)
- 堆内存
java Heap是jvm管理的内存中最大的一块,是被所有线程共享的一块区域。它里面主要是存放类的实例对象。堆内存可以细分为新生代(Young)和老年代(Old),默认情况下Young:Old = 1:2,
该比例可以通过 –XX:NewRatio 来改变。新生代又可以细分为Eden,From Survior, To Survivor,默认情况下 Eden:From Survior:To Survivor = 8:1:1,该比例可以通过–XX:SurvivorRatio来改变
-Xms:堆的初始化大小
-Xmx:堆的最大值
-XX:newSize : 新生代的初始化内存大小,应该小于-Xms
-XX:MaxnewSize:新生代的可分配的最大内存,应该小于-Xmx
-Xmn:对于-XX:newSize 和-XX:MaxnewSize同时分配,即-XX:newSize=-XX:MaxnewSize=-Xmn
-XX:PermSize: 永久代初始化大小,永久代不属于堆区域;jdk1.8之后变成了元数据MetaSpace
-XX:MaxPermSize:永久代最大值
- 逃逸分析与栈上分配
java堆区不是给实例对象分配内存的唯一选择,目前有种堆外存储技术,利用逃逸分析刷选出未发生逃逸的对象,然后避开堆区,直接在栈帧中分配内存。
逃逸分析:确定对象的作用域,当一个对象被定义在方法体内部后,它的受访问权限仅限于方法体内部,一旦被外部对象引用,那么这个对象就发生了逃逸;
如果这个对象没有被外部对象引用,jvm就会为它在栈帧中分配内存空间。
在栈帧上分配的对象,gc无须执行垃圾回收。栈帧会随着方法的调用而创建,随着方法的结束而销毁。因此栈帧上分配的对象所占用的内存空间会随着栈帧的
出栈而释放。
- 年轻代与老年代
对象优先分配在Eden区,如果Eden区没有足够的空间,则执行一次Minor GC;
大对象(需要大量的内存空间)直接分配在老年代;
长期存活的对象进入老年代,jvm为每个对象定义了一个年龄计数器,如果对象经过了一次Minor GC,那么对象会进入Survivor区,之后每经历一次Minor GC,那么对象的年龄+1
直到达到一定的阀值进入老年区;
- Minor GC与Full GC
Minor GC:新生的对象一般都放在Eden区,因为它们很快变得不可到达,当对象从Eden区消失时,就是发生了一次Minor GC
Full GC:老年区长期存活的对象消失的时候,就是发生了一次Full GC
发生GC的时候,系统会产生一定的停顿,整个应用程序会被终止,直到GC完成
- 引用计数法与根搜索算法
引用计数法:对于一个对象A,只要任何一个对象引用了A,则A的引用计数器+1,当引用失效的时候,引用计数器-1。当A的引用计数器为0的时候,则标记为死亡对象,等待GC回收。
但是引用计数法有个很严重的问题,无法处理循环引用的问题。即当A对象引用了B对象,B对象也引用了A对象;除此之外,没有任何对象引用它们。A和B应该是被回收的垃圾对象,
但是引用计数法无法识别
根搜索算法 :按照根对象(GC Root)为起点,按照从上到下的方式,搜索被GC Root所连接的目标对象是否可到达。如果目标对象不可到达,那么该对象就是被标记为死亡对象,
等待GC回收
GC Root 组成:
1.java栈中的对象引用
2.本地方法栈中的对象引用
3.运行时常量池中的对象引用
4.方法区中的静态属性的对象引用
5.与某个类对应的Class对象
如图所示,user3,user5从GC Root不可达到,因而会被标记为垃圾对象。
- 垃圾回收算法
标记清除算法:分为标记阶段和清除阶段。标记阶段,标记所有由GC Root触发的可到达的对象,此时所有未被标记的对象就是垃圾对象;清除阶段,清除未被标记的垃圾对象。标记清除算法会导致内存空间碎片过多,内存空间不连续
复制算法:将内存空间分为两块,每次只使用其中一块,在垃圾回收的时候,将内存中存活的对象复制到另一块内存空间中,之后清除内存中的垃圾对象,最后互换两个内存空间的角色。该算法的缺点是将内存空间折半,浪费内存空间
标记压缩算法:分为标记阶段和压缩阶段。标记阶段,标记所有由GC Root触发的可达到的对象;压缩阶段,将所有存活的对象压缩到一块(减少空间碎片),之后清理其它空间的垃圾对象
标记清除算法会产生空间碎片,但是不会移动对象。比较适合存活对象较多的情况;复制算法需要将内存空间折半,并且需要移动存活对象,但是不会产生空间碎片。比较适合存活对象较少的情况;标记压缩算法在标记清除算法上做了优化,减少了空间碎片
- 分代思想
根据jvm内存的不同区域采用不同的垃圾回收算法
新生代:存活对象较少,适合复制算法,只需复制少量对象就可以完成垃圾回收,并且不会产生空间碎片;新生代分为Eden区,From Survivor区,To Survivor区,默认大小比例是8:1:1.垃圾回收时,将Eden区和Survivor区的存活对象复制到另一块Survivor区域,之后清理Eden区和使用的Survivor区中的垃圾对象。这里为什么内存空间不是按照1:1的比例来划分的呢?根据IBM公司大量研究表明,新生代中98%都是朝生夕死的,移动的存活对象很少。因此设置Eden:S0:S1 = 8:1:1 ,提高内存的利用率 注意:S0和S1每次只有一个内存空间是可用的
实验:jvm参数设置 -Xms20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=2 堆初始化内存20m,新生代10m Eden:S0:S1 = 2:1:1 打印GC日志如下
Heap
PSYoungGen total 7680K, used 4309K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 5120K, 84% used [0x00000000ff600000,0x00000000ffa35660,0x00000000ffb00000)
from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)
to space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
ParOldGen total 10240K, used 0K [0x0000000088600000, 0x0000000089000000, 0x00000000ff600000)
object space 10240K, 0% used [0x0000000088600000,0x0000000088600000,0x0000000089000000)
Metaspace used 3348K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 363K, capacity 388K, committed 512K, reserved 1048576K
total(可用内存)=7680K eden=5120K from=2560K to=2560K OldGen=10240
total=eden+from或者total=eden+to 因此同一时间from space和to space只有一个区域是可用的
老年代:存活对象较多,适合标记清除算法或者标记压缩算法,因为不需要移动大量对象
- 方法区
方法区又称为永久代,主要存放加载的类的信息(class对象,从class能反映出类的结构,包括类的方法,属性,注解等)和类的静态变量,静态方法。方法区是被所有线程共享的一片区域。jdk1.8后,移除永久代PermGen,而是称为元数据MetaSpace。
- 栈内存
栈内存分为本地方法栈(Native Stack)和虚拟机栈(JVM Stack),栈内存是非线程共享的区域。
JVM Stack:每个方法被执行的时候都会创建一个栈帧,栈帧里面存储方法里面的局部变量,方法的参数,方法的出口等信息。每个方法从被调用到执行完,对应着一个栈帧从入栈到出栈的过程。
Native Stack:和JVM Stack类似,Native Stack是为java 中的native方法服务的。
总之如果调用方法就会涉及到本地方法栈,虚拟机栈等结构
- 程序计数器
程序计数器的作用是保存当前线程正在执行的方法。如果该方法不是Native,那么它保存的是jvm正在执行的字节码指令地址;如果该方法是Native,那么它保存的是空。
每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。
参考资料:
https://www.cnblogs.com/chanshuyi/p/jvm_serial_05_jvm_bytecode_analysis.html
https://www.cnblogs.com/rinack/p/9888692.html