Java运行三部曲:编写,编译,运行
编写:硬件编写代码,就是我们写代码
编译:javac将文件编译成一个或多个.class文件
编译中间的过程主要是类型和格式的检查。
如: Person.java ->词法分析器-〉语法分析器-〉语义分析器-〉字节码生成器
字节码包括class文件相关信息,java源码中的声明和常量信息(元数据),源码中的语句和表头
JVM和Java语言本身没什么关系,JVM只和class文件有关系,其他语言也能具有class文件。
运行:运行字节码文件
加载:将类文件中的字节码加载到JVM内存中
解释执行:逐行执行
类加载器:类加载(类加载)是一种机制,描述的是将字节码以.class文件的形式加载到内存,再经过连接、初始化后,最终形成可以被JVM直接使用的java类型的过程,这个过程只有在运行期才会触发,所以会牺牲一些程序启动的性能,但是却可以提供更高的灵活度。
类加载必经的五个过程:
加载,连接(校验,准备,解析),初始化,使用,卸载。
可以使用-verbose:class方法查看ClassLoader加载过程。
Class是所有类的模板
类是实例的模板
加载loading
概念:加载过程就是将class文件中的字节码‘bytecode’一行一行读入JVM内存中的方法区,但不会执行。
通过类全名来获取定义这个类的二进制字节流,这部分工作由类加载器完成。
将这个字节流所代表的静态存储结构,转化成方法区中运行时数据结构。
在堆中生成一个对应的‘java.lang.Class’对象,作为方法区中这个类的各种数据的访问入口。
JVM在加载数组的时候为了提高效率,减少重复加载(数组中类型统一且固定),加载的是数组的类型,多维数组也是递归加载类型,直到发现非数组类型停止,而数组的创建由JVM直接完成。
提示:基本数据类型和引用数据类型的加载过程没有差别,因为在编译阶段,基本数据类型就会被封装成对应的包装类。
int a ----方法区中 :Integer class
连接linking
概念:连接又分为三个步骤:验证、准备和解析
验证(Verification):检查被加载的类是否有正确的内部结构,包括代码是否正确,类与类之间的关系是否合理等,这与编译时期的检查不同。
准备(Prepanation):为类的静态成员(包括属性和方法)分配内存,并设置初始值(比如null、0、false等).
解析(Resolution):将类的二进制数据中的符号引用(字面量)替换为直接引用(内存地址)。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,在class文件中表示我和你的关系,比如全路径名,方法名称等,只要是能无歧义地定位到目标即可。符号引用与JVM内存无关,引用的目标不一定加载到内存中,在Java中,一个类将会编译成一个class文件,在编译时,java类并不知道所引用的类的实际地址,只能用一些符号来替代,各种JVM实现的内存布局可能不一样,但是它们能接受的符号引用是一致的,因为符号引用的字面量明确定义在JVM规范的class文件格式中。
直接引用可以是直接指向目标的指针,比如指向java.lang.Class,指向静态方法,指向静态属性等,还可以是相对偏移量,比如指向属性变量,属性方法等,还可以是一个可以间接定位到目标的句柄。直接引用和JVM布局是相关的,同一个符号引用在不同的VM上翻译出来的直接引用一般会不相同,如果有了直接引用,那引用的目标必定已经被加载到内存中了。总之,直接引用是在JVM中使用内存地址的形式来表示我和你的关系。
例如:
private static String name
属性和局部变量区别?
属性有初始值,不赋值可以使用。
局部变量没有初始值,不赋值不可以使用
总结:
A.java通过javac编译之后,会形成A.class和B.class(存在于硬件中)
变量b中存的到底是什么?是一个符号“我引用了com.joe.pojo.B类"
类运行时,A和B才被加载到内存中,A分配到内存地址为ex0011,B分配到内存地址为ex0012
B类有了真正的内存地址后,再将之前的符号引用"我引用了com.joe.pojo.B类"转成直接引用ex0012,这个过程就是符号,、引用转成直接引用的过程。
类加载过程
加载:在JVM的方法区中,创建一个对应的instanceKlass区域,然后将class文件中的字节码内容逐行加载对应的instanceKlass中,然后在堆中创建一个对应instanceKlass的Class对象(称为instanceKlass的Java镜像)中。
连接-校验:检查class对象中的代码是否正确。
连接-准备:为类的静态属性和方法分配内存,并设置初始值。
连接-解析:将class对象中的符号引用转成直接引用。
初始化:为类的静态属性赋真正的值,执行静态块。
类加载的触发条件
假设我是一个类,那么:
有人new我的时候,我会被加载,连接,初始化。
有人访问我的静态成员(属性和方法)的时候,我会被加载,连接,初始化。
有人反射,克隆,反序列化我的时候,我会被加载,连接,初始化。
一个类在每次被使用前,都会先检查是否被加载,连接,初始化过:
如果已经被加载,连接,初始化过,则直接使用。
如果仍未被加载,连接,初始化过,则先去执行加载,连接,初始化。
实例创建方式:
类加载完成之后,才允许根据类来new对应的实例,在new的时候会使用到方法区中对应instanceKlass对象的数据信息。
new的过程就是为实例分配一块连续的堆内存空间过程,方式有两种:
指针碰撞:分配内存空间包括开辟一块新的内存和移动指针两个步骤。
空闲列表:分配内存空间包括开辟一块新的内存和修改空闲列表两个步骤。
-new实例的过程是非原子的,可能会出现并发问题(两个线程争抢到同一块内存),JVM采用CAS乐观锁的方式来解决:
-赵四为某个实例进行new操作,申请到一块内存0x9527,开始操作,查看版本号为000.
-刘能为某个实例进行new操作,申请到同块内存0x9527,开始操作,查看版本号为000.
-赵四操作完成,再次查看版本号,仍为000,提交操作,将版本号更改为001。
-刘能操作完成,再次查看版本号,发现版本号变为了001,放弃这次操作,去申请其他内存。
-最终结果永远不会有两个线程申请到同块内存。
除了CAS乐观锁的方式可以解决并发问题,JVM还提供了一种本地线程额缓冲内存的方式,即每个线程在Java堆中都先提前分配一小块内存区域,称为本地线程分配缓冲T-LAB,各线程给实例分配内存时会在该线程的T-LAB上进行分配,这样各个线程即可互不影响。
扩展——悲观锁:正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
实例储存模式:
概念: 在JVMN中,实例在内存中的存储也是很有规律的,存储的布局可以分为三块区域:
(1)对象头区:object Header
存储对象的原数据信息,包括对象运行时数据和类型指针(指向方法区的类型实例)
存储Mark Word,包括对象自身的运行时数据,如它的哈希码和它在GC中分代的年龄、锁状态表示,线程持有锁,偏向锁ID,偏向锁时间戳等信息。
如果是数组对象,还多了一个数组长度。
(2)实例数据区:Instance Data
存储真正有效的数据,即在程序中定义的各种类型的字段数据,这部分数据有一部分是从父类中继承下来的,也有在子类中定义的,总之都要被记录下来。
各字段的分配策略为long/double,int、short/char,byte/boolean,相同宽度的字段总是被分配到一起,便于之后取数据。
(3)对齐填充区:Padding
对齐填充并不一定是必然存在的,因为HotSpot虚拟机内存管理的要求是给实例分配内存的大小必须是8字节的整数倍,所以不够的部分需要填充。又因为对象头部分正好是8字节的倍数,所以对齐填充实际上补全的是实例数据区域,对齐填充的数据并没有特殊的含义,仅仅是起到填充占位符的作用
实例调用方法
*概念:*在不同的虚拟机中,对象的访问方式也是不同的,主流的访问方式有使用句柄和直接指针两种。
(1)使用句柄:是一种间接使用指针访问实例的方式,因为它需要先在Java堆中划分出一块内存区域作为句柄池。栈中的变量存储的是句柄池中稳定的句柄的地址,而句柄中包含的才是Java堆中的实例和对应的java.lang.Class各自的具体地址。
-使用句柄访问方式的最大好处是栈中的变量存储的是稳定的句柄地址,在实例被移动时只会改变句柄中的类型数据指针,而栈中变量本身不需要被修改。
-直接指针:栈中的变量直接存储的是实例的地址。
-使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的的时间开销。- HotSpot实现中采用的是本方式。
java底层原理
最新推荐文章于 2024-09-23 22:32:02 发布