深入理解java虚拟机

3 篇文章 0 订阅

自动内存管理机制

运行时数据区域

在这里插入图片描述

程序计数器

程序计算器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,在任何一个确定的时刻,一个处理器(或一个内核)都只会执行一条线程中的指令。因此,为了能在线程切换后恢复到正确的位置,程序计算器互不影响,独立存储。被称为“”线程私有“”的内存。

补充:执行java方法,这个计算器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行Native方法,这个计数器值为空(Undefined)。此内存区域是唯一一个没有规定任何OutOfMomoryError情况的区域

java虚拟机栈

java虚拟机(Java Virtual Machine Stacks)是线程私有的,它的生命周期于线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量、操作数栈【1】、动态链接【2】、方法出入口信息。调用 -> 完成过程 = 栈帧在虚拟机栈入栈 -> 出站操作

局部变量表:局部变量表存在方法中各种局部变量的信息,如,基本数据类型,引用数据类型,外加一个retrurnAddress类型(指向了一条字节码指令的地址。调用时,方法结束回到下一条执行指令的位置)。局部变量表所要的内存空间在编译期间完成分配,当“压栈时”,这个方法需要的帧中分配多大的局部变量空间是确定的,运行期不会改变局部变量表的大小

本地方法栈

(HotSopt虚拟机)将本地方法栈和虚拟机栈合二为一

java堆

该区域的唯一亩的就是存放对象实例。所以是被线程共享的一块"运行时数据区域中占内存最大的一块"。随着JIT编译器的发展于逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术导致,所有的对象都在堆上,变得不那么“绝对”!。

方法区

它用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编辑后的代码等数据

运行时常量池

用于存放编译期生成的各种字面量和符号引用【1】,这部分内存将在类加载后进去方法区的运行时常量池中

补充:
在1.7及以后的版本中,将运行时常量池移到了堆中,运行时常量池中,每个类型对应一个常量池,例如字符串常量池,基本数据类型的常量池,它们存储运行时所有用到的常量,例如文字字符串,final定义的变量。
在这里插入图片描述

静态区

方法区中一个模块,用于存放静态变量和静态代码块,也就是static定义的变量都存在这里,类的所有的实例都共享,在类的加载过程中,就已经静态的方法、变量、代码块执行了,执行时到new出来的对象、常量等引用都存在静态区中。
在这里插入图片描述
在这里插入图片描述

HotSpot虚拟机对象探秘

对象的创建

1:虚拟机遇到一条new指令,检查该指定参数是否能在常量池中定位到一个类的符号引用。并且检查该符号引用代表的类是否已经被加载解析初始化过。如果没有先执行该类加载
2:为新生对象分配内存。存在两种分配方式。
第一种:“指针碰撞”。直接挪动指针位置。于对象大小相等的距离。这种操作必须规定java堆内存绝对规整。
第二种:“空闲列表”。维护一张空闲位置的表,表上记录那些内存可用,划分空间给对象实例,并更新列表上的记录
3:内存分配完后,内存空间都初始化为零值(不包括对象头)。这的内存分配不包括实例变量,也不包括 staic final修饰的变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。而staic final修饰的变量在编译期已经把结果放入常量池
4:init,给静态变量赋值。

对象的内存布局

对象的访问定位

                                                          句柄访问对象图
在这里插入图片描述
java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。而该句柄包括了对象实例数据与类型数据各自的具体地址

wiki中句柄的意思:句柄与普通指针的区别在于,指针包含的是引用对象的内存地址,而句柄则是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。这种间接访问对象的模式增强了系统对引用对象的控制。(参见封装)。通俗的说就是我们调用句柄就是调用句柄所提供的服务,即句柄已经把它能做的操作都设定好了,我们只能在句柄所提供的操作范围内进行操作,但是普通指针的操作却多种多样,不受限制。



                                                          直接指针访问对象
在这里插入图片描述

引用数据类型直接存放java堆中存放对象地址。这种情况java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息

垃圾收集器与内存分配

引用技术算法

**引用技术算法:**给对象添加一个引用计算器,每当有一个地方引用它时,计算器值就加1;当引用失效时,计算器值就减去1;任何时刻计数器为0的对象就是不可能再被使用的。


弊端:无法解决相互引用的问题.如下实例代码利用引用技术算法是无法正常回收的。因此Java虚拟机里面没有选用该方式

       public class ReferenceCountingGC {
        	public Object instance = null;
        	private static final int _1MB = 1024 * 1024;
        	private byte[] bigSize = new byte[2 * _1MB];

   		public static void main(String ...arg) {
   			ReferenceCountingGC objA = new ReferenceCountingGC();
   			ReferenceCountingGC objB = new ReferenceCountingGC();
   			objA.instance = objB;
   			objB.instance = objA;
   			
   			objA = null;
   			objB = null;
   			
   			System.gc();
   		}
       }

可达性分析算法

**可达性分析算法:**通过一系列称为“GC Roots”的对象作为起始点,从这些点解开始向下扫搜,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明此对象不可以用。

“GC Roots”:全局性引用(常量,类静态变量),执行上下文(栈帧中的本地变量表)

再谈引用

引用类型功能简介
强引用“Object obj = new Object”,只要obj不指向null,垃圾收集器永远不会回收掉被引用的对象
软引用描述一些还有用但是非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收【?】。如果这次回收还没有足够的内存,才会抛出内存异常
弱引用描述非必需对象,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。**当垃圾收集器工作时,**无论当前内存是否足够,都会回收掉只被弱引用关联的对象
虚引用它是最弱的一种引用关系。一个对是否有虚引用的存在,完全不会对齐声戳时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,要真正宣告一个对象的死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有于GC Roots相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。*当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,*虚拟机将这两种情况都视为“没有必要执行”。

执行 finalize()过程:将该对象放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行它。程序并不会等待它执行完,才结束。防治该线程出现死锁,堵塞无法正常关闭。稍后GC将对F-Queue中的对象进行第一次小规模标记,如果对象要在finalize()中拯救自己——只要重新于引用链上的任何一个对象建立关联即可
对象的finalize()方法最多只会被系统自动调用一次!!!

垃圾收集算法

标记-清除算法

对应生存还是死亡描述。

弊端:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

将可用内存按照容量划分为大小相等的两块,每次都只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清除掉。保证一片区域在使用的时候,另一片区域为空

弊端:事迹可用内存只有原先的一半,代价太大

标记-整理算法

跟标记-清除“标记”的步骤一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存

HotSpot的算法实现

为了加速可达性分析算法的执行过程,避免一个不漏地检查完所有执行上下文和全局的引用位置,HotSpot虚拟机添加了以下的辅助算法

枚举跟节点

OopMap:面向对象Map集合,记录了哪些地方普通对象的指针。

安全点

SafePoint;用途一:管理在哪里地方放置OopMap指令。
用途二:如何在CG发生的时候让线程“跑”到最近的安全节点上停顿下来
方案1:抢先式中断,在GC发生时,首先把全部线程全部中断,如果发现有线程中断的地方不在安全点上,恢复线程,让它“跑”到安全点上。@Deprecated
方案2:主动式中断,设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

安全区域

辅助安全点,让Sleep和Blocked状态下的不可“跑”的线程也能正常停止。在线程执行到Safe Region中的代码时,首先标识自己已经进去了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举,如果完成了,那么线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止

垃圾收集器

切换垃圾收集器命令简介
-XX:+UseSerialGC虚拟机运行在Client模式下的默认值,Serial+Serial Old。
-XX:+UseParNewGCParNew+Serial Old,在JDK1.8被废弃,在JDK1.7还可以使用。
-XX:+UseConcMarkSweepGCParNew+CMS+Serial Old。
-XX:+UseParallelGC虚拟机运行在Server模式下的默认值,Parallel Scavenge+Serial Old(PS Mark Sweep)。
-XX:+UseParallelOldGCParallel Scavenge+Parallel Old。
-XX:+UseG1GCG1+G1
收集器特点
Serial 收集器一个线程,停止其他所有工作线程,直到它收集完成
Serial Old收集器一个线程, 停止所有工作线程,使用“标记-整理”算法,处理老年区
ParNew 收集器一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用
Parallel Scavenge收集器多个线程同时收集,没有工作线程。 “吞吐量优先”收集器,吞吐量 = 运行代码时间 /(运行用户代码时间 + CG时间)。减少新生代的存储空间,从而减少CG时间
Parallel Old收集器多个线程同时收集,没有工作线程。 使用“标记-整理”,在“吞吐量优先”情况下处理老年区。和Parallel Scavenge收集器配合
CMS 收集器以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除
G1 收集器一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。

内存分配与回收策略

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
基于复制算法:     将该区域分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制的到另外一块Survivor空间上,最后清除Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。当Survivor空间不够使用时,需要依赖其他内存(这里指老年代)进行分配担保。如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

GC区别
新生代GC(Minor GC)只发生在新生代的垃圾收集动作,因为Java对象大多数都具备朝生夕灭的特性,所以Minor GC非常频繁,一半回收速度也比较快
老年代GC(Major GC/ Full GC)指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在 Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。速度比Minor GC 慢十倍以上

大对象直接进去老年代

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值得对象直接老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制

长期存活的对象进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,每“熬过”一次Minor GC,年龄加1。当它的年龄增加到一定程度(默认为15),将会被晋升到老年代。通过-XX:MaxTenurgingThreshold设置该值大小

动态对象年龄判断

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

	/**
	* VM参数: -verbose:gc -Xms20M -Xms20M -Xmn10M -XX:+PrintGCDetails
	* -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
	* -XX:+PrintTenuringDistribution
	*/
   public class TestTenuringThreshold2{
   	private static final int _1MB = 1024 * 1024;
   	public static void main(String ..arg){
		byte[] a1,a2,a3,a4;
		
		//【2】from区 占1024 kb,a1 + a2刚好等于一半
		// 直接去老年代
		a1 = new byte[_1MB / 4];
		a2 = new byte[_1MB / 4];
		// from区 不够放 ,也去老年代
		a3 = new byte[_1MB * 4];
		//【1】Eden满 发生Minor GC
		a4 = new byte[_1MB * 4];
		a4 = null;
		a4 = new byte[_1MB * 4];
	}
   }

问题:怎么知道对象创建放进Eden区还是Survivor区?
对象的创建都是绝大部分都创建在Eden区,当Eden区满的时候,系统第一次Minor GC时候才会有可能有对象在from区 或 to区。在第一次Minor GC前不存在(这观点可能有误)。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,那么能确保该次Minor GC安全,否则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均时间,如果大于,尝试这次有风险Minor GC。如果小于,或HandlePromotionFailure设置不允许冒险,那就改进行一次Full GC。

风险:把Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还能容纳这些对象的剩余空间,一共有多少对象会存货下来在实际完成内存回收之前是无法明确知道的,所有只好取之前每一次回收晋升老年代对象容量的平均大小值作为经验。于老年代剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多的空间。

虚拟机类加载机制

类加载的时机

在这里插入图片描述
如图,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

到底何时才需要初始化?

java虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

1、遇到new(创建)、getstatic、putstatic(调用非final静态属性)、或invokestatic(调用类static方法)
2、使用java.lang.reflect包的方法对类进行反射调用的时候。
3、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其初始化
4、当虚拟机启动时,用户需要指定一个执行(main()方法的类),虚拟机会先初始化该类
5、jdk1.7动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解释结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄。并且该方法对应的类未初始化,先执行初始化。(?)

您误以为会初始化的情况

第一种

	public class SuperClass {
	staic {
		sout("SuperClass init");
	}
	public static int value = 123;
}
	public class SubClass extends SuperClass {
		staic {
		sout("SubClass  init");
	}
}
	public class Main {
	psvm {
		sout(SubClass.value);
	}
}

上述代码只会输出SuperClass init!。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化

第二种

	public class NotInitialization {
	psvm {
		SuperClass[] sca = new SuperClass[10];
    }	
}

会发现并没有出书“SuperClass init”,说明没有初始化。但是会触发另一个名为“Lorg.fenixsoft.classloading.SuperClass”的类的初始化,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类创建动作由字节码指令newarray触发。
(解决了为什么不能创建泛型数组的问题!!他会触发另个一个由虚拟机自动生成的该类型的Lorg.fenixsoft.classloading.Xxxxx一维数组。由于虚拟机对于泛型是擦拭的,根本就没办法创建这种)

第三种
一个接口在初始化时,并不要求其父亲接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化

类加载过程

加载

加载阶段,虚拟机需要完成以下3件事:
1) 通过一个类的全限定名来获取定义此类的二进制节流
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3) 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
这是程序员最可控的一个阶段,可以自己实现自定义类加载器加载一个类

存在一个特例:数组
数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但是仍与类加载器有密切关系,它的数据类型,来类加载器创建。以下是数组创建过程遵循以下规则:

  1. 如果数组的元素类型是引用类型,就才用类加载器默认规则(递归下,双亲委派机制)加载该组件类型。
    2)如果是基本数据类型,Java虚拟机直接把数组标记于引导类加载器关联
    3)数组类的可见性于它的组件类型的可见性一致,如果是基本数据类型,默认public

验证

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证***
    最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候(这也间接证明了,这几个过程(除解析)有序性,是开始有序,执行和完成是交替进行的)符号引用验证可以看做对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验。通常需要校验下列内容:
  5. 符号引用中通过字符串描述的全限定名是否能找到对应的类
  6. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  7. 符号引用中的类、字段、方法的访问性(private,public,protected,defalut)是否可被当前类方位

准备

正式为类变量分配内存并设置类初始化的阶段,这些变量所用的内存都将在方法区进行分配(不包括实例变量,实例变量在对象初始化随对象一起分配在java堆中)这个阶段除了被fianl修饰的变量,都是默认值。赋值阶段在初始化阶段

解析***

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析过程又可以分别两种:
第一种: 在类加载器加载时候对常量池中的符号引用进行解析。
第二种: 等到一个符号引用将要被使用才去解析它

关注第二种!!!invokeddynamic指令(java称为准动态语言原因之一)
上述指令,对应的引用称为“动态调用点限定符”,这里“动态”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才会进行。相对的其余解析指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时进行解析

  1. 类或接口的解析
  2. 字段解析 (自身 -》接口 -》父类 -》报错)
  3. 类方法解析 (自身 -》父类 -》接口 -》报错)
    正因如此 如果一个类继承实现相同方法,会按父类的来
  4. 接口方法解析 (自身 -》接口 -》接口 -》报错)
    如果不同父接口存在多个相匹配的方法,那么将从多个放方法中返回其中的一个

初始化

先给准备阶段默认值,赋值。执行类构造器()方法的过程
若果一个类中没有静态语句块,也没有对静态变量的赋值操作,那么编译器可以不为这个类生成()方法该方法执行过程会被正确加锁,同步(静态内部类延迟初始化)。但是这会导致如果第一个线程在创建的时候过程太长,后面的线程就会堵塞

public class DeadLoopClass {
    static {
        //编译期知道该类无法正确clinit必定会死循环,所以出现Initializer must be able to complete normally
        boolean flag = true;
       
        System.out.println(Thread.currentThread().getName() +" init DeadLoopClass");
        while (flag ){

        }
    }

    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() +"Begin");
            new DeadLoopClass();
            System.out.println(Thread.currentThread().getName() +"End");
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        //结果: main init DeadLoopClass
    }
}

日,因为main程序入口先初始化了该类,搞到线程1,线程2都阻塞了。不过也足以证明上述所提到阻塞问题的发生。

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现

类与类加载器

就算是同一个Class文件,要想比较加载过后的类是否“相等”,只有在这两个类都是由同一个累加器加载的前提下才有意义。如果一个有系统应用程序加载器加载的,另一个使用自定义加载器加载,虽然是同一个Class文件,但依然是不相痛的

双亲委派模型

在这里插入图片描述
在这里插入图片描述

破坏双亲委派模型 (SPI**) 补

SPI

虚拟机字节码执行引擎

运行时栈帧结构

在这里插入图片描述
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的信息。在编译程序代码的时候,栈帧不要多大的局部变量表,多深的操作数栈都已经完成全部确定了。并切入到方法表的Code属性中。(通过javac Xxxx.java文件编译成class文件,在通过javap -c 就可以看到基于栈的指令集已经确定)。执行引擎运行所有的字节码指令都只会针对当前栈帧的操。

局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals确定了该方法所需要分配的局部变量表的最大容量

局部变量表的容量移变量槽(Variable Slot)为最小单位。存放着8种基本数据类型,和reference、returnAddress(注意Slot大小通常是32位,所以在存long、double 64位时一定要连续两个Solt。因为是线程私有,所以也不会有并发安全的问题)

reference:一、从此引用中直接或简介地查找到对象在Java堆中的数据存放的起始地址索引(直接地址访问 / 句柄访问)。二、引用中直接或间接地找到对象所属数据类型在方法区中的存储的类型信息。

returnAddress:

操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后进先出栈。最大深度在编译期写到Code属性的max_stacks。32位数据类型占栈深度1,64位则为2。执行期间,不会操作栈最大深度。

当虚拟机在执行编译过后的Class文件对应的指令的时候,会在操作局部变量表和操作数栈。不断进行入\出栈操作。

动态连接 (堆对象和方法栈产生关系的地方)

每个栈帧都包含一个指向运行时常量池中该栈所属方法的引用,持有这个引用为了支持方法调用过程当中的动态连接。

方法返回地址

当方法开始执行后,只有两种方式可以突出这个方法。


第一种方式:执行引擎遇到任意一个方法返回的字节码指令。这时候可能会有返回值传递给上层方法调用者(这里提一点,在操作数栈没说的。每一个栈帧不一定相互独立,上层的局部变量表共享区域和下层操作栈共享区域重叠),是否有返回值和返回值的类型将根据遇到何种方法的返回指定来决定,这种退出方式称为正常完成出口

第二种方式:异常退出,没有在本地方发表的表中没有搜索到匹配的异常处理器,就会导致方法退出或使用了athrow字节码指定尝试的异常(也就是try/catch 算正常退出)。被称为异常方法出口。

方法调用

参考我的java 核心技术卷I II 继承下方法调用

这里要补充。建立的虚方法表,又或是接口方法表。父类、子类的表都应具有一样的索引需要,这样当类型转换时,仅需要变跟查找的表,就可以从不同的表中按索引转换出所需要的入口地址(这也导致了为什么子类作为父类存在的时候,没有办法调用自身的方法,因为查找的父类的虚表)

晚期(运行期)优化

尽管并不是全有的Java虚拟机都采用解释器于编译器并存的架构,但许多主流的商用虚拟机,如HotSpot、J9等,都同时包含解释器于编译器

解释器

当程序需要速度启动和执行时候,解释器可以省去编译的时间,立即执行,

编译器

在程序运行后,随着时间的推移,编译器会把越来越多的代码编译成本地代码,提高执行效率

在这里插入图片描述

什么样的代码需要变成本地代码

1、被多次调用的方法
2、被多次执行的循环体

对于上述两种代码,编译器都会将整个方法(不单单循环体)作为编译对象。这种编译方式因为编译发生在方法执行的过程当中,因此形象地称之为栈上替换(On stack Replacement OSR,即方法栈帧还是栈上,方法就被替换了)

又是如何判断的“热点方法”

  1. 基于采用的热点探测,虚拟机周期线检查栈顶,如果某个方法经常出现在栈顶(“How old are you”),那么就是“热点方法”。但是存在弊端如果线程堵塞或Sleep长时间等,都会影响虚拟机的判断
  2. 基于计算器的热点探测,为每个方法建立计数器,统计方法的执行次数,如果超多一个阈值,那么这个方法就是“热点方法”.

解释性代码和本地代码有何区别(个人猜想)

当我们通过反编译看class文件的时候会看到很多指令,这些基于栈指令集的指令就是解释器运行代码的指令。一条只能是一个动作。然后本地代码可能就像汇编语言那些,更接近机械语言。一个指令存在复合操作。

两个重要的优化方式:方法内联,逃逸分析

方法内联

   //优化前的代码
   static class B {
		int value;
		final int get() { return val;}
	}
	public void foo() {
		y = b.get();
		z = b.get();
	}
	 //优化后的代码
	 public void foo() {
		y = b.value;
		z = b.value;
	}

有没有方法存在一定的问题,如果get方法没有被final修饰 是invokevirtual指定的方法(当然final方法也是调用该invokevirtual,但因为它不可继承性,它也是唯一方法)。也就是B 继承 A。还是用代码说吧

  static class B extend A {
  		int value;
  }
  static class A {
		int value;
		int get() { return val;}
	}
	public void foo() {
		y = b.get();
		z = b.get();
	}

那么代码就没方法优化成b.value。因此编译器在进行方法内联时,如果是非虚方法(invokeinterface , invokespecial(实例方法,包括初始化方法,私有方法,父类方法),invokestatic)就直接内联。如果遇到虚方法,则向“类型继承关系分析(Class Hierarchy Analysis CHA)”查询次方法在当前程序下是否有多个版本可供选择,只要一个,可以进行内联。不过这种内联属于激进优化,需要预留一个“逃生门”。如果程序后续执行过程中,虚拟机一直没有加载到会令该方法的接受者的继承关系发生变化,那这个内敛优化一直使用下去。如果发生了变化,那就退回解释状态执行,或者重新进行编译。

就算一开始查CHA的时候就存在多个版本,虚拟机还会使用内联缓存来完成方法内联。在方法入口处设立一个缓存来记录第一次调用的对象。下一次还是它就继续使用内联,下次不是他。取消内联。(有点像偏向锁)

逃逸分析

  1. 栈上分配: 如果一个对象不会逃逸出方法之外,那就无需要分配到堆当中。
    public void existInStack(){
		Person person = new Person();
		sout(person.name);
	}
		
	public Person existInHeap(){
		Person person = new Person();
		sout(person.name);
		return person;
	}
	
  1. 同步消除: 如果变量的无法被其他线程访问,那么这个变量肯定安全,对这个变量加锁会被自动消除
	public ThreadLocal<Object> map= new ThreadLocal<>();
	public void syncElimination() {
		Ojbect obj = map.get();
		//将会被取消
		synchronized(obj) {
			sout
		}
	}
  1. 标量替换: 标量(int、long等数值类型、以及reference类型等,不可在分割类型)。如果一个对象可以继续分解,被称为聚合量,如对象。如果一个对象不会被外部访问,并且这个对象可以被拆散的话。那么程序就不会创建这个对象,直接用它拆解后的标量代替
	public void scalarReplacement() {
   	Person person = new Person(String name, int age);
   	String name = person.getName();
   	int age = person.getAge();
   }	
    	//变成 可能存在想法错误。如有误请提出更改
   	public void scalarReplacement() {
   	person.name = name;
   	person.age = age;
   	String name = person.name;
   	int age = person.age = age;
   }	

Java内存模型与线程

沿用我的深入理解java内存模型

下面做一点错误知识更改。在总复习的时候会把相同部分的内容分开成一片片笔记
                                                                       CPU内存模型
在这里插入图片描述

                                                                       Java内存模式
在这里插入图片描述
可以看到基本上是一模一样的。Java内存模式不是正式存在的,只是用来屏蔽掉各种硬件和操作系统的内存访问差异。是一个抽象存在。类似于一些协议。

Java内存模型(下面称JMM):JMM规定了所有的变量都存储在主内存中(此处的主内存于计算机当中主内存名字一样,两者可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程都有自己的工作内存,线程的工作内存中保存了被线程使用到的变量的主内存副本拷贝(dup命令),线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接写主内存的变量 (根据Java虚拟机规范的规定,volatile变量依然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在内存中读写访问一般,因此上面描述对于volatile也不存在例外)

内存间交互操作

JMM中定义了一下8种操作来完成各个线程如何于主内存进行交互。

命令描述
lock(锁定)作用于主内存变量,它把一个变量标识为一个线程独占的状态
unlock(解锁)作用于主内存变量,它把一个处理锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取)作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存当中,以便随后的load动作使用
load(载入)作用于工作内存变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use(使用)作用于工作内存变量,它把工作内存中得一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令将会执行这个操作
assign(赋值)作用于工作内存变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个变量赋值的字节码指令时执行这个操作
store(存储)作用于工作内存变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
write(写入)作用于主内存变量,它把store操作从工作内存中得到的变量的值放入主内存变量中

线程安全与锁优化

锁优化

适应性自旋锁

因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。为此当一个线程遇到堵塞的时候,不会立即马上被挂起进入堵塞状态,而是为首先执行一个忙循环(自旋)。自旋的默认值是10次,可以通过-XX:PerBlockSpin修改

在jdk1.6中引入了自适应自旋锁。意味着自选次数不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定。如果自旋时期成功获得锁,那么就会加大自旋次数,反之减少次数

轻量级锁

虚拟机首先将在当前线程的栈帧建立一个名为锁记录的空间,用于存储锁对象目前的Mark Work(存储对象哈希码,GC标记,GC年龄,同步锁的信息)的拷贝。过程如图
在这里插入图片描述
在这里插入图片描述
虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新成功了,那么这个线程就拥有了该对象的锁。并且更改对象Mark Word 标志为“00”。如果CAS失败,则会先检查Mark Work是否指向该线程,是!继续进去同步块,不是!证明有多个线程争夺同一个锁,要膨胀为重量级锁。解锁过程相反,CAS交换回相互的信息,成功则整个同步完成,失败则释放锁,同时唤醒其他线程

偏向锁

当锁谁先第一次被线程获得的时候,虚拟机将会把对象头中的标记位设为“01”,即偏向模式。同时使用cas操作把获取到这个锁的线程ID记录在Mark Word之中。如果cas成功,持有偏向锁的线程每次进去到这个锁相关的同步块时,虚拟机都可以不在进行任何同步操作。当另外一个线程去尝试获取锁时,偏向模式就宣布失败。根据对象是否处于被锁定状态,撤销偏向,恢复到未锁定“01”或轻量级锁“00”状态。

常用的Jvm参数总结

参数作用
-Xms设置Java应用程序启动时的初始堆大小
-Xmx设置Java应用程序能获得的最大堆大小
-Xss设置线程栈得到大小
-XX:MinHeapFreeRatio设置堆空间最小的空闲比例。当堆空间的空闲内存小于这个数值时,JVM便会扩展堆空间
-XX:MaxHeapFreeRatio设置堆空间的最大空闲比例。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆
-XX:NewSize设置新生代的大小
-XX:NewRatio设置老年代与新生代的比例,它等于老年代大小除以新生代大小
-XX:SurviorRatio新生代中eden区与survivor区的比例
-XX:MaxPremRatio设置最大的持久化大小
-XX:PremSize设置永久区的初始值
-XX:TargetSurvivorRatio设置survivor区的可使用率,当survivor区的空间使用率达到这个数值时,会将对象送入老年代
-XX:PretenureSizeThreshold设置大对象直接进入老年代的阈值
-XX:MaxTrnuringThreshold设置在 每次新生代经过 minor gc 对象年龄加1。到达阈值到移植到老年区。默认值是15。不一定要到达这个值,根据内存使用情况计算的。
-XX:ParallelGCThreads设置执行垃圾回收线程数量
-XX:CompileThreshold当函数调用次数超过这个值,JIT会将字节码编译成本地机器码
-XX:PrintCompilation打印JIT编译信息
-XX:CITime打印JIT编译的基本信息
-XX:PrintGCDetails打印GC信息
-XX:HeapDumpOnOutOfMemoryError发生OOM时,打印堆快照
-XX:DisableExplicitGC禁止在代码层调用 System.gc() 触发 fullGC
-Xincgc触发增量GC, 每次GC部分区域
-Xverify:none不检验类文件
美团技术团队
https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值