JVM类加载机制_字节码执行引擎_Java内存模型

6 篇文章 0 订阅

类加载机制:

类加载生命期:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸载(Unloading)
	初始化:
		1.遇到new,getstatic,putstatic,invokestatic指令,类没有进行初始化,先触发初始化
		2.java反射机制
		3.初始化一个类,父类没有初始化,需要先触发父类的初始化
		4.JVM启动的时候,用户需要指定一个执行的主类(main所在的位置),JVM优先初始化
		5.JDK1.7动态语言支持
		注:
			1.只有直接定义静态字段(static int i=1;)的类才会被初始化,而(static{sysout("Hello")})并不会触发初始化
			2.类在初始化的时候必须要求其父类初始化,但是接口不需要,只有真正使用到父类接口,父类接口才会初始化
	加载:
		1.通过一个类的全限定名来获取定义此类的二进制字节流//因此可从jar,war,网络获取类结构
		2.将这个字节流所代表的静态结构转化为方法区的运行时数据结构
		3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的访问入口
		注:类加载的过程可以通过系统的类加载器来完成,也可以自定义类加载器控制字节流获取方式
		加载阶段完成之后,类的字节流文件按照JVM的格式(格式由JVM定)储存在方法区中,然后在内存中实例化一个java.lang.Class对象对于HotSpot来说,该对象存放在方法区中,这个对象将作为程序访问的方法区数据类型的接口
	验证:
		确保Class文件的字节流中包含的信息不会损害JVM
		1.格式验证:是否以魔数开头,主次版本号,常量池中是否有不支持常量的类型,编码情况,Class文件是否具有附加信息.....
		2.元数据验证:检查是否符合Java语言规范
		3.字节码验证:验证类型转换
		4.符号引用验证:字符串限定符,类字段,方法的限定符
	准备:
		为类变量(静态变量)分配内存,设置初值(默认0/false/null),实例变量的初始化跟随对象一起分配到Java堆中
	解析:将符号引用(通过一组符号描述引用目标,符号引用的实现与内存布局无关)替换为直接引用(直接指向目标的指针/句柄)
		解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄,和调用限定符7类符号引用进行,分别对应常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info,CONSTANT_MethodType_info,CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7中类型
	字段解析,类方法解析,接口方法解析
	初始化(类加载过程最后一步//初始化阶段的过程就是执行类构造器<climit>()方法的过程):
		<climit>()方法是由编译器自动收集类中所有变量的赋值动作和静态语句块(static{}块)中的语句合并产生,编译器收集的顺序是由语句在源文件中出现顺序决定
		例:
			public class Test {
				static{
					i = 0;//静态代码块中的i可以被赋值,但不能被访问(非法向前引用)正常编译
					System.out.println(i);//报 illegal forward reference(非法向前引用)
				}
				static int i = 1;//如果要访问i,应该将i在静态代码块前面进行声明
			}
		<climit>()方法与类的构造方法(<init>()方法)不同,它不需要显示地调用父类构造器,虚拟机保证在子类的<climit>()方法执行之前,父类的<climit>()方法已经执行完毕,因此在JVM中第一个被执行的<climit>()方法的类肯定是java.lang.Object
		由于<climit>()先执行,所以静态语句块优先加载
		例:
			static class parent{
				public static int a=1;
				static{
					a=2;
				}
			}
			static class sub extends parent{
				public static int b=a;//此时b会被初始化为2
			}
		注:<climit>()对于类或接口来说,不是必须的,当类中没有静态语句块,没有对类变量赋值操作,编译器将不会生成<climit>方法
		接口中不能使用静态语句块,但可以有变量赋值操作,说明接口和类都一样会生成<climit>(),接口与类不同的是,接口中不需要先执行父类的<climit>(),只有使用父接口才会去初始化
		JVM会保证一个类的<climit>()在多线程的环境中被正确加锁,同步,多个线程同时初始化一个类,将只会有一个线程去执行类的<climit>(),其他线程阻塞,直到线程活动执行完<climit>()完毕,就此会造成线程阻塞问题(并且很隐蔽)
	类加载器
		双亲委派模型
			前序:
				启动类加载器(Bootstrap ClassLoader)//C++语言实现,JVM自身一部分,主要作用是将放在<JAVA_HOME>\lib目录中/被-Xbootclasspath参数指定的路径的类库(要被JVM识别)加载到JVM内存中
				扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,用于加载<JAVA_HOME>\lib\ext目录中/java.ext.dirs系统变量所指定的路径中所有类库
				应用程序加载器:加载用户类路径上指定类库
				最后加载器为自定义加载器
				上述加载器除Bootstrap ClassLoader统称为其他类加载器//由java语言实现,独立于虚拟机外部,继承于抽象类java.lang.ClassLoader
			双亲委派模型(强制性约束模型):一个类收到加载请求首先会委派父类加载器完成,直到请求传送到顶层启动类加载器中,只有当父类反馈无法完成加载请求,子类加载器才会尝试加载,保证程序的稳定运行
		OSGi环境下,类加载器不再是树状模型,而是网状,他会采用同级之间的类进行加载

字节码执行引擎

执行引擎:使用解释器执行,通过及时编译器产生本地代码执行
运行栈帧结构:
栈帧:支持虚拟机进行方法调用和方法执行的数据结构
栈帧储存的东西:
	方法的局部变量表:
		存放参数和方法内部定义的局部变量,编译Class文件,在方法的Code属性的max_locals确定了局部变量表的最大内容
		方法执行的过程,JVM使用局部变量完成参数值到参数变量列表的传递过程
		非静态方法,局部变量表中第0个索引的slot默认适用于传递方法所属的对象实例的引用(this),其余参数按照参数表的顺序排列//为节省空间,slot可以重用
		slot(Variable Slot,变量槽):
			局部变量表容量的最小单位(未说明具体占用空间大小),每个slot都可以存放一个boolean,byte,char,short,int,float,reference,returnAddress类型数据
			一个slot可以存放32位以内的数据类型
			reference表示对对象数据的引用:可以通过此引用直接或间接地查找到对象所属数据类型在方法区中的储存类型信息
			64位的数据类型,虚拟机会以最高位对齐的方式为其分配两个连续的slot空间
			Java中64位数据(reference可能是32位,也可能是64位)的只有long和double, JVM通过索引定位的方式使用局部变量表,索引范围从0开始,64位数据,则会同时使用n,n+1两个slot
			Slot重用:当方法体中的变量作用域未覆盖整个方法体,且pc计数器超出这个变量的作用域范围,为节省空间,Slot将会复用,但是复用也会影响到GC(当发现solt没有被其他其他变量复用,GC Roots还保持这引用之间的关联,那么,即使离开了变量的作用域范围,GC却不会回收内存),所以不使用的变量要手动设置null,避免GC错误
			注:局部变量必须赋初值,于类变量不一样
	操作数栈(操作栈):
		在编译时确定了栈最大深度,在方法的Code属性的max_stacks确定(栈深度永远不会超出该深度)
		操作数栈的每一个元素可以是任意数据类型,包含long/double
		32位所占的栈容量为1,64位为2
	动态链接:
		方法返回地址:
			在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行, 方法推出的过程,实际上就是等于把当前栈出栈,因此退出时的操作:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向调用指令后面的一条指令
		方法退出的方式:
			正常完成出口:调用者的PC计数器的值可以作为返回地址,栈帧中会保留这个计数器的值
			异常出口:返回值需要通过异常处理器来确定,栈帧中一般不会保留此消息
		注:方法重载是静态分派的典型应用(编译时确定对象),方法重写是动态分派的典型应用(运行时确定对象)
	方法调用:Class文件不包含传统编译的连接操作,一切方法调用直接是调用在Class文件的符号引用,而非方法实际运行时的内存布局的入口地址
		1.解析:类加载解析阶段,将其中一部分符号引用转化为直接引用,符合编译期可知,运行期不变---静态方法,私有方法(二者的隐蔽性,不能被重写,即不可实现多态)
		JVM内置方法调用字节码指令:
			invokestatic:调用静态方法;
			invokespecial:调用实例构造器<init>方法,私有方法,父类方法;
			invokevirtual:调用所有虚方法
			invokeinterface:调用接口方法,运行时确定实现接口的对象
			(分派逻辑由用户设定引导)invokedynamic:运行时动态解析调用出限定符所引用的方法
			注: invokestatic,invokespecial一定是调用静态/私有/构造/父类方法,类加载的时候就会将符号引用转化为直接引用(上述方法为非虚方法, 除上述方法,final方法其余方法都为虚方法),final方法不能被覆盖,就没有多态的说法
		2.分派:
			静分派:在编译时就确定引用所指的对象; 典型应用就是重载
			动态分派:与多态相关---重写
		3.JVM动态分派的实现:
			建立虚表结构:当子类没有对父类的方法进行重写是,子类虚方法表里的入口地址和父类的入口地址一致,如果子类重写父类方法,子类方法表中的地址将会替换为子类重写后方法的入口地址
		4.java.lang.invoke:
			java中不能将函数名作为参数(函数指针),只能通过;当Method Handle提出后,就可以将函数名作为参数
	JDK1.7提供的动态确定目标方法机制:MethodHandle;
		MethodHandle与反射的区别:
			反射实在java代码级别模拟方法调用,MethodHandle是在字节码级别
			MethodHandle的一部分方法对应的字节码指令执行的权限校验行为,Reflection API中无需关心
			Reflection是为Java语言服务,MethodHandle可以设计为服务于所有的JVM,当然也包含Java语言
		invokedynamic指令(静态解析--解析私有方法,静态方法两大类)
			每一处含有invokedymic指令的位置称作是动态调用点(Dynamic Call Site),这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是CONSTANT_MEthodrefDynamic_info常量,该常量中可以得到:Bootsrap method引导方法,有固定的参数,返回值为java.lang.invoke.CallSite,代表真正要执行的方法调用; Method Type(方法类型);方法名称
			静态分派--方法重载的过程中调用重载,形参会根据语义选择合适的方法
			动态分派--多态性体现(重写):由于invokevirtual在运行时确定接受者不同的类型,然后会将常量池中类方法符号解析到不同的直接引用上面,invokevirtual搜索接收者的方法时通过虚方法表(如果子类重写父类方法,表中的函数入口优先指向子类的方法,反之指向父类,接口也一致)

Java内存模型:

主内存:Java内存模型规定所有的变量都保存在主内存中

工作内存:每个线程都有自己的工作内存,保存了该线程使用到的变量的主内存副本拷贝

主内存与工作内存的关系:
	线程对变量的所有操作都必须在自己的工作内存中进行,不能直接读写主内存中的变量
	不同线程之间无法直接访问对方工作内存中的变量
	线程间变量值的传递均需要通过主内存来完成
	
内存交互操作(变量与主内存中间的操作,下列操作都是原子性):
	lock:将变量标识为一条线程独占状态,作用于主内存的变量
	unlock:将锁定状态的变量释放,释放后的变量才可以被其他线程锁定
	read:将变量值从主内存传输到线程的工作内存中
	load:作用于工作内存将读取后的变量放入工作内存的副本中
	use:将工作内存中的变量传递给执行引擎,每当JVM遇到一个需要使用的变量值的字节码指令就会执行这个操作
	assign(赋值):将一个执行引擎接收到的值赋给工作内存的变量,每JVM遇到变量赋值的字节码指令执行这个操作//作用于工作内存
	store(储存):将工作内存中的一个变量值传送到主内存中,以便write使用//作用于工作内存
	write:将store操作送过来的变量放入主内存的变量中
	注:
		read-load,store-write必须顺序执行,不需要连续执行(read a,read a,load b,load b),不可单独出现
		不允许一个线程丢弃它最近的assign的操作,即变量在工作内存中改变了之后必须把该变化同步到主内存中
		不允许一个线程无原因地把数据从线程的工作内存同步回主内存中
		一个新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量
		一个变量在同一时刻只允许一条线程对其lock操作,但lock操作可以被同一条线程执行多次,多次执行lock后,只有执行相同次数的unclock,变量才能解锁
		如果对一个变量执行lock操作,将会清空工作内存 中此变量的值,在执行引擎使用这个变量前,需要重新执行load/assign来初始化变量
		如果一个变量没被lock锁定,则不允许对它执行unclock,unclock也一致
		对变量执行unclock操作之前,必须把此变量同步到主内存中(执行store/write)
		
volatile(最轻量级同步机制)
	变量被声明为volatile,具有以下特性:
		(1) 保证可见性,不保证原子性
		 a.当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
		 b.这个写会操作会导致其他线程中的缓存无效。
		(2)禁止指令重排 
		 	重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
			a.重排序操作不会对存在数据依赖关系的操作进行重排序。
		        比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
			b.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
		      比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
		 (3) 使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
	   		a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
	   		b.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
			即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变
		量及其后面语句可见。
		重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
		加volatile后,变量会生成内存屏障(即lock锁住变量):
			 I.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内
			存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
			II. 它会强制将对缓存的修改操作立即写入主存;
			III. 如果是写操作,它会导致其他CPU中对应的缓存行无效。
			//保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,其他线程也会知道,Java的运算并非原子操作,导致volatile变量的运算在并发下是不安全的,即volatile只能保证可见性,所以通常还是需要synchronized/java.util.concurrent中的原子来保证原子性
		注:long double 64位数据不具有原子性,但是jvm为其读写操作进行了原子性处理,无需为long double 添加volatile
原子性:
		定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
		原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:
		a. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
		b.所有引用reference的赋值操作
		c.java.concurrent.Atomic.* 包中所有类的一切操作
可见性:
		定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
		在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性:
		定义:即程序执行的顺序按照代码的先后顺序执行。
		synchronized:保证可见性,对一个变量执行unclock之前,必须先把此变量同步回主内存中
		final:被final修饰的变量在构造器中初始化完成,但构造器没有把this的引用传递出去,在其他线程中可以看见final字段的值
		有序性:在当前线程内,所有的操作都是有序的,但从当前线程中观察另一个线程,所有的操作都是无序的
		JVM的线程实现基于操作系统,windows,linux使用一对一的线程模型; 线程调度模式使用抢占式,自动完成
	程序次序规则:
		一个线程内,按照代码顺序(控制流顺序及逻辑顺序),前面的代码比后面的代码先行发生
	管理锁定规则:
		一个unclock操作先行发生于后面对于同一个锁的lock操作
	volatile变量规则:
		对于一个volatile变量的写操作先行发生于后面对这个变量的读操作//先write进主内存,另一线程再read进工作内存
	线程启动规则:
		Thread对象的start()方法先行发生于此线程的每一个动作
	线程终止规则:
		线程中所有操作都先行发生于对此线程的终止检测,通过Thread.join()方法结束,Thread().isAlive的返回值手段检测线程已经终止执行
	线程中断规则:
		对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生,可以通过Thread.interrupt()方法检测是否有中断发生
	对象终结规则:
		一个对象的初始化完成先行发生于它的finalize()的开始
	传递性:
		如果操作A先行发生于操作B,操作B先行发生于操作C,说明操作A先行发生于操作C
	解决线程安全:
		1.互斥同步:阻塞式同步;保证共享数据同一时刻只能被一条线程使用,使用重量级synchronize操作,在对线程状态唤醒,睡眠需要到内核中进行, 导致状态转移操作的时间比代码一般时间较长; 使用ReentrantLock实现同步,相比于synchronize他多增加: 线程等待锁时间过长自动中断,多个线程等待同一个锁,获得锁的顺序按照申请锁的顺序而定,可以锁住多个条件; 但是在性能上还是使用synchronize
		2.非阻塞式同步: 共享数据发生冲突,不需要将线程挂起,而是采取补偿措施,对修改的数据进行修正
		3.可重入代码: 执行线程中判断执行结果是否满足预期,不满足执行其他代码
		4.线程本地储存:消费者-工厂模式, 阻塞队列
		5.锁优化:
	自旋锁: 让线程执行忙循环,不让线程挂起(线程切换很浪费资源,自旋不等价于阻塞),该线程不断检查锁是否释放所占用时间短的时候,自旋效果好,当锁占用时间过长,自旋效果不好
	自适应自旋:根据上一个自旋过程时间决定自旋次数以及是否自旋
		6.锁消除:当堆上数据不会出现逃逸,那么直接会将该数据当做栈上数据,同步锁自动消除
	例:
	public String f(String s1, String s2){
	//代码反编译之后等价于s1.append(s2), 由于s1,s2不会逃逸到f外部,所以s1,s2连接的时候自动消除append的同步操作机制
		return s1+s2;
	}
		7.锁粗化:使同步模块控制的范围扩大, 就比如在for外部加锁,而不是for内部,重复加锁,消锁也会导致性能损失
		8.轻量级锁:传统锁(重量级锁),轻量级锁用于减少重量级锁使用操作系统互斥量产生的性能消耗
		9.偏向锁:在数据无竞争的时候直接消除CAS,偏向锁会偏向于第一个获得他的线程,在接下来的执行的过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要同步
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值