JVM知识点总结

1.我们自己编写的java代码,是如何在各个平台运行的?

 

        java代码通过javac编译成class文件,这种中间码称为字节码,然后通过jvm加载字节码。运行时,解释器将字节码解释为一行行机器码来执行。在程序运行期间,即时编译器会针对热点代码,将该部分代码编译成机器码以获得更高的执行效率。在整个运行过程中,解释器和即时编译器共同作用,使java程序几乎能够达到和编译型语言一样的执行速度。

2.什么是类加载器

        类加载器就是将javac编译成的class文件,通过加载,生成某种形式的class数据结构放到内存中,程序可以调用这个数据结构来构造出object,这个过程是在运行时进行的,这也是java动态拓展性的根基。

3.java类的生命周期

        1.这张图表现了一个类的生命周期,完整一点的话,我们可以在最开始加上javac编译阶段。而“类加载”只包括加载,连接,初始化这三个过程

2.需要区分“类加载”与“加载”,加载只是类加载的第一个环节--加载 

3.解析部分是灵活的,它可以在初始化环节之后再进行,实现所谓的“后期绑定”,其他环节的顺序不可改变。

加载:

所谓加载,就是读取class文件,将其转换为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。

PS:这里的class文件不一定指的是本地class文件,泛指各种来源的二进制流,比如说来源于网络,来源于数据库,甚至是即时生成的class。比如很著名的动态代理技术,就使用到了即时计算出来的class,然后实例化代理对象。

验证:

第二个阶段开始是验证阶段,验证动作是有很多个步骤的

第一个步骤:文件格式验证,该步骤其实是发生在加载阶段的,如果通过,那么才能顺利加载,顺利加载后,此时方法区内虽然已经存在了该class的静态结构,堆中也存在了该class类型的对象,但是这并不代表着JVM已经完全认可了这个类,如果程序想要使用这个类那么就必须进行连接,而连接的第一步就是进一步对这个类进行验证,我们来看看到底对方法区内的class静态结构进行了哪些方面的验证。

元数据、字节码的验证,简单概括来说就是对class静态结构进行语法和语义上的分析,保证其不会产生危害虚拟机的行为。 

如果上述验证通过,虚拟机姑且认为是安全的,接下来进行符号引用验证,该验证过程发生在解析阶段内,而解析阶段它可以在初始化阶段之前或者之后进行,所以验证包含了很多个步骤,分散在各个不同的阶段内。

验证过程结束后,接下来就是准备过程,该阶段做的事情就是为该类中的静态变量赋0值(注意这里仅仅是静态变量,而不是成员变量),

     

 此时将会出现一个被太多人混淆和误解的概念, 虚拟机内存规范中定义了方法区这种抽象概念,HotSpot这种主流虚拟机在JDK8之前,使用了永久代这种具体的实现方式来实现方法区。

在JDK8以后,弃用了“永久代”这种实现方式,采用元空间这种直接内存来取代。

所以我们常常看到有人说,JDK8及以后采用“元空间”来替代“方法区”,这种说法是完全错误的,因为“方法区”是抽象概念,“元空间”是实现方式,这种说法是完全牛头不对马嘴。

在JDK8之前,类的元信息、常量池、静态变量等都存储在永久代这种具体实现中。

而在JDK8以后,常量池、静态变量被移除“方法区” ,转移到了堆中,类的元信息依然保留在方法区内,但是具体的存储方式改成了元空间。

准备阶段之后就来到了解析阶段,该阶段主要做的事情就是将符号引用替换为直接引用。

什么是符号引用?什么是直接引用呢?

当一个java类被编译成Class之后呢,假如这个类称为A,并且A中引用了B,那么在编译阶段,A是不知道B有没有被编译,而且此时,B没有被加载,所以A肯定不知道B的实际地址,那么A怎么才能找到B呢?此时在A的class文件中,将使用一个字符串S来代表B的地址,S就被称为符号引用,在运行时,如果A发生了类加载,到了解析阶段会发现B还未被加载,那么将会触发B的类加载,将B加载到虚拟机中,此时A中B的符号引用将会被替换,B的实际地址就被称为直接引用, 但是事情没有那么简单,了解多态的同学应该知道java通过后期绑定的方式实现多态,接着上面所说,如果A调用的B是一个具体的实现类,那么就称为静态解析。如果B是一个抽象类或者接口,B有两个具体的实现类C和D,此时B的具体实现不存在,当然也就不知道使用哪个具体类的直接引用来进行替换,既然不知道,那么就等一等吧,直到运行过程中产生了真正的调用,此时虚拟机调用栈中将会得到具体的类型信息,这时候再进行解析,就能用明确的直接引用来替换符号引用。

这就解释为什么解析阶段有时候会发生在初始化阶段之后这就是动态解析,用它来实现了后期绑定,底层对应了invokedynamic这条字节码指令。

 

双亲委派模型  类加载的时候 先不加载   一层一层去父亲加载器找,在父亲加载器中找到的话 先不管 直至找到最上层,最上层能加载的话  就先加载 ,加载不了,就找儿子加载器加载

这样做的好处就是  自定义的包 和等级高的包同名的时候,可以优先加载等级高系统提供的包

(越核心的类库被越上层的类加载器加载,而某限定名的类一旦被加载过了,被动情况下,就不会再加载相同限定名的类。这样,就能够有效避免混乱)

JAVA内存模型

程序计数器:

学过计算机组成的同学应该知道,在硬件层面,程序计数器是一种寄存器,它用来存储指令地址提供给处理器执行。

在JVM这种软件层面,程序计数器也是一样的作用,它用来存储字节码指令地址,提供给执行引擎去取指执行

可以这么认为,这两种程序计数器分别存在于硬件与软件中,实现方式不一样,但设计思想是类似的,明白了JVM中程序计数器是做什么的,那么在程序运行时,我们能不能监控到程序计数器的值,答案是不能,因为虚拟机没有向外暴露查询程序计数器的接口,但是我们可以从侧面的角度去进行观察。

例如下面这个demo

  

可以看到第一列的数字,代表了字节码指令之间的偏移量,叫做bytecode index,这其实就是程序计数器所需要读取的数据 ,主要看bytecode index为11的行,指令为goto ,操作数为2, 代表了回到index为2的那行指令,这里就体现出了原代码的循环逻辑,也体现出了程序计数器的工作方式。

虚拟机栈

虚拟机栈这个名字乍一听可能让人觉得有点难以理解,我在这里换一个称呼,叫做Java方法栈

对应着后面要介绍的本地方法栈,那什么是Java方法栈呢,大家应该知道,程序执行的过程对应方法的调用,而方法的调用,实际上对应着栈帧的入栈出栈,比如我们写这样一段代码。

 运行时,程序会先调用A方法,那么A方法封装成“栈帧”入栈,由于A方法中调用了B,B方法封装成栈帧入栈,然后先执行B中的逻辑,等于B栈帧出栈,然后执行A方法 ,等于A栈帧出栈,可能有些同学以前写递归的时候稍不留神,将会出现栈溢出的异常情况,原因就是没有编写适当的递归退出条件,导致无限量的栈帧入栈,超出了方法栈的最大深度,所以就抛出了StackOverFlow的异常。

我觉得这里有三点需要注意。

第一点就是栈帧,栈帧这个概念,我们下面会详细的讲到,目前的可以简单地将其当做方法调用的一种封装。

第二点 栈帧的生成时机,在编译期间,无法确定Java方法栈的深度,因为栈帧的生成,是根据程序运行时的实际情况来决定的,这是动态的,比如你写了藏有StackOverFlow的递归代码,编译器是无法检查出这种异常。

第三点 栈帧的组成,在编译期间,由于每一个方法的源码都是确定的,而栈帧是根据方法调用来产生的,那么可以猜想,栈帧内部的一些元素是可以确定的,比如说有多少个局部变量,存储局部变量所需要的空间,而有一些元素是无法确定的,比如说该方法与其他方法之间的动态连接,这些后面都会讲到,现在我们的主要关注点就是栈帧。

 

 栈帧中主要存在四种结构,局部变量表,操作数栈,动态连接,返回地址。这几种结构和我们上文猜测的也差不多。下面我们就来详细介绍

局部变量表:

 栈帧是通过方法源码来生成的,当调用该方法时呢,传入方法的参数类型,局部变量的类型,这些在源码中都是已经确定的,既然数量与类型能够确定,那么需要占用的存储空间也能够确定,但是怎么进行存储呢,这里在局部变量表中,通过四字节的slot来存储。 

在LocalVariableTable这个局部变量表中,其中参数args占用了index为0的slot并且声明了签名为String剩下的三个局部变量abc,签名都是int 

操作数栈

在操作系统层面的操作数是计算机指令的一部分,而这里的操作数栈是JVM层面的,但作用是相似的,顾名思义,这里操作数栈就是一个用来存储操作数的栈,这里的操作数大部分就是方法内的变量,那为什么需要使用操作数栈来对操作数进行入栈出栈操作,主要有两个作用

第一点就是存储操作数,这里的操作数指的是变量以及中间结果

第二点就是,操作数栈能够方便指令顺序读取操作数,虚拟机的执行引擎在执行字节码指令的时候呢,会通过当前指令类型从操作数栈中取出栈顶的操作数进行计算,然后再将计算结果入栈,继续执行后续的指令

bytecode index为4.5的两行,iload的含义就是将int类型的操作数压栈,所以4和5两行其实就是将m和n两个变量压栈,接着就是iadd这个指令,它就是取出栈顶的两个操作数进行求和计算并将计算结果压入栈中,接着局势istore这个指令,它就是将栈顶的操作数存入局部变量表,看到这里的你是不是对操作数栈以及字节码指令有了更加清醒的认识,但是关于操作数栈,其实还隐藏着另外一个小问题,上面演示的例子只有一个栈帧,如果虚拟机中存在多个栈帧,先执行完的方法的返回值需要被当做后执行方法的变量

首先执行栈帧B,m和n作为操作数入栈,通过求和字节码指令计算结果,并将计算结果存入局部变量表,那这个中间结果又将会成为栈帧A的操作数,所以需要再从栈帧B的局部变量表中将该值复制进入栈帧A的操作数栈,这样做当然可以,但是JVM做了一些优化。

如上图,在JVM的实现中,将两个栈帧的一部分重叠,让下面栈帧的操作数栈和上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时可以共享一部分数据,而无需进行额外的参数复制传递,这算是一个优化的细节。

动态连接:

JAVA中的多态就是虚拟机栈中的动态连接实现的,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Java类加载过程中,有一个步骤叫做“连接”,在这个步骤中,JVM会将class对象(存储在方法区的运行时常量池中) 中部分符号引用替换为直接引用。为什么是部分呢?因为对于有些方法JVM能够判断这些方法所在的具体类型,所以就可以放心大胆的对方法进行连接,这叫做静态解析,而对于有些方法因为多态的存在,无法在类加载阶段就确定被调用的具体类型,只能在运行时真正产生调用的时候,根据实际的类型信息来进行连接,这就叫做动态连接,看下面的例子。

由于B是抽象类,所以在类A的加载阶段无法确定B的具体实现类,在运行时呢,当方法A中,调用方法B时,需要先查询栈帧B在运行时常量池中的符号引用,然后根据当前具体的类型信息进行动态连接。

返回地址:就是一个类的返回值

本地方法栈:

就是native标记的c语言写的方法。

方法区(JDK8以后实现方式是元空间):

方法区是虚拟机规范中的抽象概念。什么是虚拟机规范,换句话说,无论你用的虚拟机是HotSpot还是JRockit等等,他们的具体实现中,必须要存在方法区这个结构,但具体的实现可以灵活发挥,打个比方,规范好比是造一座房子的图纸,其中规定了必须要有书房这个房间,建造商拿到图纸以后,需要在限定的条件下去设计这个书房,如何进行设计建造,这就是具体的实现方式。

我们就将目光聚焦到最主流的HotSpot虚拟机上,在JDK8以前,HotSpot的开发者将面向堆的分代设计复用在方法区上,他们使用”永久代“来作为HotSpot上的方法区的实现。但是后来发现这种设计并不好。所以从JDK8开始借鉴了一些JRockit的设计思路,使用了元空间来代替”永久代“作为新的实现方式。总结来说,”方法区“是抽象,”永久代“和"元空间"是实现。

那么为什么要用元空间这种本地内存的方式来代替”永久代“呢,下面简单说一下”永久代“的两个主要缺点:

第一:就是可能引起内存溢出,”永久代“的大小设置为多少,可以通过启动参数来指定,但其中存储的数据大小是动态变化的,若阈值设置的太小咋可能导致频繁频繁的类卸载或者说内存溢出问题,设置的太大有可能会存在空间浪费,所以将会由此出现一些调优的问题。

第二点就是”永久代“本身设计复杂,”永久代“本身是面向堆来设计的,所以存储在”永久代“内的对象不是内存连续的,需要通过额外的存储信息,以及实现额外的对象查找机制来定位对象所以比较麻烦。虚拟机设计团队之所以一开始会使用永久代这种方式来实现方法区,是为了进行一定程度的代码复用,但是后来发现存在一些问题,以上两个缺点,对于方法区来说并不是不可回避的。所以目前使用基于内存的元空间来代替”永久代“,就不会有这些问题。

我们之前提到过,类加载的第一个阶段叫做加载,在这个阶段内,虚拟机将会读取被编译的Class文件生成Class对象,Class对象存储了一些类型信息,这些信息就是存储在方法区内的,这里所说的类型信息大家猜一猜的话应该都知道是诸如”类的签名“,”属性“,”方法“ 既然类型信息是从class文件读取的,那么我们就写个demo编译成字节码以后,来看看其中具体有哪些类型信息。

字节码中展示了当前类的全限定名以及父类或者接口的全限定名 

下面两行minor version 和major version 代表当前JDK的主版本和次版本号。像这里,52其实表示的就是1.8版本,ACC PUBLIC代表当前类的访问权限类型是public,ACC_SUPER其实可以不用太理会,它是一个为了实现多态的补丁。

我们在源码中定义了两个属性,基本类型int的属性num和复杂类型是String的属性str,这部分呢,我们可以看到,在字节码中,几乎没有什么理解歧义,已经表达的很清楚了

 源码中还存在三个方法,分别是无参构造方法,含参构造方法,以及一个自定义函数。这块的字节码其实更多体现的是虚拟机栈的相关内容,因为方法调用直接和虚拟机栈有关,可以回顾。

 

这里有一个细节,我们看到bytecode index为1的这行字节码,调用了invokespecial指令,可以看的出来,这里的意图是首先调用父类的构造方法,这就验证了大家熟知的”子类在构造对象时“默认先调用父类无参构造函数这一概念,其实我们平时一些靠背来记忆的概念,都是可以在字节码中得到体现的,另外我们又发现了一个名为LineNumberTable的这个变量,它的数据格式非常统一,呈现出了一种line xx:xx的形式,那这是用来干什么的呢,这其实是表示源码行数和bytecodeindex之间的映射,我以前一直有一个疑惑,当我们在debug的时候,为什么程序能够精准的停留在源码的断点处,这虽然看上去是理所当然的,但是稍微仔细一想便会发现有点不对劲儿,因为程序运行的是字节码,而断点的所在行是源码,他们之前是怎么对应起来的呢,相信有的同学可能也会存在这样的疑惑,实际上它就是通过这里的LineNumberTable来解决的,前一个数字代表源码的所在行,后一个数字代表bytecodeindex在字节码中,和类型相关的信息并不难理解,此外呢,我们注意到字节码中存在名为”Constant Pool“的内容占据了大量的篇幅,下面我们就来看看

首先呢,我们先来思考一个问题,从上层来看,大部分类都不是孤岛,他们之间存在着相互调用的关系的关系。比方说我这里的Test类就继承了Lock类,拥有了Lock类的能力,此外还存在String类的这个属性,就可以调用string的相关方法,那么这些调用是如何实现的呢?先说一种最简单最无脑的实现方式。如果类A的源码中调用了类B源码中的逻辑,那么在编译期间把类B的源码直接引入到类A一起编译,这样的话也能够达到最终目的,但显然是不合理的,因为这会造成代码大量膨胀,想想都会觉得很恐怖。比较合理的方式,就是通过类似指针的方式在类A的字节码中留下一个指针,指向想要调用的类B的字节码,这里指针就起到了”链接“的作用。这些内容其实在类加载那期视频中也提到过,在虚拟机加载字节码的时候,首先加载的就是一些静态的”符号引用“,然后在类加载的”链接“阶段或者说程序运行时将符号引用转换为直接引用。上面说到的符号引用既然是从字节码中加载进来的,那么在字节码中怎么体现的呢? Constant Pool(常量池)内的数据就体现了符号引用与一些其他的静态引用,需要注意的是,这里的”常量“和我们平时写代码时所谓的”常量“意义上不太一样,这里的常量池也不是说仅仅用来存储源码中定义的那些常量和字面上的,这里的常量池更像是一张链接表,我们看到第一行和第二行分别对应了方法和属性所需要使用到的外部链接,第三行和第四行对应了当前类信息需要使用的外部链接。说完了常量池,接下来说说运行时常量池。

运行时常量池:

运行时常量池存储着两大类数据,第一种是编译器期间产生的,主要是字节码中定义的静态信息,比如说由字节码生成的class对象(上面所说的Constant Pool就包含在内) ,再比如有字节码生成的字面量,这就是我们在编写代码中所以定义的常量自变量

第二大类运行期间产生的,这部分就比较灵活,虚拟机开发者可以将必要的信息都放进去,比如说运行时会将一部分的符号应用转换为直接引用,那么这些直接引用就可以存储进来。再比如说常见的字符串常量池。

方法区的垃圾回收呢其实不算是重点,因为方法区的数据大部分需要稳定使用,所以一般不关注该区域的垃圾回收,但是这并不意味着完全不需要垃圾回收了,当方法区的内存占用到达一定阈值,还是需要回收一下意思意思的,不然也可能会抛出OOM的异常。那么哪些信息可能会被回收呢?

比如说通过类加载进入方法区的类型信息,当内存紧张的时候可能会对小部分类进行卸载,被卸载的类需要再次使用的时候呢,就需要再次重新加载,再比如说上面提到的运行时常量池中的这个字符串常量值,当内存紧张的时候,也会对其进行部分回收。

字符串常量池:

  • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

  • JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

    • 为字符串开辟一个字符串常量池,类似于缓存区

    • 创建字符串常量时,首先坚持字符串常量池是否存在该字符串

    • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

  • 实现的基础

    • 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享

    • 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收

字符串对象的创建

面试题:String str4 = new String(“abc”) 创建多少个对象?

  1. 在常量池中查找是否有“abc”对象

    • 有则返回对应的引用实例

    • 没有则创建对应的实例对象

  2. 在堆中 new 一个 String("abc") 对象

  3. 将对象地址赋值给str4,创建一个引用

所以,常量池中没有“abc”字面量则创建两个对象,否则创建一个对象,以及创建一个引用

根据字面量,往往会提出这样的变式题:

String str1 = new String("A"+"B") ; 会创建多少个对象?
String str2 = new String("ABC") + "ABC" ; 会创建多少个对象?

str1:
字符串常量池:"A","B","AB" : 3个
堆:new String("AB") :1个
引用: str1 :1个
总共 : 5个

str2 :
字符串常量池:"ABC" : 1个
堆:new String("ABC") :1个
引用: str2 :1个
总共 : 3个

如何判断对象是否死亡?

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

2.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
    Object instance = null;
	public static void main(String[] args) {
		ReferenceCountingGc objA = new ReferenceCountingGc();
		ReferenceCountingGc objB = new ReferenceCountingGc();
		objA.instance = objB;
		objB.instance = objA;
		objA = null;
		objB = null;

	}
}

2.2 可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

正在上传…重新上传取消​

可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

2.3 再谈引用

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

1.强引用(StrongReference)

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动

虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

2.4 不可达的对象并非“非死不可”

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

2.5 如何判断一个常量是废弃常量?

运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?

JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

🐛 修正(参见:issue747reference :

  1. JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
  3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。

2.6 如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

        

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值