我们从java文件讲起
java文件经过编译编程class文件
class文件的结构主要就是将原本java里面的代码信息按jvm读取的格式进行编排,
这里按顺序依次介绍下class文件结构,
首先是coffeebabe魔数,
然后是次版本号,主版本号,
常量池个数,常量池,常量池里面主要是字面量和符号引用,字面量简单理解就是我们代码中中赋给变量的字符串值,而符号引用就是类,方法等的全路径名称,方便后面加载进内存后可以定位到具体指令信息的内存位置。
修饰符,即当前类的修饰符,用的是十六进制表示,jvm中会有对应的映射可以根据多个修饰符的十六进制累加得到相应的结果
全类名,父类名,接口个数
字段个数,字段表,这里就主要存放了字段的修饰符,字段类型,字段名等信息
方法个数,方法表,方法表主要存的就是每个方法的修饰符,返回值类型,方法名称,方法参数,方法相关属性,比如code属性,存放的就是方法中的代码编译完后对应的指令集,linenumber属性,指令对应的代码行号等
属性个数,属性表,存放的就是一些类的属性,比如sourcefile,类对应的文件路径等
此时,我们已经是获得了class文件,接下来就是类加载器来加载这些class文件,
首先得启动类加载器,这是在java命令执行的时候就会启动jvm,jvm负责创建boostrapClassLoader,然后创建launcher类,
Launcer类负责创建ExtendedClassLoader,然后创建ApplicationClassLoader,将扩张类加载器的的引用赋值给应用程序类加载器的parent属性,
此时,类加载器已经创建完毕,那么就开始进行类的加载了,首先是boostrapclassloader加载jdk核心的类,比如String这些,然后由ExtendClassLoader加载扩展类,也就是ext文件夹下的相关类,最后才是由ApplicationClassLoader应用程序类加载器加载我们应用程序的类,也就是咱们上文提到的编译完的class文件。
类加载器会将class文件加载到方法区中的元数据区,然后在堆中创建对应的Class对象供外部接口访问对应元数据区中的数据
此时,就已经完成了类的加载,就可以执行代码了,重点讲解下对象的创建流程,
当执行new指令的时候,虚拟机会在堆中为对象创建指定大小的内存空间,并设置对象头,实例数据,字节填充。对象头主要包含MarkWord,动态链接,数组大小(如果是数组的话),MarkWord主要保存的是对象的年龄,锁信息等对象相关的信息。
接下来是执行对象中的方法,每个执行线程都有自己的栈,本地方法栈,程序计数器的内存空间,
对象的方法会以栈帧的结构进行压栈,栈帧主要包括了局部变量表,操作数栈,动态链接,方法返回地址,举个简单加法操作在栈帧中的操作,首先会将定义 的变量先存放到局部变量表,然后执行取操作数方法,放进操作数栈,当执行加法操作的时候会将操作数栈顶两个元素弹栈并相加后将结果再压如操作数栈,再返回。方法返回地址主要是用于记录当方法中调用方法时,当调用的方法返回后,原本的方法的执行位置
本地方法栈就是执行本地方法是需要用到的栈空间
程序计数器主要用于记录当前线程执行当前方法执行到哪一条指令
至此,我们已经基本介绍完了从类加载到运行时内存数据区的交互过程,最后就是有些对象执行完后不需要了的垃圾回收问题
首先是怎么判断该对象不需要了的方法,大体有两个,一个是引用计数,一个是可达性分析
引用计数就是当对象被引用到的时候在对象头的记录数加1,当被置空也就是释放的时候减一,0的时候就表示没有引用,但是这个的问题是当两个对象存在相互引用的时候会产生循环引用导致无法回收,
可达性分析就是从gcroot进行迭代分析引用到了哪些对象。这些被分析到的对象即为存活对象,没被分析到的就是垃圾对象
判断是否存活后就可以进行垃圾回收了,垃圾回收首先应该有相关的算法,这里主要是四种垃圾回收算法
分代收集理论,就是根据对象的年龄分为新生代,老年代
标记复制算法,就是通过可达性分析标记出存活的对象后,将这些对象复制到另一块内存空间,然后将原来分配内存的空间全部释放掉。
标记清除算法,就是通过可达性分析标记出存活对象后,将没标记到的对象进行清除,会产生内存碎片
标记整理算法,就是通过可达性分析标记出存活对象后,将标记对象往首端挪动,使所有的存活对象都整齐紧凑地排列在一起,再将后面的所有内存空间清空回收.
讲完了垃圾收集算法,接下来说下实现这些算法的垃圾收集器,
首先讲下经典的几款垃圾收集器
Serial,SerialOld顾名思义单线程收集器,是安全点开启单线程进行可达性分析,年轻代使用标记复制算法,老年代使用标记整理算法
Parallel ,ParalleOld顾名思义,并行垃圾回收器,跟Serial只不过,回收期间开启多线程进行并发回收
ParNew + CMS ,ParNew其实是CMS版本的年轻代Paralle垃圾回收器,也是并行回收,只是专门用来配合CMS老年代回收 用
CMS分为初始标记,并行标记,重新标记,并行清理,重置标记
这里的标记用到三色标记法,白色,灰色和黑色,白色标识没被分析过,灰色标识分析过,但是被分析的对象还有部分引用没分析完,黑色 标识已经分析完该对象和其涉及到的引用,不会再分析
初始标记就是标记gcroot对应的直接引用,
并行标记就是从gcroot的直接引用开始可达性分析,这里涉及到增量更新的概念,就是在并行标记过程中,当黑色对象新产生了新的引用,那么将这些引用记录到remeberset中
重新标记,就是将在并发标记过程中产生的remeberset再遍历分析一遍
并行清理,清理掉白色对象,其实就是垃圾对象
重置标记,将剩余的所有对象的三色标记重置.
另外再介绍下比较新的G1和ZGC垃圾收集器
G1引进了region的概念,有Eden,suvivor,old,和humomous(专门用于存储大对象)
初始标记,也是标记gcroot的直接引用
并发标记,对gcroot的直接引用进行可达性分析,这期间涉及到SATB历史快照的概念,会将标记期间产生的涉及到分析过的对象的新的引用记录到rememberset中
重新标记,对rememberset中的对象都标记为存活对象
筛选回收,这是G1能提供回收时间的控制机制,它通过维护一张表,记录回收每一个region的收益比,并通过收益比的计算排序,每次根据设置的回收时间,只回收最高收益比的一部分region,达到回收时间可控制的效果
ZGC垃圾收集器
region的概念不再划分老年代,年轻代,只有大中小三种region
这里的标记涉及到颜色指针,就是将原本在对象头上的标记改为标记在指向对象的指针上,因为根据当前内存的有限性,我们64位的指针,只需要分配42位给内存标识就已经足够了,在高22位取其中的低4位作为标记位,标识是否被分析过,是否是已经重分配的对象等信息,这里也涉及到了读屏障,就是在读取该对象之前先对颜色指针进行分析
标记阶段分为
初始标记,也是对gcroot的直接引用进行标记
并发标记,对gcroot的直接引用进行可达性分析
并发重分配预备,就是筛选出要回收的region
并发重分配,将要回收的region里面的存活对象复制到另外的空白region里面,并 把新地址记录的映射记录在当前region中的rememberset中,并修改下指针的状态
并发重映射,就是将颜色指针中有被移动的对象的指针更新为最新的对象地址
但是因为颜色指针有自愈的特性,也就是当要读取颜色指针对应的对象的时候会读屏障先判断下该对象是否重分配过,如果是,那么他会去颜色指针当前指向的region的rememberset集合中获取最新的对象的位置地址,然后再去最新的地址获取对象,所以并发重映射的阶段会放在并发标记一起,因为并不急于把颜色指针都更新为最新的地址。
讲完了垃圾收集器,我们最后讲一讲调优
最常用的调优指令有jmap,jstat,jstack,jinfo
jmap -heap可以dump出当前jvm堆的信息,可以分析内存占用高时是哪些具体的类占用的,可以根据调用堆栈定位到代码
jstack可以拿到当前所有线程的运行状态,可以分析根据jsp拿到进程id,然后通过top +H命令定位到占用cpu高的线程id的十进制,然后将十进制转换为十六进制,在jstack中可以定位到该十六进制 线程的调用堆栈,从而定位到代码
jstat -gc主要统计gc回收的频率和时间,及各个区的使用情况,当然,也可以通过打印gc日志看看gc的回收情况
此外我们可以通过jdk提供的可视化工具进行分析,比如jvisualvm
也可以通过阿里的arthas进行分析,使用详见官方文档.
到此为止,我应该已经是一口气讲完了jvm的大概。还有一些细节点没办法面面俱到点到,不足之处,敬请谅解.