JVM源码
2024-09-03 10:42:03 4 举报
AI智能生成
JVM源码是Java虚拟机的实现,它允许Java程序在所有平台上运行。Java源码文件通常以.java为后缀,通过javac编译器将其转换为.class文件,然后在JVM上运行。JVM源码主要包括类加载器、执行引擎、垃圾回收器等模块。类加载器负责加载和链接class文件,执行引擎负责执行字节码,垃圾回收器负责回收无用的对象,释放内存。通过深入研究JVM源码,可以更好地理解Java程序的运行原理和调优方法。
作者其他创作
大纲/内容
HotSpot JVM将BootstrapClassLoader用C++语言比那些,而ExtClassLoader和AppClassLoader用Java语言编写,主要是处于以下几个原因: 1.引导类加载器(BootstrapClassLoader) 核心系统组件:BootstrapClassLoader是JVM的核心部分,负责加载Java核心库(如java.lang.*、java.util.*等)。这些核心库在JVM启动时必须 被加载,并且它们的加载过程必须在任何Java代码执行之前完成。 依赖关系:由于BootstrapClassLoader加载的是最基础的Java类库,它不能已离开于任何Java嘞,否则,会陷入加载循环(依赖的类还未加载,需要先加载依赖类) 性能和控制:使用C++编写可以更直接地控制内存和资源,提高性能和启动速度 2.扩展类加载器(ExtClassLoader)和应用类加载器(AppClassLoader) 实现简便:ExtClassLoader和AppClassLoader主要是加载扩展库和应用程序嘞,它们不需要像BootstrapClassLoader那样处理JVM启动的核心部分。因此可以用 Java语言编写,利用java自身的特性,使得代码更简洁和易维护 继承和扩展:用Java编写可以更方便地利用Java的面向对象特性,继承和扩展ClassLoader嘞,从而更容易地实现自定义类加载器。 运行时环境:在JVM启动之后,ExtClassLoader和AppClassLoader运行已经初始化的Java环境中,不需要担心加载器本身的类是否已经加载 总结: BootStrapClassLoader使用C++编写,确保JVM在启动时能够加载最核心的类库,并且不依赖于任何Java类,避免循环依赖,同事提高性能和控制性 ExtClassLoader和AppClassLoader使用Java语言编写,方便实现和扩展,同时在JVM启动后利用现有的Java环境和面向对象特性,更易于维护和扩展。 这种设计确保了JVM的启动和运行时类加载机制既高效又灵活,能够满足不同阶段的需求
为什么HotSpot将BootstrapClassLoader使用C++语言编写而ExtClassLoader和AppClassLoader用java语言编写
Tomcat为什么要打破双亲委派机制
为什么JVM在给对象分配内存时必须要求是一块连续的内存,不可以是散乱的内存吗?
1.因为操作数栈作为缓冲地带,可以将运算结果的中间结果保存到操作数栈2.另外操作数栈是一个顺序的高速寄存器,可以更加快速的操作数据
Java虚拟机(JVM)的设计是基于栈的架构,其中操作数栈(Operand Stack)和局部变量表(Local Variable Table)是执行字节码指令的两个关键组件。以下是一些原因,解释了为什么JVM在执行过程中不直接从局部变量表读取数据,而是先将数据压入操作数栈:1.统一操作模型:操作数栈提供了一个统一的操作模型,所有的计算都是通过栈来进行的。这种设计简化了指令集和执行引擎的实现,使得指令集更加紧凑和易于理解2.指令集简化:如果直接从局部变量表读取数据,那么指令集将需要包含更多的操作来直接引用局部变量表的特定位置。而通过操作数栈,可以使用较少的指令来完成复杂的操作。3.灵活性:操作数栈允许更灵活的操作顺序和数据流控制。例如,方法调用的参数传递、返回值处理以及控制流指令(如条件分支和循环)都可以通过操作数栈方便地实现。4.动态作用域:操作数栈支持动态作用域,即在运行时可以动态地改变变量的作用域。这是因为在执行过程中,操作数栈的内容是动态变化的5.优化编译器设计:通过使用操作数栈,编译器的设计可以更加简单,因为它只需要关注如何生成将数据推送到栈上和从栈上弹出数据的指令序列6.支持多态和动态绑定:Java语言支持堕胎,这意味着方法调用可能在运行时解析。操作数栈可以更容易地支持这种机制,因为它允许在调用方法之前将对象引用和参数推送到栈上,然后在运行时确定调用哪个方法实现7.字节码间接性:操作数栈的使用使得字节码更加简洁。例如,iload指令将一个int类型的局部变量加载到操作数栈上,后续的算术指令可以直接从栈上取操作数总之,JVM的设计选择是基于操作数栈来执行字节码是为了实现一个简单、高效且灵活的执行模型。尽管这种设计可能不是最快的方式(与基于寄存器的架构相比),但它为Java语言的特性提供了良好的支持,并且使得JVM的实现更为统一和可移植
JVM为什么不直接从局部变量表中读取数据,而是压入操作数栈
因为设计师考虑到了还有存在i--的情况,如果直接将i++设置为+1的话,那么i--操作也需要设置一个专门的字节码指令来表示-1,但是如果有设置step的动作,那么在进行i--时,可以将step设置为-1,这是一种设计思想
为什么i++不是直接+1,而是用inc字节码,将它的step设置为1
java -XX:+PrintFlagsFinal -version | findstr Thread
Windows下类似Linux下的grep命令
在Java虚拟机(JVM)中,Handle模型是一种用于访问对象和执行方法调用的机制,它主要涉及到以下概念:句柄(Handle): 句柄是一个用于间接定位对象的指针。在JVM中,每个对象都有一个句柄,这个句柄包含了对象在内存中的实际地址和其他元数据信息。使用句柄的好处是,当对象在移动时(例如垃圾回收时的压缩),只需要更新句柄中的地址,而不需要更新所有引用该对象的指针以下是Handle模型的主要组成部分和功能:1.句柄池(Handle Pool):JVM维护一个句柄池,其中存储了所有对象的句柄。句柄池中的每个句柄都对应一个具体的对象2.句柄表(Handle Table):句柄表是一个数据结构,用于管理所有句柄的分配和释放,当一个对象被创建时,JVM会在句柄表中为它分配一个句柄,并在对象被销毁时释放该句柄3.间接访问:通过句柄访问对象是间接的。这意味着当程序需要访问一个对象时,它首先获取对象的句柄,然后通过句柄表来访问对象的字段和方法4.安全性:句柄模型提供了一定程度的安全性,因为它可以防止直接访问对象的内存地址,从而避免了潜在的而已操作Handle模型与直接指针模型(Direct Pointer Model)相对,后者直接存储对象的内存地址,而不是句柄。直接指针模型在访问对象时更快,因为它省去了通过句柄间接定位对象的步骤,但缺点是在对象移动时需要更新所有指向该对象的指针。在HotSpot中,使用那种模型(句柄模型或直接指针模型)可以通过JVM启动参数来配置,然而,现代JVM实现通常倾向于使用直接指针模型,因为它们通过优化垃圾回收算法来减少对象移动,从而使得直接指针模型在性能上更有优势。总的来说,Handle模型是JVM用于管理和访问对象的一种机制,它通过句柄来间接引用对象,提供了对象移动时的灵活性,但可能会引入额外的性能开销
Java中的handle模型是什么
CPU总是以Word size为单位从内存中读取数据,在64位处理器中的word size为8个字节。64位的内存每次只能吞吐8个字节
操作系统的栈的槽位是多少字节
并行和并发是两个经常被提及的概念,尤其在讨论多线程编程、分布式系统、数据处理等领域时。尽管它们都与同时处理多个任务有关,但它们的含义和侧重点有所不同。并行(Parallelism)1.定义:并行是指同一时刻,有多条指令在多个处理器上同时执行2.核心:并行关注的是资源的充分利用,特别是在多核或多处理器系统中,通过并行执行来提高效率3.实现:并行通常通过将任务分解成多个子任务来实现,这些子任务可以同时执行,例如使用多线程或多进程4.示例:在四核处理器上同时运行四个线程,每个线程运行在独立的核上并发(Concurrency)1.定义:并发是指同一时间段内,多个任务交替执行,使得宏观上看起来像是同时进行的2.核心:并发关注的是结构设计,它允许处理多个任务,但不一定意味着这些任务同时执行3.实现:并发可以通过时间分片(time slicing)、多线程、异步编程等技术来实现,使得单个处理器可以处理多个任务4.示例:单核处理器上通过时间分片技术交替执行多个线程主要区别:1.执行方式:并行是真正的同时执行,而并发是交替执行,给人同时执行的错觉2.处理器数量:并行通常需要多个处理器,而并发可以在单个处理器上事项3.性能提升:并行可以显著提升性能,因为任务被分割并在多个处理器上同时执行。并发则更多关注于资源的有效管理,并不一定能提高性能,但可以提高资源的利用率4.应用场景:并行适用于可以分解为多个独立子任务的问题,并发适用于需要处理多个任务但不需要同时执行的场景。在软件工程中,并发和并行都是重要的概念。并发性是系统设计的一部分,它使得系统可以处理多个任务,而并行性则是优化手段,用于提高系统的性能。有时候,系统设计需要同时考虑并行和并发,以实现高效的任务处理
并行与并发的区别
自己写的博客https://blog.csdn.net/Cover_sky/article/details/140477010
```bashfind ./ -name GenerateCurrencyData.java./jdk/make/src/classes/build/tools/generatecurrencydata/GenerateCurrencyData.java```
编译OpenJDK
这里需要用attach到一个java进程,
利用jps可以查看到相关指令
将进程号输入进去,我这里换了一个程序,进程号不同
会看到运行的Java Thread
查看main线程的调用栈
JDK相关工具
引用类型数组的klass模型,我们在代码中创建的Hello数组对象引用都是空的
引用类型的klass模型
也就是_java_mirror,这里c++上的注解也是说明了这个InstanceMirroKlass的存在
Klass模型Java的每个类,在JVM中都有一个对应的Klass类实例与之对应,存储类的元信息如:常量池、属性信息、方法信息....从继承关系上也能看出来,类的元信息是存储在元空间的。普通的Java类在JVM中对应的是InstanceKlass(C++)类的实例,再来说下它的三个子类:1.InstanceMirrorKlass:用于表示java.lang.Class,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜相类2.InstanceRefKlass:用于表示java/lang/ref/Reference类的子类3.InstanceClassLoaderKlass:用于遍历某个加载器的类Java中的数组不是静态数据类型,而是动态数据类型,即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:1.TypeArrayKlass:用于表示基本类型的数组2.ObjArrayKlass:用于表示引用类型的数组总结:非数组:InstanceKlass -> 普通的类在JVM中对应的C++类 方法区InstanceMirrorKlass -> 对应的是Class对象 镜像类 堆区数组:基本类型数组boolean、byte、char、short、int、float、long、double -> TypeArrayKlass引用类型数组: ObjArrayKlass为什么还要有镜像类?是为了安全,由JVM控制可以将哪些参数返回给用户
验证1.文件格式验证。如验证class文件中是否包含魔数(CAFE BABE)、主次版本号是否在当前虚拟机处理范围之内2.元数据验证。如这个类是否有父类、这个类的父类是否继承了不允许被继承的类(如被final修饰的类)3.字节码验证。整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析确定程序语义是合法的,如保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载如本地变量表中4.符号引用验证。最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是堆类自身以外的(常量池中的各种符号引用)的信息匹配性校验,如符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用中的类和字段一级方法的访问性是否可以被当前类访问(比如调用静态方法,检查调用的方法是否可以被当前类调用)
```javapublic class MyClassLoadHello { public static int v = 10; public static final int b = 11; public static void main(String[] args) { int a = 1; int b = 2; System.out.println(a + b); }}```可以看到变量b多出了一个ConstantValue的属性,这个属性指向了常量池中11这个数值。准备阶段就会直接赋值
反观变量v则是在类的初始化<clinit>方法块中
通过HSDB可以发现InstanceMirrorKlass对象是有变量m这个属性的,但是InstanceKlass对象却显示只有两个静态属性。是不是很奇怪?字节码指令中都没有这个变量,InstanceMirrorKlass对象中却有这个属性。其实也不难理解,InstsanceKlass对象是存储在方法区中的,可以表示类的静态属性信息。由于这个属性没有赋值也没有使用,字节码层面就直接优化掉了,我们知道反射的时候可以获取到这个类的所有信息所有属性以及所有方法不管其作用域的范围是什么,如果不给InstanceMirrorKlass对象赋值这个属性,那么在反射的时候就会拿不到,这其实违背了反射的规则。所以要有静态属性赋初值这个动作,来给InstasnceMirrorKlass对象赋上这个属性。
为什么要在准备阶段赋初值?为何不直接赋值?(C++对象)InstanceMirrorKlass对象只是创建出来,并没有属性,把这个变量写入到Class对象中去,如果这个静态变量没有使用到,也没有赋初值,字节码指令中将不包含该变量.如图所示,变量m并没有在字节码指令中,因为没有赋初值也没有进行使用
准备为静态变量分配内存、赋初值。实例变量是在创建对象的时候完成赋值的,没有赋初值这一说。如果是被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步
拓展知识:编译时常量池和运行时常量池在Java中,常量池是class文件的一部分,它用于存储关于类和接口的常量以及一些符号引用。常量池分为两种:编译时常量池和运行时常量池。1.编译时常量池(Constant Pool)编译时常量池是在编译器生成的,它包含了类文件中的字面量(Literal)和符号引用(Symbolic References).字面量:如文本字符串、final常量值等符号引用:包括类和接口的全限定名、字段名称和描述符、方法名称和描述符。这些符号引用在类加载阶段或第一次使用时会被解析为直接引用。编译时常量池时.class文件的一部分,它随着类文件的生成而生成,每个.class文件都有一个自己的编译时常量池2.运行时常量池(Runtime Constant Pool)运行时常量池是类或接口在JVM运行时的一部分,当类被JVM加载时,JVM会根据.class文件中的编译时常量池来创建运行时常量池。运行时常量池是方法区中的一部分。动态性:运行时常量池具有动态性,它可以在运行期间想其中添加新的常量。例如,String的intern()方法可以将字符串常量添加到运行时常量池中解析:运行时常量池中的符号引用会在类加载过程中或第一次使用时被解析为直接引用。简而言之,编译时常量池是静态的,是.class文件的一部分,而运行时常量池是动态的,是JVM运行时数据区的一部分。运行时常量池在JVM的规范中是方法区的一部分,但在不同的JVM实现中可能会有所不同,如在HotSpot虚拟机中,它被放在了堆(Heap)中。
解析将常量池中的符号引用转为直接引用。解析后的信息存储在ConstantPoolCache类实例中。其中会涉及到如下:1.类或接口的解析2.字段解析3.方法解析4.接口方法解析符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。可以理解为静态常量池的索引直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的.某个变量的内存地址解析的时机?1.加载阶段解析常量池时(类加载以后马上解析 resolve的参数需要改为 => true)2.用的时候解析什么?只要是直接引用都需要解析1.继承的类、实现的接口2.属性3.方法如何避免重复解析:借助缓存,ConstantPoolCache(运行时常量池的缓存) if (klass -> is_resolved()) {}如图所示常量池缓存:key: 常量池的索引 2value: String -> ConstantPoolEntry静态属性是存储在堆区中的,静态属性的访问:1.去缓存中去找,如果有直接返回2.如果没有就触发解析底层实现:1.会找到直接引用2.会存储到常量池缓存中openjdk是第二种思路,在执行特定的字节码指令之前进行解析:anewarray、checkcase、getfield、instanceof、invokeddynamic、invokeinterface、invokesepcial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield.
<clinit>()方法执行死锁示例1:```javapublic class DeadLoopClass { static { if (true) { System.out.println(Thread.currentThread() + \"init DeadLoopClass\"); while (true) { } } } public static void main(String[] args) { Runnable script = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread() + \"start\"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + \"end\
<clinit>()方法执行死锁示例2:```javapublic class InitDeadLock { public static void main(String[] args) throws InterruptedException { new Thread(() -> new A()).start(); new Thread(() -> new B()).start(); }}class A { static { System.out.println(\"class A init\"); new B(); }}class B { static { System.out.println(\"class B init\"); new A(); }}```一个线程创建A对象,进而触发A的初始化,但是A的clinit方法中又创建B,又触发B的初始化,另一个线程的初始化则反过来,资源获取顺序不当造成了死锁
卸载判定一个类是否是\"无用的类\"的条件相对一个实例对象或者\"废弃常量\"要苛刻很多。类需要同时满足下面3个条件才能算是\"无用的类\":1.该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。2.加载该类的ClassLoader已经被回收3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法虚拟机可以堆满足上述3个条件的无用类进行回收,这里说的仅仅是\"可以\",而并不是和对象一样,不使用了,就必然会回收。这也造成了很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区中实现垃圾收集,而且在方法区中进行垃圾收集\"性价比\"一般比较低:在队中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70~95%的空间,而永久代的垃圾收集效率远低于此
类加载的过程类的加载由7个步骤完成,如图所示。类的加载说的是前5个阶段。
Test_1_A静态变量str的值存放在StringTable中,镜像类中存放的是字符串的指针
Test_1_Bstr是类Test_1_A的静态属性,可以看到不会存储子类Test_1_B的镜像类中。
ConstantPoolCache常量池缓存是为常量池预留的运行时数据结构。保证所有字段访问和调用字节码的解释器运行时信息。缓存是在类被积极使用之前创建和初始化的。每个缓存项在解析时被填充。从图中的代码可以看出,是直接去获取ConstantPoolCacheEntry
静态字段如何存储instanceKlassinstanceMirrorKlass
1.魔数验证
2.版本号验证
3.解析常量池、访问权限等等
4.解析类的接口信息、方法信息、属性信息、父类信息
5.创建位于方法区的InstanceKlass对象
6.创建位于堆中的InstanceMirrorClass对象
类解析的过程ClassFileParser.cpp中的parseClassFile方法。
1.深入理解类加载机制
首先我们找到启动main方法的入口
第二步,找到LoadMianClass
第三步,找到GetLauncherHelperClass中,findBootStrapClass方法。
第四步
BootstrapClassLoader在JVM中对应的是ClassLoader.cpp。我们可以看到它包含的都是一些静态属性和静态方法
通过代码方式可以查看扩展类加载器加载的路径,也可以通过java.ext.dirs指定
ExtClassLoader的继承关系图
扩展类加载器
查看应用类加载器,它是默认加载用户程序的类加载器,也可以通过java.class.path指定
AppClassLoader的继承关系图
应用类加载器
loadClass方法与findClass方法分析:1.loadClass方法是ClassLoader类中最常用的方法之一,它负责加载指定的类。它的主要特点是:1.1 它是一个public方法,可以被外部类调用。1.2 首先会检查类是否已经被加载,如果已经被加载,则直接返回对应的Class对象。1.3 如果类没有被加载,它会调用findLoadedClass方法来检查类是否已经被其他类加载器加载1.4 如果类仍然没有被加载,它会调用findClass方法(或者委托给父类加载器)来加载类。1.5 如果上面的操作还是没有成功加载类,就抛出ClassNotFoundException一场2.findClass方法是ClassLoader类中的一个protected方法,通常用于自定义类加载器时重写该方法2.1 findClass方法负责从文件系统、网络或其他来源找到并读取类的字节码2.2 在自定义类加载器时,通常重写findClass方法来实现特定的类加载逻辑。2.3 当loadClass方法确定类尚未被加载,并且父类加载器没有加载该类时,它将调用findClass方法在实现自定义类加载器时,通常会这样重写findClass:1.根据类的全限定名(name参数)转换为文件路径2.读取类的字节码文件3.调用defineClass方法,将字节码转换成Class对象。loadClass是用于外部调用的公共方法,负责整个类的加载过程,包括委托模型和类加载逻辑findClass是用于被loadClass调用的受保护方法,通常在自定义类加载器时被重写以实现具体的类查找和字节码读取逻辑
loadClass方法与findClass方法
自定义类加载器
类加载器JVM中有两种类型的类加载器,由C++编写的及由Java编写的。除了启动类加载器(BootstrapClassLoader)是由C++编写的,其他都是由Java编写的,由Java编写的类加载器都继承自类java.lang.ClassLoader.JVM还支持自定义类加载器。各类加载器之间存在着逻辑上的父子关系,因为他们没有直接的从属关系
当我们再进一步查看的话,会发现看不到它的实现,这可能和C++的写法有关系,
它这里的方法实现其实是要到jvm.cpp文件中才能看到,具体的写法我也不是很清楚,但是可以看到,最后生成关键的一步是find_instance_or_array_klass这个方法
我们可以发现,当查找加载过的类时,它是把类信息放到了一个hashtable里面,先通过class_name和类加载器loader计算出一个d_index,然后再通过这个索引去查找
如果在Dictionary字典里面找到了这个classname对应的类,则返回,没有则返回null,可以看到,查找加载过的类时,并不是直接拿着classname去找的,而是classname + classloader组合起来查找的.也就是key=>类的全限定名+类加载器 ->index value: Metadata:klass
如何查看加载过的类?findLoadedClass方法如果深入进去看的话,会发现其调用到了一个native的findLoadedClass0方法,这里的话,我们可以在openjdk的源码当中搜索
这里其实是要返回一个InstanceMirrorClass对象出来,如果加载过,则返回缓存即可,没有加载过,则进行加载
调用了FindBootstrapClass方法,
checkAndLoadMain方法中可以看到通过classloader加载该类,这也是类加载器加载一个类的流程,这个地方是要判断这个类应该交给哪个类加载器去加载,加载的细节其实就走到我们Java文件里面了ClassLoader的细节,那么这个scloader是怎么来的呢?
scloader是通过ClassLoader.getSytemClassLoader()方法创建的
在initSystemClassLoader方法里面核心逻辑是sum.misc.Lanuncher.getLauncher()
sun.misc.Launcher是C++里面的Java类,并不是我们rt.jar里面的,所以我们还得在HotSpot中去看,我们发现Launcher的构造方法中创建了扩展类加载器ExtClassLoader以及AppClassLoader
可以看到parent为null
在创建AppClassLoader的时候,把ExtClasLoader当作parent传给了构造参数,也就是说AppClassLoader和ExtClassLoader才具有真正意义上父子结构
而findBootstrapClassOrNull方法底层又是一个native方法,这个时候就需要到C++里面去看了
这个方法会调用到ClassLoader.c(也就是对应的Bootstrap)。
看到SystemDictionary::resolve_or_null,返回由Bootstrap类加载器加载的InstanceMirrorClass
再结合到java层面来看,当委托到ExtClassLoader的时候,由于它的parent为null,这个时候会调用findBootstrapClassOrNull
获取LauncherHelper,它这个动作其实是让Bootstrap类加载器进行加载的
如何打破双亲委派?为什么要打破双亲委派机制?当前文件中搜索\"Tomcat为什么要打破双亲委派机制\"因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了DriverManager接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib目录下的文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派机制。类似这样的情况就需要打破双亲委派。打破双亲委派的意思其实就是不委派、向下委派。
线程上下文类加载器1.是什么?一种特殊的类加载器,可以通过Thread获取,基于此可实现逆向委托加载2.为什么?为了解决双亲委派的缺陷而生3.怎么做?如图所示SPI机制它是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。Tomcat/Spring就是这样类似的机制
openjdk源码会有很多这样的判断AccessController.doPrivileged
双亲委派机制如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载器请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试自己加载
反射的底层原理forNamegetFieldgetMethod通过这些方法获取一个类的信息,那么它是怎么存储的呢?它会去方法区中进行查找,那么在HotSpot中它是通过Dictionary字典来存储这些信息的,底层数据结构是hashtablekey:类的全限定名+类加载器->indexvalue: Metadata:klass反射时需要先找到InstsanceKlass对象然后才能找到InstanceMirrorKlass对象,因为JVM是没法找到堆中的Class对象的,JVM找到InstanceKlass,就可以直接拿到InstanceMirrorKlass
2.类加载器、双亲委派、SPI
不同语言能在JVM上运行的本质
大端与小端模式:大端模式:高位存在低地址,低位存高地址小段模式:与大端模式相反
字节码文件组成
不同的JDK版本号所对应的major和minor版本号
常量池中的0号索引是this指针,具体是什么想法不得而知。
字节码中最难解析的是方法结构
常量池其实只有三种数据结构类型,String比较特殊,还有就是4字节类型和8字节类型。如FieldI_info类型,它的结构如下:class index:22 nameAndType index:33用一个short来存储 2个字节 :0xffff22 << 16 0x22000x2200 | 0x334个字节合起来就是0x00220033拼起来存储的
常量池项
什么是描述符,具体解释见\"字节码中的数据结构\"
字段描述符解释表
类访问和属性修饰符标志
表示方法访问权限及属性的各标志
字节码中的数据结构
3.JVM解析字节码文件过程
Java进程在操作系统内存中的结构
可以这样理解:JVM内存模型其实就是JVM在启动的时候从操作系统内存中要了一块大内存,然后将这个大内存分成五个区域:方法区、堆区、虚拟机栈、本地方法栈、本地方法栈、程序计数器.其实叫JVM运行时区域更合适。但是要区分JVM内存模型与JMM(Java Memory Model)InstanceKlass:类的元信息(方法区)InstanceMirrorKlass:镜像类Class对象(堆区)四个名词:class文件:即硬盘上的.class文件class content:类加载器将硬盘上的.class文件读入内存中的那一块内存区域Class对象:```javaClass<?> clazz = Test.class```对象:Test obj = new Test();
C++中Hotspot是如何将Klass对象放到方法区的?C++有个技术叫做操作符重写,/vm/memory/allocation.hpp,操作符重写:new可以指定这个对象存在哪里
为什么C++提供Java语言里反射的原因?1.现代语言特性需求:随着变成语言的发展,现代变成语言普遍支持反射机制,因为它可以大大提高程序的灵活性和可扩展性。C++作为一门长期发炸你的语言,也在不断地更细你和增加新特性,以保持其竞争力2.运行时类型信息(RTTI):C++中的反射机制是通过运行时类型信息实现的,这允许程序在运行时获取对象的类型信息,并进行相应的操作。这是实现多态、动态绑定等高级编程概念的基础3.框架和库开发:反射机制杜宇框架和库的开发尤为重要,因为它可以使这些框架和库更加通用和强大。例如,它可以使序列化、反序列化、对象关系映射(ORM)等操作更加容易实现4.增强互操作性:C++与其他支持反射的语言(如Java、C#等)进行交互时,反射机制可以提供更好的互操作性。例如C++/CLI是一种特殊的C++方言,用于与.NET框架交互,其中就包含了反射特性5.动态编程:虽然C++是一门静态类型语言,但在某些情况下,开发者可能需要动态编程的能力,例如在脚本语言或插件系统中。反射可以提供这种能力6.社区需求:长期以来,C++社区中一直有呼声要求增加反射机制。随着标准的更新,C++委员会逐渐考虑将这些需求纳入语言标准7.代码生成和元编程:反射机制可以与模板元编程结合使用,以实现更高级的代码生成技术,这在一些复杂的系统中非常有用需要注意的是,C++的反射机制与传统上Java中的反射并不完全相同。C++的反射能力相对较弱,通常是通过RTTI和模板元编程等技术部分实现的。而且直至2024,C++标准中并没有完整的反射机制,但有一些提案正在尝试将更完整的反射特性引入C++
局部变量表局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象的起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress累心地数据,这8种苏韩剧类型,都可以用32位或更小地物理内存来存放,但这种描述与明确指出\"每个Slot占用32位长度地内存空间\"是有一些差别地,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间区实现一个Slot,虚拟机仍要使用对齐和补白的方式让Slot在外观上看起来与32位虚拟机中的一致。既然前面提到了Java虚拟机的数据类型,在此再简单介绍一下他们。一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference(Java虚拟机规范中没有明确规定reference类型的长度,它的长度与实际使用32还是64位u虚拟机有关,如果是64位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取32位虚拟机的reference长度)和returnAddress8种类型。前面6中不需要多家解释,可以按照Java语言中对应数据类型的概念区理解它们(仅是这样理解而已,Java语言与Java虚拟机中的基本数据类型是存在本质差别的),而第7种reference类型表示对一个对象实例的引用,虚拟机规范既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少都应当能通过这个引用做到两点:1.从此引用直接或间接地查找到对象在堆中的数据存放的起始地址索引2.此引用中直接或间接地查找到对象所属数据类型在方法去中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束(并不是所有语言的对象引用都能满足这两点,例如C++语言,默认情况下(不开启RTTI支持的其概况),就之只能满足第一点,而不满足第二点。这也是为何C++中提供Java语言里很常见的反射的根本原因)对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间,Java语言中明确的(reference类型则可能是32位也可能是64位)64位的数据类型只有long和double两种。值得一提的是,这里把long和double数据类型分割存储的做法与\"long和double的非原子性协定\
动态链接每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。我们知道Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接
方法返回地址当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这死后可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可以能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
附加信息虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现,在实际开发中,一般会把动态链接、方法返回地址与其他附加信息全部归为一类称为栈帧信息
Java虚拟机栈Java虚拟机栈(Java Virtual machine Stacks)是线程私有的,它的声明周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame(栈帧是方法运行时的基础数据结构))用于存储以下几个部分1.局部变量表2.操作数栈3.动态连接4.方法出口/返回地址5.附加信息经常有人把Java内存区分为堆内存(heap)和栈内存(stack),这种分发比较粗糙,java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象分配关系最密集的内存区域是这两块。虚拟机栈和线程个数比为1:1一个虚拟机栈中有多少栈帧?跟方法的调用次数成正比
对象的创建DCL中单例对象的创建为什么要加volatile?如果在ns级别的超高并发是需要加volatile关键字的,这个关键字会禁止指令重排,因为CPU是会乱序执行这些指令的。如下指令接下来我们看下一个对象的创建流程0 new #21.堆区申请了内存(不完全对象)构造方法还未执行2.内存地址压入栈3 dup duplicate1.赋值栈顶元素为什么要复制?因为接下来调用init非静态方法,但是this指针还是空的,所以需要把栈顶元素弹出去,给this指针赋值,然后再把元素压入栈2.再次压入栈4 invokespecial #3 <init>方法this指针this = null执行方法分为两步:1.构建环境会涉及到创建栈帧、传参、保存现场。给this指针赋值,记录方法的执行之前的位置2.执行7 astore_11.pop出元素:对象的指针2.赋值给index=1的位置的变量(局部变量)
对象的创建Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(引用类型的对象)的创建又是一个怎样的过程呢?虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号一弄,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行响应的类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针想空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为\"指针碰撞\"(Bump the Pointer)。如果Java堆中的内存并不是规整的,已使用内存和空闲的内存相互交错,那就没有办法简单进行指针碰撞了,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为\"空闲列表\
AQS 管程思想
堆对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得表示那么\"绝对\"了。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做\"GC堆\
4.JVM内存模型与操作系统内存模型
字符数组的存储方式JVM有三种模型:1.Oop模型:Java对象对应的C++对象2.Klass模型:Java类在JVM对应的C++对象3.handle模型
字符串常量池的设计思想:1.字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序地性能2.JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化2.1 为字符串开辟一个字符串常量池,类似于缓存区2.2 创建字符串常量时,首先查询字符串常量池是否存在该字符串2.3 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
在JDK1.6中,调用intern()首先会在字符串池中寻找equals相等的字符串,加入字符串存在就返回该字符串在字符串池中的引用,假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将StringTable的一个表项指向这个新创建的实例
在JDK1.7(及以上版本)中,由于字符串池不在永久代了,intern()做了一些修改,更方便地利用堆中的对象。字符串存在时和JDK1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例
jdk1.7代码图这里要明确一点的是,在Jdk6以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用intern是会直接产生java.lang.OutOfMemoryError:PermGen Space错误的。Perm区域太小是一个主要原因,当然在1.8中已经直接取消了Perm区域,而新建立了一个元区域。应该是jdk开发者认为Perm区域已经不适合现在Java的发展了。正是因为字符串常量池移动到Java Heap区域后,再看下面解释。1.先看s3和s4字符串,String s3 = new String(\"1\") + new String(\"1\");这句代码中现在生成了两个对象,一个是字符串常量池中的\"1\",另一个是Java Heap中的s3引用指向的对象。中间还有2个匿名的new String(\"1\"),不去讨论他们,此时s3引用对象内容是\"11\",但此时常量池中是没有\"11\"对象的2.接下来,s3.intern(); 这一句代码,是将s3中的\"11\
美团技术分享代码一:JDK1.7以上:false trueJDK1.6: false false
1.代码一和代码二的改变就是s3.intern的顺序是放在了String s4 = \"11\"后了,这样,首先执行String s4 = \"11\";声明s4的时候常量池中是不存在\"11\"对象的。执行完毕后\"11\"对象是s4声明产生的对象。然后再执行s3.intern时,发现常量池中\"11\
美团技术分享代码二:JDK1.7以上:false falseJDK1.6 false false
字符串常量池设计原理字符串常量池底层时HotSpot的C++实现的。底层类似一个Hashtable,保存的本质上是字符串对象的引用,来看一道比较常见的案例,图中的代码创建了多少个String对象// JDK6:false 创建了6个对象// JDK7及以上:true 创建了5个对象为什么输出会有这些变化呢?主要还是字符串从永久代中脱离、移入堆区的原因,intern()方法也相应发生了变化同时也解释了JDK1.6中字符串溢出会抛出OutOfMemoryError:PermGen Space.而在JDK1.7及以上版本会抛出OutOfMemoryError:Java heap space
hash生成方式
通过hash计算索引
key的生成方式1.通过String的内容 + 长度生成hash值2.将hash值转为key
value的生成方式将Java的String类的实例InstanceOopDesc封装成HashtableEntry
String类重写了hashCode方法.可以看出String的hashcode与String的内容是有关系的
String类也重写了equals方法
字符串jdk8和jdk9的区别
双引号这种方式创建的字符串对象,只会在常量池中。因为\"11\"这个字面量,创建对象s1的时候,JVM会先去常量池中通过equals(key)方法,判断是否有相同的对象。如果有,则直接返回该对象在常量池中的引用;如果没有,则会在常量池中创建一个新对象,再返回引用
new String这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象印象。步骤大致如下:因为有\"11\"这个字面量,所以会先检查字符串常量池中是否存在字符串\"11\"不存在,先在字符串常量池里创建一个字符串对象;再去堆内存中创建一个字符串对象\"11\";存在的话,就直接去堆内存中创建一个字符串对象\"11\";最后,将堆内存中的引用返回
两个双引号
两个new String
不同方式创建字符串在JVM中的存在形式
双引号 + 双引号
双引号 + new String
拼接字符串底层是如何实现的StringBuilder拼接而成的字面量是不会放入到常量池中的
5.剖析字符串与数组的底层实现
oop模型前面的klass模型,它是Java类的元信息在JVM中的存在形式。这个oop模型是Java对象在JVM中的存在形式
空闲列表机制在操作系统中,内存分配策略的空闲列表机制是一种管理内存资源的方法。以下是其基本原理和步骤1.基本原理:1.1 内存块管理:操作系统将内存划分为多个块(block),每个块可以是空闲的,也可以是已分配的1.2 空闲列表:操作系统维护一个记录所有空闲内存块的列表,称为空闲列表。这个列表通常会记录每个空闲块的大小和起始地址。2.步骤:2.1 初始化:当系统启动时,除了操作系统本身占用的内存外,其余的内存都被视为一个大的空闲快,并被加入到空闲列表中2.2 分配内存:2.2.1 当一个进程请求内存时,操作系统会根据请求的大小在空闲列表中查找何时的空闲块2.2.2 查找策略可以是首次适配(first fit)、最佳适配(best fit)或最坏适配(worst fit)2.2.3 一旦找到合适的空闲块,操作系统会从空闲列表中移除该块,并将其标记为已分配,然后将内存分配给请求的进程2.3 内存释放2.3.1 当进程释放内存时,操作系统会回收这块内存,并将其标记为空闲2.3.2 操作系统可能会将这块空闲与周围的空闲块合并,形成一个更大的空闲块,以减少内存碎片2.3.3 合并后的空闲块或新的空闲块会被重新加入到空闲列表中2.4 碎片整理2.4.1 随着内存的分配和释放,内存可能会出现碎片化,即空闲内存分散在各个角落,导致无法满足大的内存请求2.4.2 空闲列表机制可能会通过移动已分配的内存块来整理碎片,但这在实际操作中可能比较复杂且耗时优点:简单性:空闲列表机制相对简单,易于实现灵活性:可以根据不同的内存分配策略(如首次适配、最佳适配等)来优化内存使用缺点:维护开销:随着内存分配和释放的频繁进行,空闲列表的维护可能会带来一定的开销内存碎片:可能导致内存碎片,尤其时当空闲块和已分配块的大小频繁变动时。
操作系统为什么不采用指针碰撞机制进行内存分配?操作系统不采用指针碰撞的机制进行内存分配,主要是因为操作系统的内存管理需要面对复杂和多样化的环境。以下是一些关键原因:1.多任务和多用户操作系统:必须支持多个进程和线程的运行,每个进程或线程可能需要不同大小的内存,且分配和释内存的时间点是随机的指针碰撞:适用于单一连续内存分配的场景,不适合处理多任务环境下的复杂内存需求2.内存碎片操作系统:需要处理内存碎片问题,因为不同大小的内存块被分配和释放后,内存中可能会留下无法被利用的小空闲块指针碰撞:不擅长处理内存碎片,因为它假设内存分配是连续的,如果内存碎片严重,指针碰撞机制将无法有效工作3.内存分配的灵活性操作系统:需要能够分配任意大小的内存块以满足不同进程的需求指针碰撞:通常需要一个连续的内存区域,并且当内存区域不足以容纳新分配的内存块是,需要额外的机制来处理这种情况4.物理内存与虚拟内存操作系统:使用虚拟内存技术,将物理内存与虚拟内存地址映射起来,这使得内存分配更加复杂指针碰撞:适用于简单的物理内存分配,不涉及复杂的地址映射5.安全性和隔离性操作系统:需要确保不同进程的内存是隔离的,防止一个进程访问或修改另一个进程的内存指针碰撞:需要额外的机制来保证内存的安全性和隔离性6.性能考量操作系统:必须高效地管理内存以满足性能需求,这通常意味着需要一个能够快速响应的内存分配策略指针碰撞:虽然分配速度快,但在多任务环境下,它可能导致内存利用率低下,因为它可能留下很多小的空闲内存块7.系统调用和API操作系统:提供了系统调用和API供应用程序请求和释放内存,这些调用需要能够处理各种复杂的内存分配请求指针碰撞:无法直接适应这些系统调用和API的需求
1.空闲列表OS把不常用的内存写到硬盘上,如果有进程需要读取引发缺页异常,进而会去硬盘上读
为什么JVM不采用空闲列表的内存分配策略而是采用指针碰撞的形式?Java虚拟机(JVM)内存分配策略与操作系统分配策略的不同,主要由以下几个因素决定的:1.内存管理的抽象层级不同:操作系统:操作系统负责管理物理内存,直接与硬件交互,需要处理多种复杂情况,如内存碎片、多进程/线程的内存需求等JVM:JVM运行在操作系统之上,主要负责管理Java程序的运行时内存,它对内存的管理更加抽象化,并且通常不需要处理硬件级别的内存碎片问题2.内存分配的特点空闲列表:适用于需要频繁分配和释放不同大小内存的场景,且物理内存可能存在碎片指针碰撞:适用于对象大小相对一致且频繁创建和销毁的场景,如JVM中的对象分配3.JVM内存分配的具体考虑效率:指针碰撞时一种非常高效的内存分配方式。在JVM中,当一个新的对象需要被分配时,只需要移动以下指针(分配指针),而不需要遍历整个空闲列表来查找合适的内存块。这大大减少了内存分配的开销内存连续性:指针碰撞可以保证分配的内存是连续的,这对于提高缓存命中率有好处,因为连续的内存访问往往能更好地利用CPU缓存内存碎片:由于JVM通常会分配和释放大量大小相似的对象,内存碎片问题不像在操作系统中那么严重。JVM通过垃圾回收来管理内存,看可以在GC过程中重新整理内存,减少碎片垃圾回收:JVM采用垃圾回收机制来自动管理内存。当对象不再被引用时,垃圾回收期会自动回收他们所占用的内存。这种方式与空闲列表的内存分配策略相比,减少了手动内存释放的复杂性,并且通过不同的垃圾回收u算法来优化没存使用4.JVM的内存模型:堆空间:JVM的堆空间是用于存储Java对象的地方,通常分为老年代和新生代等,不同代的内存管理策略不同。新生代采用复制算法,老年代采用标记-清除或标记-整理算法。这些算法与指针碰撞的内存分配策略更为契合。JVM选择指针碰撞的内存分配策略,而不是空闲列表,是因为这种策略更符合JVM内存管理的需求,能够提供更高的内存分配和回收效率,并且与JVM的垃圾回收机制更为兼容
2.指针碰撞(CAS)如图所示,bottom指向内存区域的头部,end指向内存区域的尾部,top指针开始指向头部(可用内存的起始位置)。如果new_top = top(当前top) + 对象大小满足这个等式,则分配成功,并top=new_top
为什么JVM不采用空闲列表的内存分配策略而是采用指针碰撞的形式?() Java虚拟机(JVM)内存分配策略与操作系统内存分配策略的不同,主要是由以下几个因素决定的: 1.内存管理的抽象层级不同 a.操作系统:操作系统负责管理武力内存,直接与硬件交互,需要处理多种复杂情况,如内存碎片、多进程/线程的内存需求等 b.JVM:JVM运行在操作系统之上,主要负责管理Java程序的运行时内存,它对内存的管理更加抽象化,并且通常不需要处理硬件级别的内存碎片问题 2.内存分配的特点 a.空闲列表:适用于需要频繁分配和释放不同大小内存的场景,且物理内存可能存在碎片 b.指针碰撞(Bump-the-pointer):适用于对象大小相对一致且频繁创建和销毁的场景,如JVM中的对象分配 3.JVM内存分配的具体考虑 a.效率:指针碰撞是一种非常高效的内存分配方式。在JVM中,当一个新的对象需要被分配时,只需要移动一下指针(分配指针),而不需要遍历整个空闲列表 来查找合适的内存块。这大大减少了内存分配的开销 b.内存连续性:碰撞指针可以保证分配的内存是连续的,这对于提高缓存命中率有好处,因为连续的内存访问往往能更好地利用CPU缓存。 c.内存碎片:由于JVM通常会分配和释放大量大小相似的对象,内存碎片问题不像在操作系统中那么严重。JVM通过垃圾回收(GC)来管理内存,可以在GC过程中 重新整理内存,减少碎片 d.垃圾回收:JVM采用垃圾回收机制来自动管理内存。当对象不再被应用时,垃圾回收器会自动回收它们所占用的内存。这种方式与空闲列表中的内存分配策略相比, 减少了手动释放内存的复杂性,并且可以通过不同的垃圾回收算法来优化内存使用 4.JVM的内存模型 a.堆空间:JVM的堆空间时用于存储Java对象的地方。堆空间通常分为年轻代、老年代等,不同代的内存管理策略不同。年轻代通常采用复制算法,而老年代可能 采用标记-清楚或标记-整理算法。这些算法与指针碰撞的内存分配策略更为契合 5.综上所述 a.JVM选择指针碰撞的内存分配策略,而不是空闲列表,是因为这种策略更符合JVM内存管理的需求,能够提供给更高的内存分配和回收效率,并且与JVM 的垃圾回收机制更为兼容
操作系统为什么不采用指针碰撞的机制进行内存分配()操作系统不采用指针碰撞的机制进行内存分配,主要是因为操作系统的内存管理需要面对更复杂和多样化的环境。以下是一些关键原因:1.多任务和多用户环境操作系统:必须支持多个进程和线程的运行,每个进程或线程可能需要不同大小的内存,且分配和释放内存的时间点是随机的指针碰撞:适用于单一连续内存分配的场景,不适合处理多任务环境下的复杂内存请求2.内存碎片操作系统:需要处理内存碎片问题,因为不同大小的内存块被分分配和释放后,内存中可能会留下无法被利用的小空闲块指针碰撞:不擅长处理内存碎片,因为它假设内存分配是连续的,如果内存碎片严重,指针碰撞机制将无法有效工作3.内存分配的灵活性操作系统:需要能够分配任意大小的内存块以满足不同进程的需求指针碰撞:通常需要一个连续的内存区域,并且当内存区域不足以容纳新分配的内存块时需要额外的机制来处理这种情况4.物理内存和虚拟内存操作系统:使用虚拟内存技术,将物理内存与虚拟内存地址映射起来,这使得内存分配更加复杂指针碰撞:适用于简单的物理内存分配,不涉及复杂的地址映射5.安全性和隔离性操作系统:需要确保不同进程的内存是隔离的,防止一个进程访问或修改另一个进程的内存指针碰撞:需要额外的机制来保证内存的安全性和隔离性6.性能考量操作系统:必须高效地管理内存以满足性能需求,这通常意味着需要一个能够快速响应的内存分配策略指针碰撞:虽然分配速度快,但在多任务环境下,它可能导致内存利用率地下,因为它可能留下很多小的空闲内存块7.系统调用和API操作系统;提供了系统调用和API供应应用程序请求和释放内存,这些调用需要能够处理各种复杂的内存分配请求指针碰撞:无法直接适应这些系统调用和API的需求因此,操作系统通常采用空闲列表、位图、伙伴系统等更复杂的内存分配策略,这些策略能够更好地处理多任务、多用户环境下的内存分配和碎片问题,同时保持较高的内存利用率和系统性能
JVM中的内存分配策略为什么不使用空闲列表的方式而是采用指针碰撞
3.TLAB线程私有堆(新生代)JVM里面如果用锁来控制对象内存的分配的话,会比较繁琐,它是堆中某一块私有内存区域,如果用完了再还给JVM,重新盛情一块更大的内存
4.PLAB(TLAB的老年代)
内存分配策略:1.空闲列表2.指针碰撞(jvm采用的)2.1 top指针:执行的是可用内存的起始位置2.2 采用CAS的方式3.TLAB 线程私有堆4.PLAB 老年代的线程私有堆
有属性的对象Mark Word:8B类型指针:4B实例数据:4B+4B对象大小
计算对象大小
通过HSDB用Memory Viewer查看该对象在内存中的分配地址发现类型指针占8字节,0x3其实是数组的长度,前面用一行来存储类型指针
关闭指针压缩的情况下
通过HSDB用Memory Viewer查看该对象在内存中的分配地址发现类型指针占4个字节,开启之后,类型指针和数组长度放在一起了
开启指针压缩的情况下
为什么数组对象在关闭指针压缩的情况下有两段填充?font color=\"#a23735\
指针压缩的底层原理?这个技术能被开发出来归根结底是来自\"所有对象大小都必须能被8整除\"这条规则。font color=\"#a23735\
指针压缩-XX:-UseCompressedOops指针压缩技术只有64位机器才有。jdk6以后引入的技术,默认是开启的
1.堆到底设置成多大比较合适?堆越大越好吗?如果堆设置的很小-> GC频率可能很高,GC时间也比较短设置的很大->GC频率会低,但是GC的时长却增加了调优不是一蹴而就的,需要逐步调整,并观察JVM的GC情况2.什么样的系统可以进行JVM调优?首先要区分出是OLAP(在线分析)系统还是OLTP(在线事务查询)系统,如果是OLAP(在线分析)系统,是没有多大调优空间的,因为一次性要查询大量对象,这个是没有办法做调优的,只能加大内存3.正常GC单次时长100ms,前端和后端的平衡,不能让前端感到明显卡顿
6.亿级流量并发系统到底是怎么调优的
查看方法区使用情况在配置Java环境的情况下,直接打开命令行输入jvisualvm命令即可打开```bashC:\\Users\\87766>jvisualvm```然后在左边找到我们的程序,右边是查看图形
查看GC情况需要安装一个插件Visual GC,左上方工具栏安装即可
调优参数```bash-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20M -XX:+PrintGCDetails```原则:最大、最小设置成一样大,避免因内存分配不足而引发扩容程序运行起来后,通过VisualVM、Arthas查看占用了多少内存,向上调优,预留20%以上的空间
1.方法区
```javapublic class HeapOverFlowTest1 { int[] intArr = new int[10]; public static void main(String[] args) { List<HeapOverFlowTest1> list = new ArrayList<HeapOverFlowTest1>(); for (;;) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } list.add(new HeapOverFlowTest1()); } }}```模拟OOM
调优vm参数```java-Xms10m -Xmx10m -XX:+PrintGCDetails```
jvisualvm查看gc情况
调优原则:1.预留30%以上的空间2.周期性看日志,重点关注Full GC频率
2.堆区
```javapublic class StackOverFlowTest { private int val = 0; public void test() { val++; test(); } public static void main(String[] args) { StackOverFlowTest test = new StackOverFlowTest(); try { test.test(); } catch (Throwable e) { e.printStackTrace(); System.out.println(test.val); } }}```模拟OOM
调优参数-Xss200k 默认1M
3.虚拟机栈
如何查看Linux内核日志呢?```bashdmesg -T```
4.直接内存JVM进程的堆与运行时数据区平行1.Unsafe.allocateMemory2.ByteBuf.allocateDirect(NIO)怎么调优?不调优会怎样?(默认是不受限制)
5.调优参数类型1.KV类型:-XX:MetaspaceSize=10M2.boolean类型:-XX:+UseCompressedOops3.简写类型:-Xms10m
-q:只显示Java进程的ID
-m:输出Java进程的ID + main函数所在类的名字 + 传递给main函数的参数
-l:输出Java进程的ID+main函数所在类的全限定名(包名+类名)
-v:输出Java进程的ID+main函数所在类的名称+传递给JVM的参数应用:可以通过次方式快速查看JVM参数是否设置成功
jps源码的位置/openjdk/jdk/src/share/classes/sun/tools/jps
Ubuntu环境下,它的路径是/tmp/hsperfdata_username
如何识别的Java进程?jps输出的信息全是Java进程的信息,是如何做到的?Java进程在创建的时候,会生成相应的文件,进程相关的信息会写入该文件中。Windows下默认路径是:C:\\Users\\username\\AppData\\Local\\Temp\\hsperfdata_username
查看PerfData参数
PerfData文件1.文件创建每启动一个Java进程,/tmp/hsperfdata_username就会生成进程号的一个文件,这个文件是一个内存映射文件。有时候不会生成,受参数的影响,-XX:-/+UsePerfData默认是开启的,它是通过attach到Java进程中去,它属于寄生在Java进程当中,可以读取Java进程的内存。-XX:-/+PerfDisableSharedMem(禁用共享内存)默认是关闭的,即支持内存共享。如果金庸了,依赖于PerfData文件的工具就无法正常进行了2.文件删除正常情况下:默认情况下随Java进程的结束而销毁非正常退出:下一次去读目录的时候会检测进程是否存在, 用kill -0 去检测进程是否存活,不存货就会删除该进程号文件,不然就会留下垃圾文件3.文件更新-XX:PerfDataSamplingInterval=50,即内存与PerfData文件的数据延迟为50ms
6.工具如jps该命令是纯Java编写的
OOM killerdmesg -T 日志查看内核日志
堆OOM会生成日志,分配内存new的执行流程
元空间OOM会生成日志,解析类、类加载器、动态字节码 cglib
直接内存OOMunsafe bytbuffer都不会生成日志,JVM进程的堆(OS知道)
栈OOM,为什么没有听说过这个地方发生OOM开发阶段就会知道栈帧是否会发生OOM了
7.JVM异常退出
CPU占用过高如何排查?1.定位到占用CPU最高的进程2.定位到目前占用CPU最高的线程IDtop -H -p pid将线程ID由十进制转换为十六进制3.定位线程jstack pid | grep 十六进制线程id -A 300
7.OOM与调优
Java程序在运行过程中会产生大量的对象,但是内存大小是有限的,如果光用而不释放,那内存迟早被耗尽。如C/C++程序,需要程序员手动释放内存,Java则不需要,是由垃圾回收期去自动回收。垃圾回收器回收内存至少需要做两件事情:标记垃圾、回收垃圾。于是诞生了很多算法
垃圾判断算法之引用计数算法最简单的垃圾判断算法。在对象中添加一个属性用于标记对象被应用的次数,每多一个其他对象引用,计数+1,当引用失效时,计数-1,如果计数=0,表示没有其他对象引用,就可以被回收。这个算法无法解决循环依赖的问题。像Redis中就使用了这样的算法,Netty中的ByteBuffer也是如此,Python中。在该算法中,没有其他对应引用A对象和B对象,但是AB对象之间存在着互相引用,以致于垃圾收集器无法回收
引用类型无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与\"引用\"有关。在JDK1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过于狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些\"食之无味,弃之可惜\"的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。1.强引用是指在程序代码之中普遍存在的,类似\"Object obj = new Object()\"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象2.软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用3.弱引用也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用,如ThreadLocal4.虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用
对象的生存还是死亡即使在可达性分析算法种不可达的对象,也并非是\"非死不可\"的,这时候他们暂时处于\"缓刑\"阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为\"没有必要执行\
垃圾判断算法之可达性分析算法通过一系列被称为\"GC Roots\
内存池Memory Pool内存池,如果拿运行时数据区域类比的话 => 它就是JVM内存模型管理器Memory Chunk内存块 => 堆 方法区Memory Cell细胞 => 一个对象对应多个Cell为什么要有内存池?对象的频繁操作会涉及到用户态和内核态的切换。核心:避免频繁调用操作系统API去向操作系统分配内存、释放内存技术是没有绝对的,只有优点的技术。解决了一些问题,又诞生了另一些问题。在OS中,随着我们打开的进程越来越多,内存空间也变得越来越紧张,对于已经打开的内存,OS系统是不会回收的,那么OS是怎么做的呢?OS会进行类似LRU的操作,把不经常用的内存导入到硬盘空间,swap空间,但也不会无限导入,超过了swap空间,就会触发OS的OOM Killer机制。那么如果进程突然切换了,会触发缺页异常,如果在硬盘上,就导入到物理内存中,这也就是垃圾收集器的诞生背景,因为这块内存是JVM自己控制的,所以操作系统没法帮我们做,思维要严谨MAC长时间不用,硬盘空间占用会非常多,把内存置换到物理硬盘上
标记-清除算法最基础的收集算法是\"标记-清除\"(Mark-Sweep)算法,如同它的名字一样,算法分为\"标记\"和\"清楚\"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的主要不足有两个:1.效率问题,标记和清楚两个过程的效率都不高;2.空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法为了解决效率问题,一种称为\"复制\"(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一般,未免太高了了一点。现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代的对象98%是\"朝生夕死\
移动的对象为什么还可以访问?因为对象的引用不是写死的,而是动态计算出来的。1.静态存储 写死的,那么每轮GC都需要依赖中间数据结构来存储新地址,工作量会比较大2.动态计算, 动态地址是怎么计算的?计算规则:1.找到内存块的起始位置 + 数据块的起始地址 * 8字节对齐(对应的Hotspot源码中是HeapWord这个结构)get_data() + get_start() * get_align_size()eg:如图所示:0 + 3 * 8 = 24
标记-整理算法复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的时,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。因为没有多余的内存区域为老年代做担保,即便有,仍然需要有一块区域考虑到不能担保的情况。根据老年代的特点,有人提出了另外一种\"标记-整理\"(Mark-Compact)算法,标记过程仍然与\"标记-清楚\"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。缺点:内存整理是CPU密集型的,比较耗费CPU
分代收集算法当前商业虚拟机的垃圾收集都采用\"分代收集\"(Generational Collection)算法。这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外对他进行分配担保,就必须使用\"标记-清除\"或\"标记-整理\"算法来进行回收
多标 浮动垃圾GC线程已经标记了B,此时用户代码中A断开了对B的引用,但此时B已经被标记成了灰色,本轮GC不会被回收,这就是所谓的多标,多标的对象即成为浮动垃圾,躲过了本次GC.多标对程序逻辑是没有影响的,唯一的影响是该回收的对象躲过了一次GC,造成了些许的内存浪费
少标 浮动垃圾并发标记开始后创建的对象,都视为黑色,本轮GC不清除这里面有的对象用完就变成垃圾了,就可以销毁了,这部分对象即少标环境中的浮动垃圾三色标记解决的是开始垃圾收集期间数据的变动1.新创建的引用2.已有的引用间的关系变动,在漏标问题中,可能出现空指针异常,2.1 CMS 重新标记(增量更新) G1重新标记(原始快照)3.如果执行完重新标记之后,又需要回到这些新创建的白色对象的初始标记,标记阶段将永远不会结束,如果频繁在创建对象
1.读屏障+重新标记在建立A对D的引用时将D作为白色或灰色对象记录下来,并发标记结束后STW,然后重新标记由D类似的对象组成的集合重新标记环节一定要STW,不然标记就没完没了了
2.写屏障+增量更新(IU)这种方式解决的是条件二,即通过写屏障记录下更新,具体做法如下:对象A对D的引用关系建立时,将D加入待扫描的集合中等待扫描这种方式强调的是引用关系的新增对象黑色对白色的引用建立,增量更新,更新以后记录
4.实际应用CMS:写屏障+ 增量更新(效果不是很理想)G1:写屏障 + STAB最终标记阶段需要STW
漏标问题 程序会出错漏标是如何产生的呢? GC把B标记玩,准备标记B引用的对象,这时用户线程执行代码,代码中断开了B对D的引用,改为A对D的引用,但是A已经被标记成黑色,不会再次扫描A,而D还是白色,执行垃圾回收逻辑的时候,D会被回收,程序就会报空指针异常了代码表示B.D = nullA.D = ref;漏标问题是如何产生的?条件一:灰色对象 断开了白色对象的引用;即灰色对象原来的成员变量的引用发生了变化条件二:黑色对象 重新引用了该白色对象;即黑色对象成员变量增加了新的引用
三色标记把遍历对象过程中遇到的对象,按照\"是否访问过\"这个条件标记成三种颜色1.白色:尚未访问过2.黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问过了3.灰色对象:本对象已访问过,但是本对象引用到的其他对象尚未全部访问完。全部访问完,会转换为灰色为什么新创建的对象默认是黑色?不能是灰色、白色 不可能是灰色黑色:本轮GC不管白色:本来GC要管,尚未访问过,那么标记阶段将一直持续,直至用户程序不再创建新对象为止经过一轮三色标记后,对象的颜色是何时还原的?在对象移动之后,就会设置成无色
读写屏障(有点像Spring的AOP)1.读屏障(即在读前增加屏障做点事情)读屏障()读操作2.写屏障(即写的前后增加屏障做点事情)写前屏障()写操作写后屏障
三色标记与读写屏障所有的垃圾回收算法都要经历标记阶段。如果GC线程在标记的时候暂停所有用户线程(STW),那就没三色标记什么事儿了,但是这样会有一个问题,用户线程需要等到GC线程标记完才能运行,给用户的感觉就是很卡,用户体验很差。现在主流的垃圾收集器都支持并发标记。什么是并发标记呢?就是标记的时候不暂停或少暂停用户线程,一起运行。这势必会带来三个问题:多标、少标、漏标。垃圾收集器是如何解决这个问题的呢?三色标记+读写屏障
具体的设计实现?以G1为例,G1基于Region模型划分了2048个Region,每个Region是2M,总共是4G.也就是说要有2048张卡表,卡表中的每一页是512B.卡页中的1B管理4KB的内存(2M/512B=4KB),卡表:Region = 1:1.如果在4KB空间中如果存在老年代->新生代,卡页的位置标成1,卡页变成脏页。再扫描的时候只需要把4KB中的所有老年代对象拿出来扫描就解决了。当然也可以扩容每个Region的大小
记忆集(Remembered Set)、卡表(Card Table)我们知道在G1垃圾收集器中,它是把Java堆分为多个Region,那么垃圾收集是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所在:Region不可能是鼓励的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象所引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性分析确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题起始并非在G1中才有,只是在G1中更加突出。在CMS垃圾收集器中,也会存在这样的引用关系:新生代->新生代(没问题,对象要么都存活要么都死亡)、新生代->老年代(也是没问题的,无非新生代的对象存活的时间久点)、老年代->老年代(也没问题,同生共死)、老年代-> 新生代(有问题,万一新生代被回收了,会发生空指针异常)。那么如何解决这个问题的呢?答案就是利用卡表在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中的每个Region都有一个与之对应的Remembered Set都有一个与之对应的Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remebered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
垃圾回收算法由于JVM要对自己管理的对象进行回收,于是就诞生了不同的垃圾回收算法.
8.垃圾回收算法
串行、并行、并发串行:一个GC线程运行并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
STW(Stop The World)即GC线程与用户线程无法并发运行,GC线程执行期间需要暂停用户线程。比如:你妈给你打扫房间,需要把你从房间归纳出去,不然她一边打扫垃圾,你一边制造垃圾,没完没了了
ParNew收集器Serial收集器的多线程版本,唯一能与CMS收集器搭配使用的新生代收集器。相关参数:-XX:+UseConctMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器-XX:+UseParNewGC:强制指定使用ParNew-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集器线程与CPU的数量相同
SerialOldSerial收集器的老年代版本。基于标记-整理算法有两个用途:1.与Serial收集器、Parallel收集器搭配使用2.作为CMS收集器的后备方案
Parallel Old收集器Parallel收集器的老年代版本。基于标记-整理算法
不分代(暂时)单代,即ZGC[没有分代]。我们知道以前的垃圾回收器之所以分代,是因为源于\"[大部分对象朝生夕死]\"的假设,事实上大部分系统的对象分配行为也确实符合这个假设,那么为什么ZGC就不分代呢?因为分代实现起来麻烦,作者就先实现出一个比较简单的单代版本。后续会优化
ZGC的内存布局ZGC收集器是一款基于Region内存布局的,暂时不设分代的。使用了font color=\"#a23735\
ZGC的Mark0和Mark1的作用是什么?ZGC的Mark0和Mark1是ZGC并发标记阶段中的两个关键步骤,分别用于处理不同的任务,以确保垃圾回收的准确性和效率。这两个步骤的主要作用如下:Mark0阶段作用1.Mark0是ZGC并发标记过程中的第一个阶段。它主要负责标记从GC Roots可达的对象,并初始化整个并发标记过程2.在Mark0阶段,ZGC通过遍历GC Roots(静态变量、栈上的局部变量、寄存器中的引用等),将所有直接可达的对象标记为存活。这个阶段通常是短暂的Stop-The-World(STW)暂停3.Mark0阶段的标记结果作为后续并发标记的基础。它确保了所有从GC Roots开始的对象都被正确标记,避免漏标具体任务1.标记从GC Roots可达的对象2.初始化并发标记所需的数据结构3.确保初始标记阶段不遗漏任何根对象的标记Mark1阶段作用1.Mark1是ZGC并发标记过程中的另一个关键阶段,通常是并发标记的第二个阶段。与Mark0不同,Mark1主要负责处理在Mark0之后对象的引用变化,并确保这些对象能够正确标记2.在Mark1阶段,ZGC会继续堆堆中的对象进行遍历和标记,以确保所有存活的对象(包括在Mark0之后新创建或新引用的对象)都能被正确标记为存活具体任务1.并发地遍历和标记对象图中的其余对象,这个阶段通常与应用线程并发执行2.处理Mark0阶段后对象引用的变化,确保这些变化不会导致漏标3.确保整个堆中的所有可达对象在标记过程中都能被正确标记总结在ZGC的并发标记过程中,Mark0和Mark1分别承担了不同但相互补充的任务:1.Mark0主要负责从GC Roots开始的初始标记,确保垃圾回收有一个可靠的起点2.Mark1则负责继续标记堆中的其余对象,确保在并发标记过程中,没有存活对象被漏标这两个阶段共同作用,确保ZGC在进行垃圾回收时,能够准确地标记和回收内存,同时保持极低地暂停时间
CMS和G1中是采用Write Barrier来解决对象引用发生变化的,而ZGC是采用Load Barrier来解决的,通过在每次访问对象时触发读屏障,ZGC可以捕捉到应用线程引用的修改,确保即时对象的引用在标记过程中被修改,也不会导致漏标问题
ZGC是如何解决漏标问题的
ZGC参数设置启用ZGC比较简单,设置JVM参数即可:-XX:+UnlockExperimentalVMOptions 【-XX:+UseZGC】。调优也并不难,因为ZGC调优参数并不多,远不像CMS那么复杂。它和G1一样,可以调优的参数都比较少,大部分工作JVM能很好的自动完成
ZGC触发时机ZGC目前有4种机制触发GC:1.定时触发:默认为不适用,可通过ZCollectionInterval参数配置2.预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要是统计GC时间,为其他GC机制使用3.分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点4.主动触发(默认开启,可通过ZProactive参数配置)距离上次GC堆内存增长10%,或超过5分钟时,对比距离上次GC的间隔时间(49 * 一次GC的最大持续时间),超过则触发
垃圾收集器目前JVM中的收集器有九种,了解5个,详解2个。因为并发、分区管理式的收集器才是未来的趋势。注意:标记阶段标记的是存活对象,回收未被标记的对象。
CMS垃圾收集器1.如果第一轮GC还没有完成,而此时由于用户线程的继续执行导致又触发了新的GC请求,那么第二轮GC不会立即执行,而是需要等待第一轮GC完成后才会开始2.GC触发机制当队中的老年代使用量达到一定阈值时,会触发CMS GC以回收内存。如果在一次CMS GC还没有完成时,用户线程继续分配对象,导致老年代再次达到触发阈值,理论上需要再次进行GC2.CMS GC 重入性CMS垃圾收集器本身并不支持\"重入性\",即它同时进行多次垃圾收集。CMS的设计中,一次GCGC需要完成它的所有阶段后,才能开始新的GC。因此,如果在一次CMS GC还没完成时,用户线程的内存分配再次触发了GC请求,那么新的GC请求将会背延迟,直到当前的CMS GC完成3.并发模式失败(Concurrent Mode Failure)如果在CMS执行的过程中,用户线程分配内存速度过快,导致老年代空间不足,无法等待CMS完成,此时JVM会触发\"并发模式失败\"(Concurrent Mode Failure)这种情况下,JVM会切换到一个单线程的\"Searial Old\" GC执行一次Stop-The-World(STW)全堆回收,以保证系统能够继续运行,Searial Old GC是一种较慢但确保回收的垃圾收集方式
G1垃圾收集器G1收集器在一次GC尚未完成时,如果又触发了新的GC请求,第二次GC不会打断第一次GC,而是会在第一次GC完成后开始。这一点与CMS类似,但G1的优势在于它的设计更灵活,能够更好地控制GC暂停时间并减少进入Full GC的概率。
ZGC垃圾收集器1.如果在ZGC正在进行一次垃圾回收时(例如正在进行并发标记或并发重定位),用户线程分配新对象的速度很快,导致需要再次进行垃圾回收,ZGC不会因为新的GC请求而停止当前的GC操作。相反,他会继续完成当前的GC操作,同事计划和开始新的GC周期2.由于ZGC的设计理念是\"全并发\",因此它能够非常灵活地处理多个垃圾回收周期的重叠情况。新的GC周期可以与旧的GC周期重叠执行,而不会导致显著的暂停时间增加
在并发垃圾收集器在第一次GC没有进行完,可以发起第二次GC吗?
枚举根节点从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项工作必须在一个能确保一致性的快照中进行——这里\"一致性\"的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对应引用关系还在不断变化的情况。该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程(Sun将这件事称为\"Stop The World\
安全点(如果线程随便哪个位置都可以停下来,这个问题就会简单很多)在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实地问题随之而来:可能导致引用关系变化,或者说OopMap内容变化地指令非常多,如果为每一条指令都生成OopMap,那将需要大量的额外空间,这样GC的空间成本将会变得很高。实际上,HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在\"特定位置\
安全区域使用Safepoint似乎已经完美地解决了如何进入GC的问题,但实际情况却并不一定,Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint.但是,程序\"不执行\"的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,\"走\
为什么需要STW? 安全点、安全区域又是什么?暂停线程 暂停所有有可能导致引用关系变动的线程为什么要暂停? 如果引用关系一直在变的话,GC不干净遍历线程,发起挂起信号。主动式:JVM的实现方式,借助安全点实现抢先式: 给每个线程发送暂停信号在漏标问题中,对于引用发生变化的对象,它会被保存到OopMap里面记录线程阻塞前需要更新OopMap,那么什么时候记录呢?
9.垃圾收集器与GC日志
JVM中的执行引擎是什么?
AOT提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理论上,提前编译可以减少即时编译带来的预热时间,减少Java应用长期给人带来的“第一次运行慢\"的不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施。但是提前编译的坏处也很明显,它破坏了Java\"—次编写,到处运行\"的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包;也显著降低了Java链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能在运行期才确定,否则就只能舍弃掉己经提前编译好的版本,退回到原来的即时编译执行状态。AOT的优点在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗可以在程序运行初期就达到最高性能,程序启动速度快运行产物只有机器码,打包体积小AOT的缺点由于是静态提前编译,不能根据硬件情况或程序运行情况择优选择机器指令序列,理论峰值性能不如JIT没有动态能力同一份产物不能跨平台运行参考文章https://cloud.tencent.com/developer/article/2228910
Java被称为\"半编译半解释型\
Java为什么是半编译半解释型语言?分成两个角度来看1.触发JIT之前javac编译,java运行2.触发JIT之后,运行期即时编译(C1、C2)+解释执行(模板解释器比字节码解释器高效很多)
字节码解释器做的事情是:Java字节码->C++代码->硬编码,比如说一条_new指令,字节码解释器bytecodeInterpreterC++代码中有很多跟new指令无关的才能到目标代码,好比你要执行一条字节码指令_new,字节码解释器需要执行很多操作,比如说要读取字节码文件对应索引的指令,才能到真正这条字节码需要做的事情,比较繁琐。一个字节码指令占一个字节,2^8-1=255,所以最多只能有255个
两种解释器的底层实现,JVM中目前来说有两种解释器
-Xint
-Xcomp
-Xmixed
为什么没有用纯编译模式?首次启动的时候要生成所有执行流,可能是担心生成执行流的时间比较长,用mixed的话,可以字节码解释器一边运行一边用热点代码编译
JVM三种运行模式JIT为什么能提升性能呢?原因在于运行期的热点代码编译与缓存JVM中有三种两种及时编译器,就诞生了三种运行模式1.-Xint:纯字节码解释器模式2.-Xcomp:纯模板解释器模式3.-Xmixed 字节码解释器+模板解释器模式(默认)
Server Compiler则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-O2参数的优化强度,它会执行所有经典的优化动作,如无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表达式外提(Loop Expressin Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块重排序(Basic Block Reordering)等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、空值检查消除(Null Check Elimination,不过并非所有的空值检查消除都是依赖编译器优化的,有一些是在代码运行过程中自动优化了)等。另外,还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的基金优化,如守护内联(Guarded Inlining)、分支频率预测(Branch Frequency Prediction)等。Server Compiler的寄存器分配是一个全局着色分配器,它可以充分利用某些处理器架构(如RISC)上的大寄存器集合。以即时编译的标准来看,Server Compiler无疑是比较缓慢的。但它的编译速度依然远远超过传统的静态优化编译器,而且它相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销,所以也有很多非服务端的应用选择使用Server模式的虚拟机运行JIT编译过程本来就是一个虚拟机中最体现技术水平也是最复杂的部分,不可能以较短的篇幅就介绍得很详细,另外,这个过程对Java开发来说是透明的,程序员平时无法感知它的存在。
方法调用计数器触发即时编译
查看及分析即时编译结果(需要开启VM 参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining)```javapublic class JITTest { public static final int NUM = 15000; public static int doubleValue(int i) { // 这个空循环用于后面演示JIT代码优化过程 for (int i1 = 0; i1 < 100000; i1++); return i * 2; } public static long calcSum() { long sum = 0; for (int i = 0; i < 100000; i++) { sum += doubleValue(i); } return sum; } public static void main(String[] args) { for (int i = 0; i < NUM; i++) { calcSum(); } }}```带%的输出说明是由回边计数器触发的即时编译,可以看到calcSum()和doubleValue()方法都已经被即时编译,并且还看到了doubleValue方法已经被内联编译到calcSum()方法中
热点代码缓存区热点代码缓存时保存在方法区的,这块也是调优需要调的地方server编译器模式下代码缓存大小起始于2496KBclient编译器模式下代码缓存大小起始于160KB
热机且冷机故障(热机在崩溃的边缘)新加的机器,流量切过去之后,就挂了。热机运行了很长时间。冷机才刚运行不久。冷机字节码解释器模式,CPU升高就挂了原因:Java刚启动时,一段时间触发JIT之后,性能才会达到最高。怎么解决呢?1.缓缓的切流量,慢慢测2.加更多的机器。JIT触发,会将多个执行流合并
开始栈上分配,创建100w个对象,HSDB查看对象的数量,只有11w个对象
关闭栈上分配
10.执行引擎、JIT、逃逸分析
Javaboolean 1Bbyte 1Bchar 2Bshort 2Bint 4Blong 8Bfloat 4Bdouble 8Boop 4B | 8B(取决于操作系统的位数、指针压缩是否开启)C++char 1Bshort 2Bint 4Bfloat 4Blong 8Bdouble 8B指针4B、8B(取决于操作系统的位数)
数据在内存中是如何存储的?操作数栈/局部变量表中的一个格子称为一个插槽(Slot).虚拟机栈会对数据类型进行封装,在HotSpot源码中会封装成StackValue结构,其中有一个type属性会表示类似的0x44332211是内存地址还是一个数值,用来帮助GC Root分析C++在编译的时候,汇编代码已经区分好了数据占多少个字节
内存的存储模式1.直接用int(32bit) long (64bit) 浪费内存2.byte数组 C++全部用这种方式实现的,在汇编层面就确定了,这个数据类型3.Object int、long(Java特有的)4.混合用数据1.小于4B的全部用int存储2.double用两个int存储3.引用类型全部用object存储
常量池项内容如图所示,我们可以看到它是把double类型分割成了高低位
对于8字节的数据类型,在Java常量池中占两个Slot,double的存储时分成了两个高低位,对于常量池解析时,字节码是拆开了,但是我们在自己编写时可以进行合并起来。不一定非要和HotSpot源码一样,存的时候是合起来,取的时候也是合起来取。如代码所示,我们定义了一个值为10的double类型变量a,ldc2_w字节码指令将常量池推送到了操作数栈上,我们可以跟着去看下它的常量池项存储的内容。
13.让JVM有自己的数据类型
double常量池解析解析常量池时已经合并,真正在用的时候,就是在处理合与分字节码指令解析、编译系统、JIT都是按照4B来设计的,这个坑迟早要填的
虚拟机栈和OS的栈之间的关系(寄生关系)虚拟机栈本身是OS栈,每个Slot其实是8B,但是JVM把它当4B来用。寄生对象是Call Stub,它会创建OS栈,先申请一段OS栈,然后再延申一段JVM的变量用来存储。执行原生栈时,才会创建OS栈,JVM执行main方法是不会生成OS栈的
byte++和i++哪个效率更高?int++只对应了一句字节码指令,而byte++对应了五句字节码指令。性能会有5倍的差异。
iinc指令拓宽Intel CPU是怎么解决指令集不够用的问题呢?最开始其实也只有一个字节,后面扩展成二级指令,现在已经到三级指令了。所以其实你的知识面越底层,真正的理解底层,你就能预测出未来可能到来的变化:比如这里说的,JVM当前的以及指令不够用了,它也要引入二级指令。这就是阿里P8的硬要求之一:技术前瞻性。iinc指令不管是i++还是i--操作,编译后对应的字节码指令都是iinc指令。如图所示,iinc字节码结构如图所示。1.IINC:对应的是字节码指令2.Slot Index对应的是这个指令作用的局部变量在局部变量表中的索引3.step 一次加多少。固定值为1,如果这个值不存在特殊考虑,这个字节可以节省下来。这个要看JDK后面会不会有这个计划超级加倍不知道大家有没有发现一个问题,iinc指令的第一个参数,即代表slot index这个参数,只有一个字节,这就是i++、i--存在的约束。这个约束就是如果局部变量的索引超过128,就只能走byte++那种复杂的逻辑。那么JVM是怎么做的呢?JVM做了指令拓宽,增加了一个字节位
```java// 代码1int i = 1;System.out.println(i++);// 代码2float v = 1;System.out.println(v++);```为什么代码1的执行效率高,因为代码1是专属指令iinc 0 by 1 局部变量表中index=0的位置加1,写回去iinc就是加1,为什么还要写出来?原因在于iinc不只是用于++操作还可以用来--操作、+=操作,这也是JVM设计者的精髓巧妙之处(还节省了一个字节码指令的位置),如图所示。操作步骤如下:1.第一个操作数: 拿到slot的index2.第二个操作数: 增加或减少的步长3.完成运算4.写回局部变量表为什么说JVM的字节码指令位置是很紧张的呢?因为一个字节码指令设计的时候只给了一个字节,一个字节的话,无符号的话,最大是255,言外之意,JVM的字节码指令最多只给255个,现在已经用了202个
14.让JVM能进行运算
代码所示
诞生的背景研究一个伟大的技术,不了解它的过去不足以更好地理解它的现在甚至它的未来。我们先来看看Lambda表达式时如何一步步在JVM中生长出来的。在JDK8之前,我们想使用某个接口实现类,要么提前写好实现类,要么使用匿名内部类的方式。匿名内部类代码如下```javainterface GreetingService { void greet(String message);}public class Main { public static void main(String[] args) { // 使用匿名内部类实现GreetingService接口 GreetingService greetingService = new GreetingService() { @Override public void greet(String message) { System.out.println(\
添加下面的JVM参数,可以dump出Lambda表达式生成的对象-Djdk.internal.lambda.dumpProxyClasses
1.常量池中会有一项:JVM_COnstant_InvokeDynamic_info
2.类属性里面会有一项:BootstrapMethods
JVM_CONSTANT_MethodHandle_info(LambdaMetafactory)
JVM_CONSTANT_MethodHandle_info(lambda$main$0)
JVM_CONSTANT_MethodType_info
4.还会多出一个方法。这个方法是编译器自动生成的。所以可以这样说,Lambda表达式的实现,是编译系统与运行系统互相配合实现的
5.Lambda表达式的调用指令是invokedynamic
字节码层面上面代码写出的lambda表达式,对应的字节码文件会多出一些内容
2.从常量池项JVM_CONSTANT_InvokeDynamic中拿到BootstrapMethods的索引。即我们目前调用的是第几个BootstrapMethod
3.BootstrapMethod结构中的Bootstrap方法,对应的常量池项是MethodHandle.JVM就是通过执行LambdaMetafactory.metafactory创建出CallSite,进而创建出Lambda表达式对应的对象的。BootstrapMethod结构中的其他信息,都是一些辅助信息,是调用mefafactory方法需要传的参数<java/lang/invoke/LambdaMetafactory.metafactory : (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;>
4.JVM也是通过执行LambdaMetafactory.metafactory完成Lambda表达式对应的函数式接口与具体实现中的代码的关联。背后的实现原理就是通过字节码,我们可以使用-Djdk.internal.lambda.dumpProxyClasses可以将生成的类保存到文件中。看下生成的文件内容
总结来说就是JVM通过BootstrapMethod找到LambdaMetafactory.metafactory并执行,完成Lambda表达式对应的函数式接口与具体实现中的代码的关联,默认的,在内存中会生成一个新的类,并返回这个类的实例,所以,可以这样调用run方法```javaobj.run(1);```
如何实现调用
15.让JVM支持Lambda表达式
四种异常处理的方式1.int a = 1/ 0; 直接不管2.代码throw new RuntimeException ATHROW指令3.try catch4.方法签名抛出throws当使用try catch捕获异常时,Method code会有一些变化,异常表会存储,多一个goto指令,存储异常信息
16.让JVM支持异常处理
GC的发展1.内存结构的发展一开始是一块整堆,2.GC算法的发展,由于Java是自动垃圾回收,所以回收算法效率会很重要标记-清除:容易发生碎片化标记-整理:它虽然解决了碎片化问题,但是它是一个CPU密集型,耗时长短和堆大小有关。标记-复制:所以后面内存结构就划分为了两半,采用复制的方式来转移存活对象,但是发现内存空间浪费太严重,于是有人根据对象的存活周期和统计学理论划分除了新生代与老年代。也就有了新生代8:1:1的划分Region模型的由来G1如果想要做到可预测停顿的话,那么内存空间就不能太大,如果不重新设计模型,当扫描大块的内存空间时将会耗费很长时间,所以它必须重新规划堆区,这也就有了Region模型,每个Region固定大小2M再想提升效率,那么将就得多开几个线程来解决比如串行回收调整成并行回收再到后面再调整成并发回收
安全点的插入位置
Linux中的信号类型
HotSpot注册Linux信号函数的地方
HotSpot识别空指针和STW的地方
HotSpot在init_2方法中对poling-page进行赋值的
poling_page就是一块内存地址
HotSpot是如何存储所有线程的
HotSpot会在线程创建的时候放入到线程链表中
更改polling_page
三色标记算法理论+STW原理剖析
代码示例字节码含义iconst_'i'将相当于'i'部分的int类型的常量添加到操作数栈中istore_'n'将操作数栈头部的int类型的值保存到局部变量数组的第'n'个元素中new创建一个新的对象并将其添加到操作数栈dup复制操作舒展头部的值并将复制出的值添加到操作数栈中(在new 对象时,是会给this指针赋值的)invokesepcial调用实例的初始化方法等特殊方法astore_'n'将操作数栈头部的引用类型的值保存到局部变量数组的第'n'个元素中return从方法中返回void
BasicBlock的结构,生成的OopMap是以BasicBlock为单位的,每个执行块都有一个OopMap
当一个对象及其引用关系被遍历完之后,会OopMap中放入到black table(黑色对象集合当中)而当一个对象仅仅本身被扫描完,则会从OopMap中放入到gray table中
漏标是如何记录的?G1:是利用写前屏障+原始快照的方式将oop放入到一个队列里面CMS:利用写后屏障+增量更新的方式写入到脏页当中这个引用关系的变动是被记录在线程的一个属性队列里面的
手写三色标记算法
JVM源码(Java是半编译半解释型语言)
0 条评论
回复 删除
下一页