JVM总结

JVM总结

1.JVM原理:

我们都知道Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码 。每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了 。

2.JVM的生命周期

JVM实例:JVM实例对应了一个独立运行的java程序——进程级别。一个运行时的Java虚拟机(JVM)负责运行一个Java程序。
当启动一个Java程序时,一个虚拟机实例诞生;当程序关闭退出,这个虚拟机实例也就随之消亡。 如果在同一台计算机上同时运行多个Java程序,将得到多个Java虚拟机实例,每个Java程序都运行于它自己的Java虚拟机实例中。

(1)JVM实例的诞生

当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。

(2)JVM实例的运行

main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。

(3)JVM实例的消亡

当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。

3.JVM体系结构:

  1. 类装载器ClassLoader:用来装载.class文件
  2. 执行引擎:执行字节码,或者执行本地方法
  3. 运行时数据区:方法区、堆、Java栈、程序计数器、本地方法栈
    在这里插入图片描述

3.1类的装载

将一个类加载到Java虚拟机中需要经历三个阶段:加载->链接(验证、准备,解析)->初始化。

  1. 加载:这是由类加载器(ClassLoader)执行的。1.通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据接口,3.根据字节码在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

  2. 链接:
    2.1.验证:验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求。
    2.2.准备:为静态域分配存储空间并设置类变量的初始值(默认值)。
    2.3.解析:将常量池中的符号引用转化为直接引用。

  3. 初始化:类的初始化顺序 :父类(静态变量、静态代码块)–>子类(静态变量、静态代码块)–>父类(变量、代码块)–> 父类构造器–>子类(变量、初始化块)–>子类构造器。注意:静态代码和静态变量同级,变量和代码块同级。谁在前先执行谁。类只会初始化一次。
    补充:静态初始化,是在加载类的时候初始化。而非静态初始化,是new类实例对象的时候加载。

类什么时候才被初始化: 1.创建类的实例,2.访问类或接口的静态变量,调用类的静态方法 3.反射,4.初始化一个类的子类(会首先初始化子类的父类)

3.2类装载器

3.2.1类装载器的种类
  1. 启动类加载器 也叫根加载器 (Bootstrap) :系统启动的时候,首先会通过由C++实现的启动类加载器,加载<JAVA_HOME>/lib目录下面的jar包,或者被-Xbootclasspath参数指定的路径并且被虚拟机识别的文件名的jar包。把相关Class加载到方法区中。用户不可直接使用。
  2. 扩展类加载器 (Extension) :负责加载<JAVA_HOME>/lib/ext目录下或者被java.ext.dirs系统变量指定的路径中的类。
  3. 应用程序类加载器 也叫系统类加载器(AppClassLoader):负责加载用户类路径下制定的类库,如果应用程序没有自定义过自己的类加载器,此类加载器就是默认的类加载器。可以通过getSystemClassLoader方法得到应用程序类加载器。
  4. 用户自定义加载器 : 如果你的程序有特殊的需求,你可以自定义你的类加载器的加载方式 。用户自定义加载器类要继承ClassLoader抽象类。
3.2.2类加载器的加载流程

在这里插入图片描述
注意,如上图通过以上三个类加载器加载类到方法区之后,方法区中分别对应有各自的类信息存储区。不同类加载器加载的同一个类文件不相等。

3.2.3类装载器的双亲委派机制

在这里插入图片描述

  • 一个类加载器收到了类加载请求,不会自己立刻尝试加载类,而是把请求委托给父加载器去完成,每一层都是如此,所有的类在请求最终都传递到最顶层的类加载器进行处理;
  • 如果父加载器不存在了,那么尝试判断有没有被启动类加载器加载;
  • 如果没有被加载,则再自己尝试加载。

问题:为什么要有这么复杂的双亲委派机制?
如果没有这种机制,我们就可以篡改启动类加载器中需要的类了,如,修自己编写一个java.lang.Object用自己的类加载器进行加载,系统中就会存在多个Object类,这样Java类型体系最基本的行为也就无法保证了。

双亲委派机制的特点

1.可见性原则: 应用类加载器是可以读取到由扩展类加载器和启动类加载器加载进来的Class;扩展类加载器是可以读取到由启动类加载器加载进来的Class;
2.唯一性: 类是唯一的,没有重复的类;

转载自:一篇图文彻底弄懂类加载器与双亲委派机制

3.3执行引擎

执行引擎负责具体的代码调用及执行过程。

输入:字节码文件
处理:字节码解析
输出:执行结果。

类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),然后执行引擎执行会执行这些字节码。执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。
不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。字节码可以通过以下两种方式转换成合适的语言。

  1. 解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
  2. 即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

3.4运行时数据存储区域

JVM 的运行时数据区主要包括:堆、栈(虚拟机栈,本地方法栈)、方法区、程序计数器等。而 JVM 的优化问题主要在线程共享的数据区中:堆、方法区。
在这里插入图片描述

3.4.1程序计数器(线程私有的)

程序计数器:是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码。字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令。 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

举例:在多线程中,线程需要频繁切换,如果线程A执行一段代码执行到一半,线程抢到了CPU执行权,当线程B执行完后,如何能够找到线程A被抢断执行到的位置呢,这就是计数器的作用。计数器就记录了线程正在执行的内存地址,以确保被打断是能够回到原来的地方再次执行。

补充:如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。
由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

3.4.2栈(线程私有的)

栈是后进先出的。JVM 中的栈包括 Java 虚拟机栈本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 方法服务,本地方法栈则为 JVM 使用到的 Native 方法服务。

JDK 中有很多方法是使用 Native 修饰的。Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)。Native 方法是与操作系统直接交互的。比如通知垃圾收集器进行垃圾回收的代码 System.gc(),就是使用 native 修饰的。

在这里插入图片描述

栈帧是栈的元素。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。

局部变量表
栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型的局部变量(包括参数)、和对象的引用。但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。

JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用
在这里插入图片描述
Slot 复用

为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。

凡事有利弊。Slot 复用虽然节省了栈帧空间,但是会伴随一些额外的副作用。比如,Slot 的复用会直接影响到系统的垃圾收集行为。具体实例演示请看下方的转载原文。

操作数栈
操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。

通过一段代码来了解操作数栈。

public class OperandStack{
 
    public static int add(int a, int b){
        int c = a + b;
        return c;
    }
 
    public static void main(String[] args){
        add(100, 98);
    }
}

add 方法刚开始执行时,操作数栈是空的。把局部变量100压栈,局部变量98 压栈。然后弹出两个变量(100 和 98 出操作数栈),对 100 和 98 进行求和,然后将结果 198 压栈。最后弹出结果198(出栈)。

下面通过一张图,对比执行100+98操作,局部变量表和操作数栈的变化情况。
在这里插入图片描述

方法返回地址
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

补充:
指向运行时常量池的引用:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

转载自:一文搞懂JVM内存结构

3.4.3方法区(线程共享)

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段和class文件常量池等)+运行时常量池存在方法区中。
注:JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。
在这里插入图片描述

常量池 
常量池分为class常量池、运行时常量池和字符串常量池

class文件常量池
在.class文件中除了有类的版本【高版本可以加载低版本】、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)【此时没有加载进内存,也就是在文件中】,用于存放编译期生成的各种字面量和符号引用。

下面用一张图来表示常量池里存储的内容:
在这里插入图片描述

字面量: 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值,基本数据类型等。

符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

主要包括以下常量:
1.类和接口和全限定名:例如String这个类,它的全限定名就是java/lang/String。
2.字段的名称和描述符:所谓字段就是类或者接口中声明的变量,包括类级别变量(static)和实例级的变量。
3.方法的名称和描述符。所谓描述符就相当于方法的参数类型+返回值类型。

直接引用:直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。

当第一次运行时,要根据符号引用的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。

运行时常量池

当class文件被加载完成后,jvm就会将class常量池中的内容存放到运行时常量池中, 由此可知,运行时常量池也是每个类都有一个。 在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

运行时常量池是当class文件被加载完成后,java虚拟机会将class文件常量池里的内容转移到运行时常量池里,在class文件常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

运行时常量池里的内容除了是class文件常量池里的内容外,还将class文件常量池里的符号引用转变为直接引用,而且运行时常量池里的内容是能动态添加的。例如调用String的intern方法就能将string的值添加到String常量池中,这里String常量池是包含在运行时常量池里的。

字符串常量池
字符串常量池会用来存放字符串,字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

注:在jdk1.8后,将String常量池放到了堆中。

转载自:Java几种常量池区分(字符串常量池、class常量池和运行时常量池)

3.4.4堆(线程共享)

堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存用于存放由new创建的对象和数组
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

年轻代

年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

老年代

老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。
注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。

永久代:

永久代:方法区,不属于java堆,另一个别名为“非堆Non-Heap”。

堆的内存模型大致为:

在这里插入图片描述

4.JVM垃圾回收机制

GC的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。

哪些内存需要回收?

JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此不需要过多考虑回收的问题。因为方法结束或者线程结束时,内存自然就跟随着回收了。Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

4.1引用计数法

堆中每个对象实例都有一个引用计数器。当对象被引用一次,计数器加1。当对象引用失效一次,计数器减1。对于计数器为0的对象意味着是垃圾对象可以被GC回收。

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

下面通过一段代码来对比说明:

 public class GcDemo {
public static void main(String[] args) {
    //分为6个步骤
    GcObject obj1 = new GcObject(); //Step 1
    GcObject obj2 = new GcObject(); //Step 2

    obj1.instance = obj2; //Step 3
    obj2.instance = obj1; //Step 4

    obj1 = null; //Step 5
    obj2 = null; //Step 6
 }
}

class GcObject{
public Object instance = null;
}

在这里插入图片描述
再回到前面代码GcDemo的main方法共分为6个步骤:
Step1:GcObject实例1的引用计数加1,实例1的引用计数=1;
Step2:GcObject实例2的引用计数加1,实例2的引用计数=1;
Step3:GcObject实例2的引用计数再加1,实例2的引用计数=2;
Step4:GcObject实例1的引用计数再加1,实例1的引用计数=2;
执行到Step 4,则GcObject实例1和实例2的引用计数都等于2。

接下来继续结果图:

在这里插入图片描述

Step5:栈帧中obj1不再指向Java堆,GcObject实例1的引用计数减1,结果为1;
Step6:栈帧中obj2不再指向Java堆,GcObject实例2的引用计数减1,结果为1。
到此,发现GcObject实例1和实例2的计数引用都不为0,那么如果采用的引用计数算法的话,那么这两个实例所占的内存将得不到释放,这便产生了内存泄露。

4.2可达性分析算法

这是目前主流的虚拟机都是采用GC Roots Tracing算法,比如Sun的Hotspot虚拟机便是采用该算法。 该算法的核心算法是从GC Roots对象作为起始点,利用数学中图论知识,图中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。这里涉及两个概念,一是GC Roots,一是可达性。

可以作为GC Roots的对象:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表);
  • 方法区中类静态属性或常量引用的对象;
  • 本地方法栈中JNI(Native方法)引用的对象。

关于可达性的对象,便是能与GC Roots构成连通图的对象,如下图:
在这里插入图片描述

从上图,reference1、reference2、reference3都是GC Roots,可以看出:
reference1-> 对象实例1;
reference2-> 对象实例2;
reference3-> 对象实例4;
reference3-> 对象实例4 -> 对象实例6;
可以得出对象实例1、2、4、6都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。
而对于对象实例3、5直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。

转载自:java中垃圾回收机制中的引用计数法和可达性分析法(最详细)

4.3JVM对象的四种引用类型

强引用在程序代码中普遍存在的,类似Object obj=new Object()这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用也叫幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知(用来得知对象是否被GC)。

无论引用计数算法还是可达性分析算法都是基于强引用而言的。

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;

第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

4.4常用的垃圾回收算法

4.4.1标记-清除(Mark-Sweep)算法

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
在这里插入图片描述主要缺点:

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

4.4.2复制(Copying)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题
在这里插入图片描述
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

4.4.3标记-整理(Mark-Compact)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

在这里插入图片描述

4.4.4分代收集(Generational Collection)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域(年轻代,老年代,永久代)。根据不同代的特点采取最适合的收集算法。

新生代几乎是所有 Java 对象出生的地方,Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。特点是每次垃圾回收时都有大量的对象需要被回收,新生代是 GC 收集垃圾的频繁区域。因此采用复制算法

1.年轻代分三个区。一个Eden区,两个 Survivor区(内存比8:1)。
2.大部分对象在Eden区中生成。当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。然后To区和From区名称互换,即To区变为From区,From区变为To区。
3.对象每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会进入老年代。

年老代中存放的一般都是一些生命周期较长的对象。特点是每次垃圾收集时只有少量对象需要被回收,因此采用标记-清除算法或标记-整理算法

哪些对象会进入老年代
1.新生成的大对象:指的是需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组 (例如:byte[] 数组)
2. 长期存活的对象:对象每经历一次 Minor GC,年龄就增加一岁,当它的年龄增加到一定程度时 (默认为 15 岁),就会晋升到老年代中
3. 动态对象年龄判断: 如果在 Survivor 空间中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象就可以直接进入老年代,而无须等到MaxTenuringThreshold 中要求的年龄
4. Survivor 区中存放不下的对象:对象优先在 Eden 区中分配,当 Eden 区中没有足够的空间分配时,会触发一次 Minor GC,每次 Minor GC 结束后 Eden 区就会被清空,其会将依然存活的对象放到 Survivor 区中,当 Survivor 区中放不下时,则由分配担保机制进入到老年代中

永久代:方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

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

4.5常见的垃圾收集器

传送门:十一、常见的垃圾收集器

补充知识点:

  1. 内存泄漏 :即部分对象虽然已经不再使用,但是因为有root持有引用,所以并没有被销毁,所占用的内存一直没有被释放。一次两次发生影响不大。如果频繁发生,最终会出现内存溢出。
  2. 内存溢出 :通俗的讲,就是指程序申请内存时,发现内存不够用。

全文参考:深入详细讲解JVM原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值