JVM知识总结

2 篇文章 0 订阅

JVM知识点总结

什么是jvm

jvm就是java虚拟机,本质上是一个程序虚拟机。所以我们首先得搞懂什么是虚拟机,虚拟机是在操作系统上层的一款软件,分为程序虚拟机和系统虚拟机,程序虚拟机就是用于执行程序的,系统虚拟机可以用于模拟一台物理设备。jvm是一台程序虚拟机用于执行字节码文件。

jvm怎么工作的

程序首先经过词法分析,分析出关键字,变量和常量,接着进行语法分析分析出程序执行的语法树。接着生成字节码文件。字节码文件就是java程序和虚拟机之间沟通的桥梁。jvm的主要构成部分分为三层分别为类加载层,运行时数据区和执行引擎。jvm通过解析字节码文件将文件中的类加载到内存模型中再通过执行引擎执行这些程序得到结果。

生命周期

jvm的生命周期分为启动,运行和结束三个流程。在启动阶段由虚拟机指定一个初始类进行加载,在加载初始类的过程当中会涉及到很多类的加载比如说这个类的父类,父接口,这个类引入的类以及这个类执行需要依赖的类等等。执行阶段可以看做是虚拟机进程执行的过程。结束阶段可以是java程序运行结束虚拟机结束工作,调用系统结束函数结束,程序抛出异常结束,系统抛出异常结束等。

特点

特点上我们主要关心虚拟机基于什么样的指令集设计的,程序虚拟机的设计有两种一种是基于栈指令架构设计的,一种是基于寄存器架构设计的。基于栈指令设计的虚拟机的特点在于设计简单易于实现,指令一般比较短实现代码需要用到更多的指令;这种架构适用于内存受限或者跨平台性较强的虚拟机。基于寄存器架构实现的虚拟机的优点在于执行的速度特别快,缺点在于存在寄存器分配的难题和跨平台性比较差。它使用的指令集一班比较长所以完成程序所需要的指令一般比较少。

常见的java虚拟机

常见的java虚拟机我们分为三类来认识。第一类是比较古老的虚拟虽然已经不在使用但是设计思想值得借鉴,这类虚拟机有Classic Vm和Exact Vm 前者是java1.0所使用的的虚拟机特点是只支持解释器不支持即时编译器,后者两者都支持,Exact VM也称之为准确内存管理的虚拟机,它使用句柄的形式来管理对象这样可以快速找到对象的类型以及存储的位置方便垃圾的回收。第二类虚拟机是现在正在使用的虚拟机首当其冲就是Oracle公司的HotSpot虚拟机这个是当下使用最广的虚拟机,接着就是J9它是Google公司研发的特点就是在谷歌自己的软件1上面运行比较稳定但是在其他软件的运行上容易出现bug所以现在一班是google内部使用。第三款就是JRocket虚拟机它一班使用在服务器端,号称运行速度最快的虚拟机,它只支持即时编译器。接着第三类就是平台依赖性比较强的虚拟机分别有Alibaba字眼Taobao Vm和微软的SoftWare VM以及Liquid Vm。等等。

jvm内存模型

在这里插入图片描述

jvm的内存模型主要分为类加载层,运行时数据区和执行引擎三层,类加载层主要负责识别、验证和加载字节码文件中的各个类。运行时数据区主要用于加载运行java程序运行时的各个内存结构,执行引擎主要负责执行操作内存中的各个结构运算出程序的结果。

类加载层

类加载器的作用

类加载器的作用就是,通过一个类的全限定名来找到一个类(这个类可以在文件系统中,可以是被压缩的,可以是在网络中,甚至可以是现在生成的通过代码生成的,通过其他格式转化来的(JSP),通过数据库存储和在加密文件中获取的)接着需要以字节流的方式读取这些类的信息,将这些信息转化问运行时数据区中方法区的一个类并且创建一个java.lang.Class用于获取这些类的信息。

类加载的过程

类加载的过程主要分为三个步骤:

①加载:

类加载过程主要是负责通过全限定名获取类并且通过字节流的形式获取类的字节码文件并且转化为方法区中的一个类并生成java.lang.class来访问类的信息

②链接*************

链接的过程主要分为三个步骤验证,准备和解析。

验证主要是验证主要是保证所加载的类是符合虚拟机要求的在加载之后不会危害虚拟机的安全,主要是验证字节码文件的格式,字节码验证,符号应用验证和字面量验证。

准备主要是给所有的static变量附初始值的过程,需要注意的是final关键字声明的是常量在字节码的编译阶段就已经完成,也不是实例变量实例变量的初始化是在调用构造方法的时候确定的

解析阶段要做的是主要是将字节码中的一个符号引用转化为一个真是引用的过程,符号引用就是规定当前的引用所要引用的位置,真是引用可以是一个指针,句柄或者是一个偏移量。解析主要针对的是类,接口,字段,类方法,接口方法,或者是方法类型。这个过程可能会混合着初始化一起执行。

③初始化

初始化的过程主要是执行类中的静态代码快或者赋值操作的方法,它执行的其实是方法,它由javac编译器自动自动收集而来。

类加载器的分类和使用

分类:

在java虚拟机规范中类加载器主要分为引导类加载器和用户自定义类加载器(扩展自ClassLoader抽象类)两种。但是日常我们的分类主要有引导类加载器,扩展类加载器应用程序类加载器,用户自定义类加载器四类。引导类加载器是使用C++写的一个用于整个系统引导启动的类加载器,它主要用于加载一些比较基础的类比如说String,Object这些我们在程序中不用引入就可以直接使用的类。扩张类加载器主要用于引入java扩展包下的类java.ext.dirs和java.lib.ext。应用程序类加载器加载了大部分我们锁编写的类它加载java.class.path或者环境变量classpath中定义的路径的类。自定义类加载器的使用主要目的是隔离我们自己所定义的类,改变类加载过程,扩展类的加载源或者防止我们的类泄露。

使用:

ClassLoader介绍:

ClassLoader是一个抽象类,其中定义了我们类加载的逻辑所有自定义类加载器都扩展自它。其中定义的方法主要有获取父类,loadClass中定义了双亲委派机制,findClass遍历与我们自动的扩展类,findLoadClassLoader用于寻找本了的类加载器,还有一些用于动态加载类的方法和动态链接类的方法。

如何自定义

我们需要如何自定义类加载器呢,我们指导1java中所有的自定义类加载器都扩展自ClassLoader。所以我们自然的需要去继承这个类,该类中有两个方法用于方便我们自定义类加载器,java1.2之前的做法是重写loadClass方法,但是1.2之后不推荐这样做而是提供了findClass方法方便我们扩展类,如果我们不需要特殊的操作其实完全可以去继承URLClassLoader来完成自定义类加载器。

获取类加载器:

获取类加载器的方式有很多,比如我们可以通过getClassLoader方法来获取类的加载器,可以通过Trread.currentThread.getContextClassLoader用于获取线程上下文类加载器,可以通过ClassLoader.getClassLoader用于获取系统类加载器,用DriverManager.getCallerClassLoader获取调用这个类的类加载器。

双亲委派机制

什么是双亲委派机制

java加载一个类的时候是按需加载的当需要一个类的时候才回去加载它。双亲委派机制就是当一个类加载器获得类的加载请求的时候并不是自己加载而是寻找父类加载器来加载直到顶到引导类加载器。然后逐级往下指导找到一个能够加载这个类的类加载器为止。

优点

优点就是保证了沙箱安全机制,让基础的类优先加载而保证整个程序的正常运行。而且还可以避免类的重复加载。

如何打破

其他琐碎的内容

java中如何唯一识别一个类

java中识别一个类的方式是完全限定名加上类加载器,java在将一个类放入到方法区的时候会将类加载器的引用都当做是类型信息的一部分一起存储。

java中的类的使用分为主动使用和被动使用两种被动使用不会触发类的加载机制

运行时数据区

在这里插入图片描述

运行时数据区概述

内存是十分重要的资源,时磁盘和CPU之间的桥梁和中间仓库,承载着操作系统和应用程序的实时运行。jvm内存布局规定了java程序运行过程当中的内存申请,分配和管理。但是不同的虚拟机实现有所不同我们只需要了解最为通用经典的jvm内存布局,jvm的内存布局统称为运行时数据区。

运行时数据区的内存布局

运行时数据区分为堆,栈,本地方法栈,方法区(元空间)和程序计数器。其中堆和方法区是线程共有的其他的各个部分是线程私有的。

运行时数据区的监控

Runtime类用于表示此时内存中实时环境。

程序计数器

程序计数器是什么

程序计数器是内存中很小的一块内存空间,当执行的是一个java方法用于存储下一条需要执行的字节码指令当执行的的是一个本地方法它存储的就是空值

特点

程序计数器是线程私有的,它随着线程的创建而创建随着线程的销毁而销毁,程序计数器也是vm内存布局当中唯一没有垃圾回收和内存溢出的区域

常见的两个问题

栈是什么:
java虚拟机栈,每个线程在创建时都会创建一个虚拟机栈,虚拟机栈当中存在一个个的栈帧,每一个栈帧多对应这java中一个方法也就是说栈帧是java方法的内存表示。栈其实是和线程相关联的随着线程生而生随线程死而死。它的作用就是保存方法的局部变量、部分结果并且参与方法的调用和返回。

栈的特点:
栈本身的特点就是它是java虚拟机中访问次数很频繁的一个结构。并且只存在入栈和出栈的操作。在栈中会出现OOM(栈其实在不同的虚拟机中实现不一样,分为可以动态扩展的栈和不可动态扩展的栈,其中可以动态扩展对的栈中在栈内存不够的时候会出现OOM也就是内存不够分配,但是大部分的虚拟机实现中是不可扩展的一个机构所以出现的异常是StackOverFlow),并且不存在GC。

栈帧
栈帧是什么,如何使用?

栈中存储的其实是一个个的栈帧,每个方法对应着一个栈帧也就是说一个栈帧存储着一个方法运行过程中所需要的数据的数据集。栈上的操作其实只有入栈和出栈两种这里出入的就是栈帧。每个栈中真正有效的其实就是栈顶的栈帧,我们称之为当前栈帧,这个栈帧对应的方法称之为当前方法,这个方法所在的类称之为当前类。当前方法可以调用其他方法这会导致另一个栈帧的创建和入栈,当被调用的方法执行完毕之后(这里其实有两种退出正常退出和异常退出,正常退出值得是当前方法执行return指令退出,具体使用的指令需要根据方法的返回值决定比如ireturn对应的是boolean,short,int等等,对应的还有freturn,dreturn,lreturn,return等等。另一种异常退出的方式指的是当前方法中出现了异常并且没有在当前方法中处理,这样退出的话退出的地址就会由异常表决定。)就会进行当前栈帧的出栈操作然后通过returnAdress(调用当前方法的栈帧中下一条指令的地址)返回,当前栈帧出栈,恢复上一个栈帧的局部变量表,操作数栈并且将当前返回的结果入栈。

栈帧的内存布局

栈帧的内存布局分为局部变量表,操作数栈,动态链接,方法出口,和一些附加信息组成。

局部变量表

局部变量表其实可以看做是一个数组,这个数组用于存储方法的变量列表和方法中声明的各个变量不过只能够存储八种基本数据类型的变量,ReturnAddress,和引用数据类型的变量。局部变量表其实是使用槽位(Slot)来存储数据的,这个槽位的数量也就是占用内存的大小在编译器就已经确定并且运行时不变的。如果是构造方法或者是实例方法那么第一个槽位存储的就是当前对象,然后优先分配变量列表再分配方法中声明的变量,分配的个数根据类型而定对于32bit的数据占用一个槽位,64bit的数据占用两个槽位如果占用两个槽位的话访问就只需要访问前一个槽位。并且局部变量表还提供了复用的机制,如果占用了本槽位的一个变量已经超过生命周期那么它可以被接下来的变量复用。并且这个局部变量表只有在当前栈帧中有效,当方法执行结束就会被销毁所以不存在线程安全问题。

操作数栈

操作数栈其实就是一个栈结构只能存在入栈和出栈的操作,指令根据入栈和出栈的操作使用操作数栈进行各种操作运算,所以操作数栈的作用就是计算保存中间结果。在最先介绍jvm虚拟机的时候有提到过jvm的设计其实是基于栈的指令架构设计的这里的栈指的就是操作数栈,操作数栈其实就是执行引擎中的一个工作区。操作数栈使用的整个过程是这样的首先在一个栈帧创建的时候会创建一个空的操作数栈,而且操作数栈的大小是确定的,它可以用于存储32bit和64bit的数据但是存储何种类型的数据完完全全是通过指令来决定的,并且只能够采用入栈和出栈操作来操作它。如果方法的调用带有放回值的话它也会将最终的结果压入栈用并且更新pc寄存器的地址来获取它。

栈顶缓存技术

由于栈的访问频率十分高而且它是运行在内存当中的,这涉及到许多的内存读写操作,必然会导致程序运行的速度变慢。所以我们使用到一个新的技术叫做栈顶缓存技术,它将栈顶的元素提前缓存到寄存器中,由cpu直接访问提高程序运行的整体速度。

动态链接

动态链接我们可以简单的理解为是指向运行时常量池中当前方法引用的一个指针。要包含这样一个指针的目的就是实现动态链接。

什么是动态链接呢?

jvm中将方法的符号引用转化为方法引用有两种方式,第一种称之为静态链接也就是在编译期就能够确定执行的是什么方法,并且在运行时不会改变,使用这种方式的方法叫做非虚方法(包括私有方法,final方法,静态方法,构造方法和父类方法)对应的绑定方式就是早期绑定就是说在编译器就将方法和对应的类型绑定;第二种方式称之为动态链接,动态链接的针对的是只有在运行时才能够确定明确执行的方法,执行的方法一般是虚方法,它的绑定机制就是晚期绑定就是在运行期间才确定下来方法和哪个类型绑定。

对应的指令有五条:
调用非虚方法
invokestatic 静态方法
incokespecial 私有,构造器,父类方法
调用虚方法
incokevirtual
调用接口方法
invokeinterface

动态调用
incokedynamic

方法重写的本质:
首先我们进行动态链接的时候,方法会首先查看局部变量表的第一个参数,找到当前对象,查看是否存在方法,并且能够通过权限验证(如果不能通过权限验证报illegalAccessError),接着寻找父类,如果最后都没有找到就会报AbstractMethodError

为了简化这个过程在虚拟机中会维护一张虚方法表,以查表的方式来决定调用。它在类加载的链接阶段创建并开始初始化,在初始化之后初始化完成

方法的返回地址

方法的放回地址其实就是记录调用本方法的方法的pc寄存器地址。这里其实有两种退出正常退出和异常退出,正常退出值得是当前方法执行return指令退出,具体使用的指令需要根据方法的返回值决定比如ireturn对应的是boolean,short,int等等,对应的还有freturn,dreturn,lreturn,return等等。另一种异常退出的方式指的是当前方法中出现了异常并且没有在当前方法中处理,这样退出的话退出的地址就会由异常表决定。

其他信息

还可以附带一些其他的比如说debug需要用到的信息

本地方法栈

本地方法栈和虚拟机栈十分类似,两者的区别在于虚拟机栈主管的是java方法的运行,本地方法栈主管的是本地方法的运行。它和OOM和栈基本一致,当一个线程调用本地方法的时候将脱离java的管控,它共有和java虚拟机一样的级别,可以访问虚拟机中的内容甚至可以直接访问寄存器中的值。在不同的虚拟机中本地方法栈的实现也是不一样的甚至可以不存在本地方法栈,HotSpot中就将本地方法栈和虚拟机栈合二为一。

堆是jvm中最重要也是分配内存最多得内存区域,堆内存的大小在可以动态设置大小(-Xmx,-Xms)在JVM启动的时候它就会被创建并且分配大小。堆的内存不要求是物理上连续的只需要保证逻辑上连续即可。所有的线程共享一块堆内存,为了保证各个线程不同时访问同一块内存导致多线程加锁,对内存提供了TLAB。

堆内存的布局

堆上的内存可以细分为新生代,老年代,和元空间其中新生代和老年代的比例默认为1:2(可以使用NewRatio指定)。新生代存储的对象的特点是朝生夕死,它可以细分为伊甸园区,幸存者一区,幸存者零区三者的比例默认是8:1:1(可以使用SurviveRatio来指定)这样分区的目的就是使用标记复制算法。老年代主要用于存储需要长期使用的对象。元空间就是方法区的实现。

堆内存的上的对象分配

新对象的创建其实是一个比较复杂的过程,设计者在这几的适合不仅仅要注意这些对象应该在哪如何分配还需要考虑后期GC。

年轻代的分配:

对象的一般分配过程是这样的,新生的对象会首先被分配到eden区,等到eden区满了之后会触发manor GC(young GC)将对象放到幸存者0区,接着分配的时候还是从eden区分配然后等到eden区满的时候再次触发young GC将存活下来的对象放入到幸存者1区中接着交换两个幸存者区,循环以上的过程。

老年代的分配:

什么时候对象会进入到老年代呢,在年轻代存在一个参数叫做分带年龄(MaxTenuringThreshold)默认是15,意思是一个对象能够在年轻代中存活15次不被回收就会将其存入老年代,一些大对象,比如说eden区或者survive区存储不下对象的时候也会被存储到老年代。当老年区满了之后会触发所谓的Major GC(old GC)回收空间,如果空间再次爆满就会触发OOM。

对内存上的内存回收

垃圾回收的过程就是GC的过程,在堆中的GC分为两种一种是局部回收就是只回收堆空间的一部分,另外一种叫做Full GC就是进行全堆的回收。其中局部回收可以分为Minor GC(年轻代回收),Major GC(老年代回收,通常Major GC会和FullGC混用,目前只有CMS支持老年代的单独回收),Mixed GC(年轻代和老年代的混合回收目前只有G1垃圾回收器支持)。

Minor GC的介绍:

Minor GC回收的是年轻代,年轻代的对象具有朝生夕死的特点所以Manor GC触发的很频繁并且回收的时间也是很短的会造成短时间的STW(Stop The World对象回收的过程当中必须暂停对象的分配和使用,不然很难识别对象是否可以回收)。在进行ManorGC之前会进行空间分配担保,就是查看老年代是否有容纳整个年轻代的连续内存空间。如果存在则直接分配,如果不存在则查看是否有连续空间大于年轻代垃圾回收之后的平均大小,有就直接分配没有就报出OOM在java1.6之后默认开启空间分配担保。

Major GC的介绍:
MajorGC指的是对老年代垃圾回收,老年代对象的特点是相对稳定,很少触发GC但是老年代的空间比较大所以一次GC的时间也会很久并且伴随着STW。再一次major GC之后会伴随这一次的Minor GC如果对象空间仍然不足就会报出OOM

Full GC的介绍:

Full GC指的是对整个堆空间的垃圾回收,它在老年代空间不足,元空间空间不足,或者使用System.gc()的时候会触发

堆空间和线程

整个堆空间是多多线程共享的,所以在多线程分配的时候难免出现多个线程访问同一个内存,导致加锁降低分配的性能和吞吐量。所以在eden中划分出提供一个线程使用的TLAB大占用eden的十分之一。

所有的对象都是分配在堆上的吗

随着逃逸分析技术的发展在栈上分配,对象分离(标量替换),同步消除称为可能所以对象也有可能不分配在堆中。

逃逸分析:

这是一种减少java中内存同步负载和堆分配压力的跨函数全局数据流分析算法。它分析一个对象的引用的作用域。

栈上分配:
当一个对象在一个方法中不逃逸的时候可以实现对象分配在栈上,这样可以在对象使用之后随着栈空间的回收被回收

同步消除:
当一个对象在一个线程中不逃逸,代表着不存在多线程同步的问题,可以直接自动去掉同步的代码。

对象分离(标量替换):
标量类似于一个八大基本数据类型,聚合量就是一个对象,当发现一个对象不需要当做一块连续的空间访问的的时候可以将对象分离加载到寄存器中提高处理的速度。

逃逸分析技术还不是很成熟,并且也具有自己的缺点,就是分析了半天发现这个对象是逃逸的那就不能使用如上的优化手段还拜拜浪费了逃逸分析的时间。

方法区
方法区概述

方法区是堆内存中一块用于存储类型信息,静态变量,常量和即时编译器缓存的空间。它是所有线程共有的一块内存空间。

方法区的布局,初始化,分配,回收

布局:

方法区用于存储类型信息(包括类信息如本类的全限定名父类信息接口信息,字段信息如字段名字段的权限或者修饰符,方法信息方法名返回值方法的权限和修饰符等),还有一块特殊的空间叫做运行时常量池,运行时常量池中存储的其实是字节码中常量池的数据。常量池中存储的包括字面量,类引用,字段引用,方法引用等信息。在类加载的时候通过动态链接的方式将这些引用转化为方法区中的运行时常量池中的一个个具体引用信息。

初始化:
方法区的初始化其实在虚拟机开始运行的时候就完成了,它的内存空间要求逻辑上连续但是不要求物理上也是连续的。它的大小可以通过MetaSpaceSize参数和MaxMetaSpaceSize参数来指定。在升级成元空间之后其实它的大小和本地内存是一致的,会设置一个高水平先通过每次回收的内存的多少来调整这个高水平先。

回收:

在虚拟机规范中并没有要求对方法区这个空间的回收和压缩。在HotSpot虚拟机的实现中将它称之为非堆就是将其与堆区分开来成为一块独立的区域。它的垃圾回收分为两块首先第一块指的是运行时常量池的回收,运行时常量池中存储的是字面量和符号引用,符号引用的回收只要常量池中的常量没有被引用就可以被回收。另一块就是类型信息的回收,它的回收条件有一些苛刻,只有当该类型的Class对象没有被使用,不存在该类及其子类的实例以及加载该类的类加载器被回收该类型才能被回收。

方法区再java1.7和1.8的改变

HotSpot虚拟机在1.7到1.8做出了重大的改变,将虚拟机的实现从永久代改为了元空间。主要有内存使用和内存布局两点区别。

内存使用:永久代使用的是虚拟机本地内存有限,元空间使用的是计算机本地内存,不容易发生OOM。

内存布局:1.8之后字符串常量池和静态变量被移动到堆中存储,主要的原因是这两块的内存区域的垃圾回收比较频繁。

堆,栈,方法区三者的关系

这三者的关系主要体现在对象上,方法区负责存储类的信息也就是对象的模板,栈负责存储对象的引用,堆真正存储数据。

OOM产生和及解决

首先使用内存分析工具分析出导致OOM的是内存溢出还是内存泄漏。如果存在内存泄漏的话需要准确定位到引起内存泄漏的代码处,如果出现的是内存溢出的话适当加大出现OOM的位置的内存。

执行引擎

本地方法接口和本地方法库

为什么要有本地方法接口

本地方法接口的目的主要是实现与java外环境的交互和与操作系统的交互,java操作一些底层硬件或者在操作系统内操作是其实不是很方便所以需要使用本地方法接口来实现。

是什么

本地方法可以简单理解为一个与外界别的环境沟通的接口,它的实现体是使用别的语言写的。

垃圾回收

垃圾回收的概述

什么是垃圾,为什么回收何时如何回收?

在java中那些没有应用指向的对象称之为垃圾,垃圾回收是必要的如果我们只分配内存给对象而不回收垃圾对象那么就像只制造垃圾不回收垃圾一样迟早会发生OOM程序奔溃。在早期的垃圾回收比如C++实现的垃圾回收是手动回收需要程序员分配和回收内存,优点在于可以准确定位和回收垃圾缺点在于可能会忘记垃圾回收,在程序开发的同时需要注意垃圾回收。晚期的垃圾回收例如Java,Python,Ruby的垃圾回收是自动垃圾回收,优点在于程序员可以专注于程序的开发不必关心内存怎么样了,缺点在于当发生OOM的时候我们不知道如何下手所以我们需要充分了解垃圾收集器的原理和使用并且调节和使用,但是这些自动回收的垃圾收集器会引起STW,这就需要我们升级GC达到较好的效果。

垃圾回收的算法

垃圾回收其实分为三个阶段分别为标记阶段,finaliza阶段和清除阶段三个阶段分别使用到不同的算法

标记阶段算法
引用计数法

引用计数法实现原理就是给每个对象记录引用个数当有对象的引用个数为0是就代表它是垃圾

优缺点

优点在于简单高效,缺点在于需要耗费时间空间最致命的问题在于循环引用

什么语言使用

Python使用的是引用计数法,解决循环引用的方法在于手动解除和使用虚引用

枚举可达性分析算法

枚举可达性分析算法就是维护一个GCRoots集合,我们在标记垃圾的时候枚举每一个GcRoot查看它关联的引用标记不是垃圾,但是也需要看所使用的的GCRoot是什么引用再决定。栈中局部变量表,本地方法栈中,方法区中的静态变量和常量,当堆中进行局部回收是其他区域的对象。以及锁对象,常驻在内存中的基本数据类型,常驻异常类型的对象都可以作为GCRoot

finaliza阶段

finaliza方法的作用就是在垃圾回收之前调用用于关闭资源等操作的。它的过程是这样的,首先查看该对象是否重写了finaliza方法是否调用过,如果没有重写没有或者调用过直接可以回收。如果没有调用过并且重写过则将其加入一个队列中,由一个优先级很低的finalizer线程来执行方法。当在执行的过程当中又与GcRoot引用链上的对象发生关系就复活。

清除阶段
标记清除算法

过程

如算法名字一样,首先通过GCRoot确定可以存活的对象再经过finaliza阶段,标记出存活对象,然后清理垃圾,这里的垃圾清理并不是正式删除而是在空闲列表上标明这片空间是可用的。

优缺点

这里只说缺点因为它是最早出现的算法,后续的算法都在它的基础上演进的,缺点在于执行效率慢需要一个个的枚举判断回收,会出现STW,并且出现大量的内存碎片正因为这样所以在对象分配的时候需要使用1空闲列表的方式分配对象。

使用场景:

老年代,不经常回收。

标记复制算法

算法过程

标记复制算法是这样的,将内存空间划分为两片的空间,一次只使用一片空间,当使用后标记使用的空间中的可用对象复制到另一片上去接着使用另外一片空间。

优缺点

优点:没有内存碎片,速度快

缺点:两片空间,浪费一片。对于G1这样的复制算法来说需要维护引用十分困难。

使用场景:它适合使用在可用对象少的场景,所以使用在朝生夕死的新生代。

标记清除压缩算法

算法过程

标记清除压缩算法,根据算法的名字首先是标记阶段使用枚举可达性分析算法标记,清除阶段将可以用的对象整理到一端。压缩阶段再接着清理不可用的内存。这样内存空间就是规整的可以使用指针碰撞的方式进行分配内存空间。

优缺点

缺点:比标记清除算法还要慢,STW。

优点:解决了内存碎片的问题

增量收集算法、分区算法

算法过程

优缺点

优点:每次STW时间少

缺点:线程切换的开销

GC中的一些概念
四大引用

常说的四大引用是强软弱虚引用,其实还有一种终结器引用。java中大部分的对象都是强引用它的特点就是死不回收,所以大多引起内存泄漏的就是它。弱引用只有在内存空间不足的时候才回收,软引用则是在下一次GC时比回收,这两者的特点就是在资源够的时候不回收,使用的场景一般是缓存例如Mybatis的底层大量使用到软弱引用。虚引用又叫做幽灵引用或者幻影引用,即时使用引用来访问这个对象也显示为空。后三者可以配合引用队列来使用在回收的时候通知,可以监控对象回收的过程。终结期引用则是配合finaliza方法来使用只有在第二次回收的时候才会被回收

安全点、安全区域

安全点就是在这个时候才可以暂停下来进行垃圾回收。所有线程进入安全点的方式有两种,一种是叫做抢先式中断,就是暂停所有的线程如何放行没有到达安全点的线程跑到安全点。第二种是主动中断,就是等到所有的线程都跑到安全点再中断。

安全区域的概念就是有的线程在阻塞或者睡眠,那么我们就默认它进入安全区域可以中断,它在进入安全区域之前会设置一个标记。当它恢复运行态的时候其实会查看GC是否结束,如果结束直接清醒,如果GC没结束就等待。

垃圾回收器
垃圾回收器的分类

根据是否并行分类:分为串行垃圾回收器和并行垃圾回收器,其中并行垃圾回收器根据工作模式可以分为独占式垃圾回收和并发垃圾回收两种。

根据回收的带:分为新生代回收器和老年代回收器

根据回收后的内存是否存在碎片:如CMS和G1使用的都会产生内存碎片。

垃圾回收器的性能指标

吞吐量,GC占用内存:分别为用户程序执行时间占总时间的比例和GC时间占总时间的比例

停顿时间,回收频率:停顿时间主要是STW的时间,回收频率则是多少时间回收一次。

内存占用:内存和资源的占用情况

几种常见的垃圾回收器

在这里插入图片描述

Serial

serial就是串行的垃圾回收器,就是同一时间只有一条线程在负责垃圾的回收优点是专门做垃圾回收效率比较高。分为Serial,Serial Old,Serial是java1.3之前唯一的垃圾回收器是使用的算法是标记复制算法适合使用在单核资源少的机器上比如说Client的新生代,serial Old则使用标记压缩算法使用在使用在Server端老年代也适合使用在单核资源少的机器上。

Paralell

并发垃圾回收器的特点就是多个线程,优点就是多个线程一起回收速度快,但是线程切换需要时间。并发分为ParNew,ParScanvage,ParOld。其中parNew和ParScanvage使用在新生代使用的是标记复制算法两者的区别在于Scanvage更加注重吞吐量多使用在交互少的后台计算上,而ParOld使用在老年代使用标记压缩算法。

CMS

CMS垃圾回收器是一款专注于停顿时间的垃圾回收器。它的过程分为四个阶段首先初始标记阶段,首先标记出所有的GCRoot,接着是并发标阶段标记出所有的可用对象链,接着是重新标记阶段,主要是对前两个阶段的修正,接着是并发清除阶段,用户线程和垃圾清除线程一起垃圾回收。它使用的算法是标记清除算法所以会引起内存碎片,这样导致它的内存对象空间分配使用空闲列表的方式。所以总体来说这个过程当中初始标记阶段和重新标记阶段还是需要STW的,但是耗费时间最长的并发标记和并发清除阶段是和用户线程一起运行的总体上STW时间很短。在并发清除阶段需要特别说明因为在并发清除阶段需要和用户线程一起使用所以需要预留一定的空间给用户线程使用者将导致不到内存分配满的时候就会触发GC,要是CMS运行期间无法满足用户线程的内存空间将采用OldGC。

优点在于:停顿时间短并发清除

缺点在于:会产生内存碎片,难以收集浮动垃圾,内存受限

参数:设置内存回收的阈值,设置FullGC之后是否整顿空间,多少次CMS之后整顿时间,设置线程的数量

G1

为什么要有G1,为什么叫G1:

随着业务越来越复杂,使用的人数越来越多。所以迫使GC的停顿时间越来越短,所以诞生出G1这样的垮年轻代和老年代并且可以预测停顿时间的垃圾回收器。之所以教唆G!垃圾回收器是G1将内存分为许许多多的Region,每个Region都扮演这不同的角色,我们在后面会维护一个优先级队列这样首先回收效益高的空间。

G1的和CMS的区别(并行并发,分带收集,空间整合,可预测的时间模型)。

并发并行:

首先说G1是并发和并行都有的,它并发用户线程和垃圾回收线程。并且使用多线程并发执行垃圾标记和回收。CMS垃圾回收器也是如此,并发体现在线程可以并发在垃圾回收线程和用户线程之间,并行体现在多线程标记和回收。

分带收集:
G1也是分带垃圾回收器,不同于创痛的分带它将内存分为多个Region每个Region扮演这不一样的角色。它是一个跨带收集的垃圾回收器,但是CMS只能够在老年代使用。

空间整合:
CMS的空间整合体现在执行了多次之后整合一次空间,或者当在并发清除阶段内存不能偶满足用户线程使用的时候使用Serial Old整合。G1的整合体现在各个Region之间使用标记复制算法,但是整个内存来说回收之后整合Region使用的是标记压缩算法。

可预测的时间模型:

对于CMS来说只是降低了STW的时间,对于G1来说它维护了一个优先级队列,可以在指定的时间之内回收最优价值的Region。

G1的过程

在这里插入图片描述

G1垃圾收集器的总体流程是首先执行年轻代的回收接着是老年代并发标记,再接着是年轻代和老年代并发标记和MixedGC。一旦上诉的过程完成垃圾回收就会促发一次Full GC使用一个单线程进行整堆的回收。

在了解年轻代回收,年轻代和老年代并发标记,混合回收之前首先需要了解RememberSet和Dirty Card Table的使用。RememberSet其实是为了解决堆中不同区域的相互引用的问题,我们知道当对堆中某个区域垃圾回收的时候堆中的其他区域也会作为GCRoot存在,那么如果不存在RememberSet的话就需要在回收堆中的某个区域的时候去扫描其他所有的堆区域这样显然不切实际。那么RememberSet中其实记录的就是其他哪个其他Region的引用中存在本Region的对象的引用。它的实现很简单每当需要应用对象时促发一个Write Barier过程查看所引用的对象是否在本Region中如果,如果不存在则通过Dirty Card Table将信息记录到该Region中。Dirty Card Table其实是解决Remember Set中的效率问题先记录到Dirty Card Table中再有指定的线程记录到每一个Region。

年轻代回收过程:
标记过程:

首先标记出所有的GCRoot找出所有的GC引用对象,接着使用Dirty Card Table更行RSet,然后修正引用。。

回收过程:

接着就进行可用对象的复制然后处理轻软若虚以及终结器引用

老年代和年轻代并发标记以及混合回收的过程:

并发标记:

首先是初始标记阶段,这个阶段标记出所有的GCRoot必须是STW的,接着是跟区域扫描扫描出所有的Surviver引用的老年代对象,必须在Young GC之前做不然Young GC之后就会改变对象引用。接着是并发标记阶段,应用线程和GC线程一起标记对象。再接着是再标记阶段主要是为了修正前两个阶段的标记这个阶段也是STW的。

混合回收:

混合回收分为两个阶段,第一个阶段叫做独占清理,虽然叫独占清理但是他只是负责统计一个标记的可用对象是哪些,统计Region回收效益拍成一个优先级队列。为下一个阶段做准备。下一个阶段叫做并发清除阶段,他将老年代分为8个区域根据优先级和限定的时间惊醒部分回收。

FullGC:

一旦上诉的过程完成垃圾回收就会促发一次Full GC使用一个单线程进行整堆的回收。当没有足够的空间晋升或者对空间满的时候或者停顿时间实在太短就会促发fullGc

G1可用设置的指标:

可用试着的G1的指标有开启G1,设置Region的大小,停顿时间的多少,总线程的多少,用于GC的线程的多少,以及促发并发标记过程的阈值是多少。

其他的垃圾回收器

ZGC和Shenandoah

字节码文件的结构

对象创建的过程

对象的创建方式:

对象的创建方式可以是根据工厂创建,new,clone和反序列化。

对象的创建过程:

一个对象的创建首先要检查该对象所属的类是否加载,链接,初始化。接着考虑内存得到分配根据内存布局是否规整可以分为指针碰撞和空闲列表两种方式,其中需要注意多线程问题可以使用TLAB或者使用CAS机制解决。接着考虑对象的初始化,初始化为0值,然后考虑对象头信息的创建和构造方法的调用。

对象的内存结构:

对象的内存结构分为三个部分,分别为对象头信息,对象数据以及对象填充。对象头信息中分为对象的类型信息和运行时的元数据,运行时元数据包括Hash值,GC分带年龄,是否有锁,锁的地址,偏向锁的偏向的线程和偏向的时间戳。对象的数据存储就是各个字段的信息。对象填充主要是用于对象空间规整。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值