Java虚拟机内存模型
1、New出来的对象优先放在堆里,也有一些是放在栈里,但一般都是放在堆上;
2、栈里分为很多个栈帧内存区——线程运行每个方法时,虚拟机会给每个方法分配一个专门的内存区域,用来存放方法中的局部变量(与数据结构中的栈一样,先进后出(FILO)),如下图:
3、在栈里面,先调用的方法会后释放资源,后调用的方法先释放资源;
4、Java官方提供了一份jvm字节码手册,可以对照.class文件的内容进行查询;
5、javap命令,使用-c参数可以对.class字节码文件进行反编译为人类可读的jvm指令码,可以参考JVM指令手册进行阅读;
6、局部变量表中维护了方法中的局部变量信息,其中下标为0的是this变量,下标为1的是在该方法中定义的第一个变量,下标为2的是在该方法中定义的第2个变量……以此类推;
7、在方法中定义一个局部变量并赋值时,所赋的值,会先作为常量压入操作数栈,然后再存入对应的局部变量(局部变量表);
8、每个方法开始执行时,java虚拟机不光会分配一块专属的栈内存区域,还会分配一块专属的程序计数器内存区域;
9、程序计数器用来记录JVM指令码当前执行的行号,即JVM指令码的行号,也可以理解为当前正在执行的JVM指令码的内存标识位置,之所以给每个线程分配独立的程序计数器内存区域,主要是因为系统底层会存在中断操作,为了保存执行现场;
10、每次做运算,其实都是和操作数栈在做交互;
11、操作数栈其实就是程序运行过程,代码中的操作数临时存放的一个中转空间;
12、栈里面方法出口(返回地址)其实就是调用函数(上级函数)在调用被调用函数(当前函数)时,JVM会记录被调用函数执行完成后,需要返回到调用方法的位置;
13、如果某个函数里的某个局部变量是一个对象,则其对应的局部变量表中存放的是该对象在堆上的内存地址,而不是对象本身;
14、方法区主要包括:常量、静态变量、类信息(字节码文件通过解析、加载等过程之后的内容);
15、如果静态变量是对象的话,那么方法区中的静态变量的值存放的是该对象在堆上的内存地址,而不是对象本身;
16、本地方法栈用于存放native方法(本地方法)所需要的内存空间,也是每一个线程独享的;
垃圾回收机制
1、堆内部的结构如下图所示,堆没有为每个线程分配内存空间,而是共用的,堆内部分为两大区域,分别是年轻代区(默认占堆大小的1/3)和老年代区(默认占堆大小的2/3),其中,年轻代又细分为新生代区(Eden,伊甸,占年轻代大小的4/5)、From区(占年轻代大小的1/10)和To区(占年轻代大小的1/10);
2、当第一次Eden区装满时候,JVM会触发minor GC机制,对Eden区进行垃圾回收;
3、判定对象是否为垃圾对象,用到的是“可达性分析算法”,其中有一个GC Roots的概念,即先找到所有的程序中局部变量、静态变量、成员变量等变脸,然后按照其引用关系,一层层的往下找,直到找到没有引用其他变量时结束(如A对象里面的属性是B对象,B对象的属性是C对象等),这样形成的一个链条叫GC Roots对象链,凡是在对象链上出现过的数据都是非垃圾对象,非垃圾对象会被复制到Survivor区的From区,剩下的没有出现在GC Roots对象链上的对象就是垃圾对象(例如某进程已经运行完了,其有一个局部变量是对象,这个成员变量已经被销毁了,此时在堆上的对应的对象没有被任何进程的成员变量引用,所以就是不会出现在任何GC Roots对象链上,则就是垃圾对象),完成复制动作后,Eden区被清空;
4、对象从Eden区复制到From区时,对象的分代年龄会由0变成1;
5、当第二次Eden区装满的时候,还会触发minor GC机制,不过这次会对Eden区和Survivor区统一进行垃圾回收,同时将Eden区和From(S1)区的非垃圾对象复制到To(S2)区域,然后对相应对象的分代年龄加1,即,将Eden区和From(S1)区清空;
6、第三次Eden区装满的时候,继续触发Minor GC机制,对Eden区和Survivor区统一进行垃圾回收,同时将Eden区和To(S2)区的非垃圾对象复制到From(S1)区,然后对相应对象的分代年龄加1,即,将Eden区和To(S2)区清空;依次类推,在内存回收时,每次都会清理Eden区,但是From(S1)区和To(S2)区是被轮流清理的,每次清理时的非垃圾对象,会被轮流的复制到From(S1)区和To(S2)区(即第一次复制到From(S1)区,第二次复制到To(S2)区,第三次再被复制到From(S1)区),且每复制一次,对象的分代年龄都会自动加1;
7、当某个对象的分代年龄被加到15(这个配置可以修改)时,该对象会被复制到老年代区,JVM参数-XX:MaxTenuringThreshold可以设置分代年龄是多少时被复制到老年代;
8、当某个对象是大对象(字符串、数据等)时,该对象会直接进入老年代,不会进入年轻代,JVM参数-XX:PretenureSizeThreshold可以设置大对象的大小,当对象超过设置的值时,就会认为是大对象;
9、当一批对象被复制到Survivor区,并占Survivor区(目标区域S1或S2,看挪到哪个区域就和哪个区域比,而不是整个Survivor区)大小的50%以上,那么此时大于等于这批对象年龄最大值的对象,会被复制到老年代(对象动态年龄判断机制),JVM参数-XX:TargetSurvivorRatio参数可以设置占Survivor区的大小为多少时触发对象动态年龄判断机制;
10、当老年代装满时,JVM会执行Full GC,Full GC的机制和minor GC机制类似(也是用“可达性分析算法”找出垃圾对象),只不过Full GC会对整个堆进行垃圾收集(包括老年代和年轻代),当使用Full GC后,老年代仍然是满的话,就会报内存溢出错误;
11、在Java程序运行时,可以通过命令行,使用jvisualvm命令打开JVM诊断工具,该工具会自动识别本地所有正在运行的JVM进程,该工具可以安装一个Visual GC插件,用来查看GC执行过程;
12、当JVM对某个Java进程在做GC(包括Minor GC和Full GC)时,会STW(stop the word),暂停该进程内所有的用户线程;
13、JVM调优的主要目的就是减少GC次数,减少STW次数,提高程序运行速度,因为Minor GC引发的STW时间非常短,Full GC引发的STW时间比较长,所以最主要的目的是减少Full GC的次数,及一次Full GC的时间;
14、正常的JVM系统,一般几小时、几天,甚至几周才做一次Full GC操作;
15、在实际的系统中,先估算进程每秒产生的堆上的大小,及这些数据的生命周期(即经过多长时间后,这些对象会变成垃圾对象,或者这些对象没用了),然后根据上面的回收机制规划年轻代和老年代的大小,需要注意的是:
(1)尽量减少Full GC的次数;
(2)尽量杜绝对象动态年龄判断机制的触发,即尽量不要让对象直接进入老年代,让其一直在Eden区和Survivor区;
(3)尽量让对象在年轻代被GC,而不要进入老年代。