众所周知,在Java中类的编译以及加载等操作都是由JVM完成的.
那么到底是什么样的一系列变化,使我们在代码中可以轻松的直接new出一个对象呢?
编译->(装载->验证->准备->解析->初始化)->使用->卸载
源代码文件.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> JVM字节码文件*.class*
这个阶段主要的目的就是源码文件编译成JVM可以解析的类文件(java文件转成class文件)
会经过词法分析,语法分析,语义分析,生成字节码几个阶段.
最后剩成的JVM字节码文件,使用命令“javap -c test”可以查看test.class的字节码信息,主要包含三项内容:
常量池: 存放着字面量(文本字符串,final的常量)和符号引用(类和接口全限定名,字段名称和描述符、 方法名称和描述符,方法句柄和方法类型,动态调用点和动态常量)
访问标志: 访问权限信息(public ,final ,abstract)。
类索引: 类索引、父类索引、接口索引(类的全然限定名)。
字段表集合: 用于描述接口或者类中声明的变量信息(修饰作用域、实例变量还是类变量,是否可变、是否可序列化、并发可见性、字段类型、字段名)
方法表集合: 用于保存描述方法的信息(方法的名称,访问权限,返回值,参数类型等信息)。
指令码: 方法体中的代码经过编译后,最终变成了可执行的系统指令码保存在Code属性中,Code是属于方法集合中的一个属性。
简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
这里有两个重点:
注:为什么会有自定义类加载器?
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456
将常量池内的符号引用替换为直接引用的过程。
两个重点:
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
方法区属于逻辑区域,JVM规范指定方法区是用于保存已经被虚拟机加载的类信息、类变量等数据, 此区域属于线程共享区域。
栈这部分区域主要是用于线程运行方法的区域,此区域属于线程隔离区,每一个线程创建后都会申请一个自己单独的空间来运行方法,每个方法运行时候会创建一个栈帧,栈帧存储着方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每调用一个方法都会生成一个新的栈帧,调用方法就是一个压栈和出栈的过程,遵循先进后出的原则。
由于java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制,本地方法栈和虚拟机栈功差不多,区别在于本地方法栈是虚拟机调用native方法时使用的。
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,程序计数器是记录某个线程当前执行指令的位置,此区域属于线程隔离区。
堆内存主要是用来存放我们运行过程中创建的对象数据,此区域属于线程共享区。
1、大部分对象朝生夕死
对于JVM而言,大部分对象都是属于一个朝生夕死的状态,这部分对象随着方法的调用而创建,方法的结束而消亡,只有少部分的对象会长久的留在JVM 内存中。
2、大对象创建和回收比较耗时
对象本身创建就需要经过内存申请、初始化等一系列的操作,然后回收对象的时候需要对其进行标记清理,而对象越大这个过程需要的时间也就越长。
基于对象朝生夕死的特性,一般情况对象创建都会放到新生代中,只有经过一定次数的GC后还没有被回收的对象,我们认为这部分对象在未来也会长时间存在,所以会把这部分的对象转移到老年代的区域中去。
另外基于对象的大其创建和回收越耗时的特性,我们的大对象创建后就会被分配到老年代,避免大对象频繁的创建和回收造成性能损耗。
对于新生代区域包含Eden区,Survior区
Survior区包含Survior0,Survior1区.
新生代中的对象属于普通对象(未满足GC次数).符合这一条件的直接会在新生代中存活.
但是我们了解到上面新生代的分区逻辑,是为了解决空间碎片问题:
经过GC后的内存中会有很多不连续的内存空间(空间碎片),这些不连续的内存空间会让我们新创建的对象内存申请变得麻烦,每次申请内存前JVM都需要根据对象的大小去寻找合适的空间。
一次一次GC,保证能放置的内存碎片足够用,会一次次的迭代,Eden区,Survior0区,Survior1区,便是为了保证内存碎片的完整性而存在,一次一次移动从Eden到Survior0再至Survior1区的复制移动,来保证Eden区中的碎片完整性.
从新生代中的对象经过一定次数的GC或者新创建的大对象会进入老年代区.