赞
踩
java虚拟机总结将使用HotSpot VM
JVM 是 java虚拟机,是用来执行java字节码(二进制的形式)的虚拟计算机,一般有以下5个模块:
我们知道加载是将class文件的数据加载进方法区;那么class到底由什么信息呢?规定:
·无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
·表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表由下列数据项按严格顺序排列构成
分别为:魔数、副版本号、住版本号、常量表、访问标志(access_flags)【这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等】、类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引表(interfaces)是一组u2类型的数据的集合,因为前面是接口索引个数【这也是为什么可以实现接口可以多个,但是只能继承一个父类】;后面就是字段表、方法表和属性表【为了让类能在某些场合使用】;
class在编译后会有一系列重要的信息:魔数,主副版本号,常量池等;
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件
常量池:也称class常量池或金泰常量池由俩部分组成:常量池计数器(constant_pool_count):记录常量个数,和常量池项(存储常量实体);
常量池计数器是由1开始计算的,常量池项结构非常简单可以简单概括为一个结构体
cp_inf{
//常量类型
u1: tag;
//常量数据
u2: inf[];
}
常量类型主要有20种:
1.String、Int、Float、Long、Double
String由俩个部分组成:第一个是CONSTANT_String_info第二部分是CONSTANT_Utf8_info;其中CONSTANT_Utf8_info是真正保存数据的所以会由一个length字段,而CONSTANT_String_info只保存指向对应的CONSTANT_Utf8_info的索引;
2.实体的Feild、Method(类中方法和接口中方法)
3.符号引用
【链接的过程和计算机的链接阶段几乎一样】
**验证:**确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性;主要包括四个验证:格式验证、语义验证、字节码验证、符号引用验证;
格式验证:魔数检查、版本检查、长度检查
语义验证:包括抽象类接口的实现类是否已经实现相关方法,final关键字是否被继承
字节码验证:主要是跳转指令是否正确
符号引用验证:主要是符号引用的直接引用是否存在
准备:主要就是为类变量赋值,换句话说就是为方法区的该类的非final类变量赋值【这也是为什么类变量有初始值的原因】;
解析:将常量池中的符号引号转换为直接引用的过程(简言之,将类、接口、字段和方法的符号引用转为直接引用);这是由于java没有像cpp那样有一个专门的链接完成这个步骤,所以会有符号引用;
类初始化阶段:使用为类变量赋予正确的初始化值
Java类型初始化过程中对static变量的初始化操作依赖于static域和static代码块的前后关系,static域与static代码块声明的位置关系会导致java编译器生成方法字节码。
若该类具有父类,Jvm会保证子类的< clinit >() 执行前,父类的< clinit >() 已经执行完成。
初始化阶段就是执行类构造器方法< clinit >()的过程。此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码快中的语句合并而来
不会为所有的类都产生< clinit>()
初始化方法;如果没有静态变量、代码块或者静态变量不需要赋值则不会产生,特别的:对于静态常量,JVM尽可能的将其赋值在准备阶段完成显式赋值,但是如果是涉及到引用则会等到初始化阶段才赋值【也就是说只有这种情况下才会产生< clinit>;使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。】,如果其调用方法赋值的则会到该阶段才赋值;
clinit()的调用会死锁吗?
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕
正是因为函数()带锁线程安全的,因此**,如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁**。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息
什么时候回触发类初始化?【即调用clinit()】
对于类的二进制数据流,虚拟机可以通过多种途径产生或获得(只要所读取的字节码符合JVM规范即可)
1.虚拟机可能通过文件系统读入一个class后缀的文件(最常见)
2.读入jar、zip等归档数据包,提取类文件。
3.事先存放在数据库中的类的二进制数据
4.使用类似于HTTP之类的协议通过网络进行加载
5.在运行时生成一段Class的二进制信息等
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和JAVA类加载器(Defined ClassLoader)
启动(引导)类加载器 BootstrapClassLoader【相当于计算机的引导程序】
①. 这个类加载使用C/C++语言实现的,嵌套在JVM内部;所以就省略。
②. 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容),用于提供JVM自身需要的类(String类就是使用的这个类加载器)
③. 由于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
④. 并不继承自java.lang.ClassLoader,没有父加载器
⑤. 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
SecureClassLoader:继承自ClassLoader,添加了关联类源码、关联系统policy权限等支持。
URLClassLoader:继承自SecureClassLoader,支持从jar文件和文件夹中获取class,继承于classload,加载时首先去classload里判断是否由bootstrap classload加载过,1.7 新增实现closeable接口,实现在try 中自动释放资源,但扑捉不了.close()异常【一般我们扩展用户类加载器就是基于这个类,例如热部署https://www.cnblogs.com/lichmama/p/12858517.html】
扩展类加载器 ExtClassLoader【相当于计算机的第二阶段的引导程序】
①. Java语言编写,由sum.music.Launcher$ExtClassLoader实现
②. 派生于ClassLoader类,父类加载器为启动类加载器
③. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
应用程序(系统)类加载器 AppClassLoader
①. java语言编写,由sum.misc.Launcher$AppClassLoader实现
②. 派生于ClassLoader类,父类加载器为扩展类加载器
③. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
④. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
⑤. 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
自定义类加载器
①. 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们换可以自定义类加载器,来定制类的加载方式(自定义类加载器通常需要继承于 ClassLoader)
②. 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java 开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源
③. 自定义 ClassLoader 的子类时候,我们常见的会有两种做法:
重写loadClass()方法(不推荐,这个方法会保证类的双亲委派机制)
重写findClass()方法 -->推荐
所有java的类加载器都是基于ClassLoader,关系图如下:【扩展类加载器没有重写loadClass方法,而AppClassLoader的loadClass中同样调用了:parent.loadClass(name, false),使其符合双亲委派机制】
类加载器间的协作关系:【从上面可以知道ExtClassLoader并没有重写URL的ClassLoader的loaderClass方法,所以调用AppClassLoader的符类加载器就是调用ExtClassLoader,具体看java11的Launcher的155和216行】
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先检查是否加载,如果没有首先把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求,子加载器才会尝试自己去完成加载。【代码如下的LoaderClass.loadClass】
java并没有要求一定要是双亲委派机制,但是双亲委派机制无疑具有许多好处;
loadClass
//换句话说默认loadClass只会加载 public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } //resolve==true,加载class的同时需要进行链接 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { //同步操作,保证只能加载一次 synchronized (getClassLoadingLock(name)) { //在缓存中判断是否已经加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); //先进行双亲委派机制实现,1.查看是否有双亲--》2.尝试在双亲中加载 try { if (parent != null) { c = parent.loadClass(name, false); } else { //parent==null 父类加载器是引导类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } //如果父类加载加载失败才会使用,则进入本类加载器 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 调用当前classloader的findClass; //查找具有指定二进制名称的类。 //这个方法应该被遵循加载类委托模型的类加载器实现覆盖,并且在检查请求类的父类加载器后将由loadClass方法调用。 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } //检查是否需要链接 if (resolve) { resolveClass(c); } return c; } }
为什么需要类加载器而且是多级类加载器【使用双亲委派机制】:显然类加载器就是将class字节码文件加载进java虚拟机;同时为了安全,避免java的核心api被修改替换,我们知道每一级类加载器都会加载相应部分的类库的类,这样就可以避免个人篡改类库的类;而且,使用双亲委派机制也可以避免对于类的重复加载;
为什么父类加载器不能获取感知子类加载器加载的类,但是子类加载器可以感知父类加载器加载的类;这和类加载器的实现有关,从上面我们知道每次他都指只会检查本类加载器加载的类的缓冲不会检查子类的,然后类加载器使用的是双亲委派机制就是使得子类加载器可以看到父加载器的,但是父类加载看不到子类加载器加载的;
双亲委派机制的优缺点:很显然双亲委派机制使得java更加安全、健壮、即避免核心api被破坏又节省内存空间;但是正是由于其双亲委派机制是单向的使得如果希望父类加载器可以加载子类加载器不能实现;
怎么打破双亲委派:本质就是打破双亲委派机制的逐层传递的原则;一般有俩种实现,父加载器调用子加载器【就是反转:TTCL线程上下文容器类加载器】、还有就是形成网状的【就是平级:OSGi】;
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为
Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实
现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更
加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将以java.*开头的类,委派给父类加载器加载。
2)否则,将委派列表名单内的类,委派给父类加载器加载。
3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器
加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类查找失败。
首先介绍线程私有的三个部分
然后就是线程共有的俩个部分:【这里jdk1.8前后差距有点大】
虚拟机栈的基本单位是栈帧,栈帧主要由:局部变量表,操作数栈,动态链接,返回地址、附加信息等五个部分组成;每个栈帧对应一个方法,可以这么说java程序的执行就是虚拟机栈的栈帧的进出栈【这里并不是说所有的方法都会进入虚拟机栈,显然本地方法和static方法都不会进入】;
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析(静态分派,显然方法的重载属于静态分派)。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接(动态分派,方法的重写属于动态分派)。
正如前文堆类加载得分析:Java代码在进行Javac编译的时候,并不像C和C++那样有“链接”这一步骤,而是在虚拟机加载Class文件后进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,在类创建时或运行时解析、翻译到具体的内存地址之中。
返回地址:很显然当前java得返回主要有俩种,异常中断抛出和正常返回;方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。方法异常退出时,返回地址是要通过异常处理器表来确定的;
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:当前栈帧出栈,恢复上层方法的局部变量表和操作数栈【显然在Java虚拟机栈中弹栈后栈顶完成了恢复了】;把返回值(如果有的话)压入调用者栈帧的操作数栈中【一般而言有返回值都会有该操作,因为后面往往伴随赋值或者其他操作均会使用到操作数栈】,调整PC计数器的值以指向方法调用指令后面的一条指令等;
堆按照对象生存年代【新生和老年】和大小可以分为三部分:eden、survivor1、survivor2【上面三个区保存新生代,俩个survivor可以认为时中年】、older四个区【老年期】
四个区的大致作用如下:
java的内存模型由俩种
为了避免线程在创建对象的时候加锁,一般而言还会将eden分为俩部分线程区eden和公用eden;线程区eden又分给若干个线程,这样每个线程都有自己的eden区【TLAB Thread Local Allocation Buffer】,在创建对象的时候就不要同步;
下图为对象实例化简图:具体看对象篇
配置
配置新生代与老年代在堆结构占比
默认:-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
调整这个空间比例
-XX:SurvivorRatio:(Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1)
-Xmn:设置新生代最大内存大小,一般使用默认值就可以了
大对象配置
-XX:PretenureSizeThreshold 超过大对象就会直接再老年区创建而不在eden创建
. -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:HandlePromotionFailure:是否设置空间分配担保
(JDK6之后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC)
对象由eden向older过程
我们知道堆主要保存java对象实例,那么java堆的数据保存主要分析的也是对象到底是怎么存在的;
对象在堆中包括三个部分组成:对象头(Object Header)、实例数据(Instance Data)、对齐填充;
对象头:对象头主要由标志字段(Mark Word)和类型指针组成,很显然类型指针和前面的reference斗志由虚拟机决定其为8个字节还是16个字节【jvm支持对于指针的压缩】,jvm固定了标志字段页根据虚拟机不同为8或者16个字节;这样对象头就占用了16或者32个字节;
标志字段(mark word):用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据;这里需要注意由于mark word是固定大小的,这就有一个问题mark word可能不够用,jvm的Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据;例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向) [1] 下对象的存储内容如表2-1所示
类型指针:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例;并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身【这就话就是说对于对象的定位有俩种策略,直接定位【就是像这里再对象中添加一个字段:类型指针】和间接定位【句柄池的方式】;】
句柄:java堆会划分出一块内存来作为句柄池,reference中存储对象的句柄地址,而句柄中又包含了实例数据与类型数据各自的具体地址信息。优式:稳定
实例数据:是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容;
HotSpot虚拟机默认的分配策略如下所示:【相同宽度的字段总是被分配到一起,并且在满足这个条件的前提下,在父类中定义的字段会出现在子类字段之前】
对齐填充:HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,所以对象的大小也必须为8的整数倍;对齐填充可以认为是为了方便对于内存的管理;
没有发生逃逸:当一个对象在方法中被定义后,对象只在方法内部(或者本线程内使用)使用(这里关注的是这个对象的实体)。没有逃逸的对象可以通过:栈上分配、标量替换和同步省略:加快对象的创建回收和使用;而且由于其在栈上实现显然其不需要经过GC垃圾回收即可自动回收;
逃逸:当一个对象在方法中被定义后,它被外部所引用程度可以分为:不逃逸(只在本线程本方法能够访问)、线程逃逸(外部方法和其他线程均能使用),方法逃逸(其他线程不能引用,尽管本线程其他方法可以访问)。对于不逃逸及方法逃逸均可能使用栈上分配;
JDK1.7版本之后,HotSpot中默认就已经开启了逃逸分析;
配置
-XX:+DoEscapeAnalysis 显式开启逃逸分析
-XX:+PrintEscapeAnalysis 查看逃逸分析的筛选结果、
同步消除(Synchronization Elimination):线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉。
jdk1.8的方法区对于运行时常量池和字符串常量池的位置网上没有明确答案,深入理解java虚拟机也比较含糊。结合深入理解java虚拟机和网上的基本论断,我认为运行时常量池逻辑上是包含字符串常量池的,但是运行时常量池的非字符串部分不在虚拟机内存中而是在元空间,堆中是字符串常量池 【深入理解java虚拟机的:1.到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出;2.Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。3.运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常
量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。】
方法区主要分为俩个部分java head的字符串常量池和和元空间
首先是字符串常量池:StringTable类,它是一个Hash表,默认值大小长度是1009;使用链地址法处理冲突【在JDK7.0中,StringTable的长度可以通过参数指定。】;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
元空间还保存着其他的信息,包括类型信息、feild信息、方法信息和jit代码缓存;
有了以上基础:大致可以总结如下:
其实对象得定位取决于对象得访问方式:直接访问或者间接访问;直接访问显然reference就可以直接定位到对象得实例变量;还有一种是句柄池得方式,先去句柄池中找到对象得实例指针到真正得实例变量;下面分别是间接访问和直接访问示意图
java对于对象是否存活得判断使用得是可达性分析,并不是通过计数器计数得办法;【计数器计数有一个最大得问题是可能有已死得对象循环引用导致不能失活,更严重得是一个见循环引用可能会使得绝大多数死亡对象不能正常销毁,例如a->b b->a a->c a->d b->h;上面得对象都只有着部分得引用】
java使用的是可达性分析来解决计数器无法解决循环引用的问题;这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集【着意味着GC roots是多个的】,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达。
根节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain);
GC Root对象一般可以简单分为俩大类:不死的对象和普通的对象【前面的4个都都是普通对象,一般很可能随着线程死亡而消失;后面的2个几乎不会死亡,方法区常量引用的对象死亡条件比较苛刻】
除此之外还有一些各个虚拟机根据相应回收算法持有的临时对象;
引用分为:强引用,软引用,弱引用,虚引用;
对象的死亡有俩次标记:1.在可达性分析算法中判定为不可达,进行第一次标记;2.如果对象执行过finalize方法或者没有重写该方法进行第二次标记;否则将其移到F-Queue等待该线程执行F-Queue的对象的finalize方法【注意虚拟机并不保证该方法一定会被执行或者执行完成,收集器会对该队列进行一次小规模回收】,没有重新可达的将进行第二次标记;俩次标记后就标志对象已死【另外绝对不推荐使用finalize进行复活,因为具有不确定性和对虚拟机的性能消耗比较大】
有了上面的基础后我们可以比较好的理解垃圾回收机制【就是内存回收和整理嘛】:
我们知道通过一系列的GC root就可以把对象是否可达找出来,但是这里涉及一个重要问题GC Root的所有根节点怎么找出来,这里涉及三个部分:虚拟机栈到堆空间,堆空间的同代引用和堆空间的跨代引用
对于虚拟机栈到堆空间:
1.oopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。 这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。
2.每个方法可能会有好几个oopMap,就是根据safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。
3.循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。
堆空间的跨代引用:
卡表(记忆卡的实现)在HotSpotVM中以数组形式存在,数组每个元素对应的内存块称为卡页;一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0;如何标记脏页?一般来说就是当有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,所以需要把维护卡表的动作放到每一个赋值操作之中;通过写屏障实现,这里的写屏障就是一个类似AOP的环绕通知,简单来说:进行写操作(即引用赋值)之前或之后附加执行的逻辑;
安全点选择:安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的【方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点】;
为了在垃圾回收器进行遍历GCRoot图的可达性分析的时候尽可能的能够并发的处理业务线程【即在进行查找回收对象的时候,不中断用户线程】,为此引进三色标记法:三色标记法规定:如果垃圾回收器没有遍历到的对象为白色、已经访问过但是其引用对象还没全部访问过为灰色,已经访问过且其全部子对象都访问过为黑色; 三色标记法存在漏标黑色和多标黑色的问题【这时不得不说一下赋值器,我们知道:新建引用最后到达安全点的时候会将其加入Oomap,对于赋值器如果不是新建引用而是赋值引用【或者引用的引用……】必然是GCRoot可达的【不然你怎么找到该引用并然后赋值?】】;漏标:漏标就是用户线程在黑色节点上进行赋值引用并且恰好只有收集器标记节点的引用指向该节点而且该节点还未访问又被删除了该节点;
其中漏标非常严重和危险;但是漏标必须满足以下俩个条件:
由于必须满足俩种条件,所以有俩个解决思路:增量更新和原始快照;
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的;
怎么更新引用?
标记复制算法:很明显标记复制算法会改变对象的内存地址所以标记复制算法需要更改引用的地址【怎么更改呢?】;标记复制算法是在标记的过程复制的【换句话说这个时候就可以更改引用地址】,然后将地址保存到对象头的MarkWord中,下次又有新的节点指向该节点时就可以借用MarkWord直接更改引用地址而不是去复制;
标记整理算法:简单来说就是首先标记,最后将存活对象复制到新空间;,在整理前先计算出所有对象保存的新位置保存在MarkWord,然后通过修改所有的引用
首先垃圾回收器的类型主要有:【很显然他们都是作用在堆上的】
在开始介绍俩个重点的垃圾回收器前,现在重点关注另外5个回收器;
Serial的运行过程示意图,Serial Old就是将新生代换为老年代
Parallel Scavenge
Parallel Old
https://blog.csdn.net/qq_38350925/article/details/104957595
每个region总体不严格属于哪一个分代,只在其在存有数据时变为只属于一个分代;这使得各个年龄带不再是连续的;
每个区有三个指针 preTams、nextTAMS、top指针,其中top指针指向当前分配到的位置,在 GC 过程中新分配的对象都当做是活的,G1 在 Region 中通过 top-at-mark-start (TAMS) 指针来解决这个问题,分别使用 prevTAMS 和 nextTAMS 来记录新分配的对象。
https://blog.csdn.net/qq_43295483/article/details/120243977
hostVM的执行引擎主要是即时编译器和解释器,可以说java是一门半解释半编译的语言;默认情况下都是同时具有解释器和即时编译器的
重点介绍即时编译器:java的即时编译器有三种:C1【客户端即时编译器】、C2【服务端即时编译器】、Graal【jdk10新进即时编译器】
在了解了其特点后重点关注其实现的过程:client即时编译器是一个三段式的编译器;
什么时候使用即时编译器什么时候使用解释器在hostpotVM中是通过热点探测实现的,所谓的热点探测就是根据代码执行的频率确定其是否为热点代码,对于热点代码使用即时编译器否则使用解释器;
所谓的回边计数器就是对于循环的循环次数进行统计,超过一定的循环次数就会使用即时编译器堆代码进行编译;其执行过程可以大致认为如下:
方法计数器和回边计数器的思想一致,不过由于方法执行的时间具有跨度性,所以可以分为俩种:绝对次数的方法计数器和相对次数的方法计数器;默认情况使用相对次数的方法计数器;
相对次数的方法计数器:在设置的时间跨度内如果执行次数超过阈值就会使用即时编译器编译代码,否则就会热度半衰【就是热度减半】,一般这个时间跨度称为半衰期;
绝对次数的方法计数器:等到执行次数超过阈值就进行编译,一般所有方法最后都会被编译;
阈值设置
执行示意图
PS:后续所有开源学习笔记同步到gitee,有需要去拉取 https://gitee.com/wusport/open-source-notes
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。