Java虚拟机(JVM)面试题

 JVM

说一下 JVM 的主要组成部分及其作用?

总体来说,方法区和堆是所有线程共享的内存区域;而虚拟机栈、本地方法栈和程序计数器的运行是线程私有的内存区域,运行时数据区域就是我们常说的JVM的内存

  • 类加载子系统:根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区中的方法区中,类加载器主要分为三种:启动类加载器、扩展类加载器和应用程序类加载器。
  • Java堆:堆就是给对象分配内存,用来存放对象实例
  • 方法区:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(注意这里没有局部变量,局部变量是存在栈中的)
  • 程序计数器:用于记录当前线程所执行的字节码指令地址
  • 虚拟机栈【Java方法】:每个java方法被执行的时候都会同时在虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、返回方法地址等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
  • 本地方法栈:与虚拟机栈类似,用于支持Java调用本地方法(Native Method)。本地方法是用C、C++等编写的方法,可以直接调用操作系统的底层接口和硬件资源,实现Java程序无法完成的功能。
  • 执行引擎(解释器):负责将编译后的字节码指令翻译成机器码并执行。执行引擎主要分为两种:解释器和即时编译器。
    • 扩展:解释器是最基本的执行引擎,它将每个字节码指令逐条翻译成机器码并执行。解释器的优点是简单、易于实现,可以在任何平台上运行,但缺点是执行效率低,因为每次执行都需要将字节码翻译成机器码。为了提高执行效率,JVM还提供了即时编译器(Just-In-Time Compiler,JIT Compiler),它将整个方法的字节码编译成本地机器码后再执行。即时编译器的优点是执行效率高,但缺点是编译时间长,且需要占用大量的内存空间。
    • 在实际运行中,JVM会根据程序的运行情况自动选择使用解释器或即时编译器。对于一些热点代码(即被频繁执行的代码),JVM会将其编译成本地机器码并缓存起来,以提高执行效率。这种技术被称为即时编译(Just-In-Time Compilation,JIT Compilation)。
  • 本地库接口:本地库接口(Native Interface,JNI)是Java提供的一种机制,用于在Java程序中调用本地方法(Native Method)。本地方法是用C、C++等编写的方法,可以直接调用操作系统的底层接口和硬件资源,实现Java程序无法完成的功能。
    • 扩展:JNI允许Java程序通过本地方法调用本地库中的函数,将Java程序与本地代码进行交互。在Java程序中,可以使用native关键字声明一个本地方法,然后通过JNI调用本地方法。在本地代码中,可以通过JNIEnv结构体和Java Native Interface函数来访问Java对象、调用Java方法等。JNI的使用需要遵循一定的规范,包括命名规范、数据类型映射规范、异常处理规范等。使用JNI需要编写C、C++等本地代码,因此需要熟悉本地编程语言的语法和特性。同时,使用JNI可能会带来一些性能上的损失,因为需要进行本地方法调用和数据转换等操作。需要注意的是,使用JNI需要谨慎,因为本地方法可能会访问操作系统的底层接口和硬件资源,存在安全风险。在使用JNI时,需要确保本地方法的安全性和可靠性,并遵守相关的安全规范和最佳实践。

骚戴理解:本地方法栈与虚拟机栈的区别在于,虚拟机栈用于存储Java方法的信息,而本地方法栈用于存储本地方法的信息。本地方法栈也是线程私有的,每个线程都有自己的本地方法栈,互不干扰。需要注意的是,本地方法栈与虚拟机栈的区别在于所处理的方法不同,但它们的作用和结构都非常相似。因此,在实现JVM时,通常会将本地方法栈和虚拟机栈合并为一个栈来实现,这也是为什么HotSpot JVM中只有虚拟机栈而没有本地方法栈的原因。

JVM各个组成部分的作用

首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要执行引擎(Execution Engine)在运行时解释执行字节码,将字节码转换为机器码并执行,同时提供Java程序运行所需的各种资源和服务,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能(调用本地库接口因为不同的操作系统的底层是C或C++语言写的,然后虚拟机要把这个class字节码转为对应操作系统的机器码就需要调用本地库接口来调用本地库方法,里面有其他语言的交互)

Java程序运行机制详细说明

简单来说Java程序的运行机制分为编写、编译和运行三个步骤。

1.编写

编写是指在Java开发环境中进行程序代码的编辑

2.编译

编译是指使用Java编译器对源文件进行错误排查的过程,编译后将生成后缀名为.class的字节码文件,该文件可以被Java虚拟机(JVM)的解释器正常读取。

3.运行

运行是指使用Java解释器将字节码文件翻译成机器代码,执行并显示结果。字节码文件是一种和任何具体机器环境及操作系统环境无关的中间代码,它是一种二进制文件,是Java源文件由Java编译器编译后生成的目标代码文件。编程人员和计算机都无法直接读懂字节码文件,它必须由专用的Java解释器来解释执行,因此Java是一种在编译基础上进行解释运行的语言。

在运行Java程序时,首先会启动JVM,然后由它来负责解释执行Java的字节码,并且Java字节码只能运行于JVM之上。这样利用JVM就可以把Java字节码程序和具体的硬件平台以及操作系统环境分隔开来,只要在不同的计算机上安装了针对于特定具体平台的JVM,Java程序就可以运行,而不用考虑当前具体的硬件平台及操作系统环境,也不用考虑字节码文件是在何种平台上生成的。JVM把这种不同软硬件平台的具体差别隐藏起来,从而实现了真正的二进制代码级的跨平台移植。JVM是Java平台无关的基础,Java的跨平台特性正是通过在JVM中运行Java程序实现的。接下来了解一下Java的运行流程,如图所示。

Java运行流程

图中,从编写出来的Java源文件,到编译为字节码文件,再到通过JVM的解释器解释成不同平台的机器语言,这些机器语言最后在不同的平台上执行得到java程序,然后将程序的运行结果展示给用户,这是一个完整的Java运行流程。

说一下 JVM 运行时数据区

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:

不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

深拷贝和浅拷贝

浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。

深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

浅拷贝

浅拷贝(shallowCopy)只是增加了一个引用指向已存在的内存地址,通过浅拷贝复制一个新对象,把这个新创建的对象的引用指向原来对象的内存地址

骚戴理解:也就是新对象和原来的对象都是用的同一个对象,两个的引用指向同一个对象。所以修改新对象的属性值会修改原来对象的属性值

另一种描述角度

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象

浅拷贝演示

package com.ys.test;

public class Person implements Cloneable{
    public String pname;
    public int page;
    public Address address;
    public Person() {}
    
    public Person(String pname,int page){
        this.pname = pname;
        this.page = page;
        this.address = new Address();
    }
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    public void setAddress(String provices,String city ){
        address.setAddress(provices, city);
    }
    public void display(String name){
        System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address);
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }
    
}
package com.ys.test;

public class Address {
    private String provices;
    private String city;
    public void setAddress(String provices,String city){
        this.provices = provices;
        this.city = city;
    }
    @Override
    public String toString() {
        return "Address [provices=" + provices + ", city=" + city + "]";
    }
    
}

下面我们产生一个 Person 对象,并调用其 clone 方法复制一个新的对象。

注意:Object 类提供的 clone是只能实现浅拷贝的,调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。

@Test
public void testShallowClone() throws Exception{
    Person p1 = new Person("zhangsan",21);
    p1.setAddress("湖北省", "武汉市");
    Person p2 = (Person) p1.clone();
    System.out.println("p1:"+p1);
    System.out.println("p1.getPname:"+p1.getPname().hashCode());
    
    System.out.println("p2:"+p2);
    System.out.println("p2.getPname:"+p2.getPname().hashCode());
    
    p1.display("p1");
    p2.display("p2");
    p2.setAddress("湖北省", "荆州市");
    System.out.println("将复制之后的对象地址修改:");
    p1.display("p1");
    p2.display("p2");
}

运行结果

首先看原始类 Person 实现 Cloneable 接口,并且覆写 clone 方法,它还有三个属性,一个引用类型 String定义的 pname,一个基本类型 int定义的 page,还有一个引用类型 Address ,这是一个自定义类,这个类也包含两个属性 pprovices 和 city 。

接着看测试内容,首先我们创建一个Person 类的对象 p1,其pname 为zhangsan,page为21,地址类 Address 两个属性为 湖北省和武汉市。接着我们调用 clone() 方法复制另一个对象 p2,接着打印这两个对象的内容。

从第 1 行和第 3 行打印结果:

p1:com.ys.test.Person@349319f9

p2:com.ys.test.Person@258e4566

可以看出这是两个不同的对象。

从第 5 行和第 6 行打印的对象内容看,原对象 p1 和克隆出来的对象 p2 内容完全相同。

代码中我们只是更改了克隆对象 p2 的属性 Address 为湖北省荆州市(原对象 p1 是湖北省武汉市) ,但是从第 7 行和第 8 行打印结果来看,原对象 p1 和克隆对象 p2 的 Address 属性都被修改了。

也就是说对象 Person 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化。

深拷贝

深拷贝(deepCopy)是增加了一个引用,同时申请了一个新的内存,并且把新增加的引用指向新的内存,使用深拷贝的情况下,释放内存的时候不会出现浅拷贝释放同一个内存的错误,通过深拷贝创建一个新对象,在堆中重新开一片内存,把原来的对象给复制一份,然后把这个新对象的引用指向这个新开辟的内存地址。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。

另外一个角度描述

深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容

类装载方式

Java类装载方式包括命令行方式、Class.forName()方式和类加载器方式。下面分别介绍这三种方式及其案例。

1. 命令行方式

命令行方式是最简单的一种类装载方式,使用java命令显式地加载类。例如,我们可以使用以下命令来加载HelloWorld类:

java HelloWorld

2. Class.forName()方式

Class.forName()方式使用Class类的静态方法forName()来加载类。例如,我们可以使用以下代码来加载HelloWorld类:

Class.forName("HelloWorld");

3. 类加载器方式

类加载器方式是最常用的一种类装载方式,使用类加载器ClassLoader来加载类。例如,我们可以使用以下代码来加载HelloWorld类:

ClassLoader.getSystemClassLoader().loadClass("HelloWorld");

骚戴理解:Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

什么是类加载器?类加载器有哪些?

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到JVM 内存,然后再转化为 class 对象。

主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用(是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库)
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类(负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库)
  3. 应用程序类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以 通过 ClassLoader.getSystemClassLoader()来获取它(负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没 有自定义类加载器默认就是用这个加载器)
  4. 自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现

骚戴扩展:Java核心类库包含了Java SE平台的基本功能,它是Java开发的基础和核心。Java核心类库包括以下几个方面的内容:

1. 基本数据类型和包装类:Java核心类库提供了8种基本数据类型及其对应的包装类,包括byte、short、int、long、float、double、char和boolean。

2. 字符串和文本处理:Java核心类库提供了丰富的字符串和文本处理类,包括String、StringBuffer、StringBuilder、正则表达式、字符编码转换等。

3. 集合框架:Java核心类库提供了丰富的集合框架类,包括List、Set、Map、Queue、Stack等,以及对应的实现类,例如ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等。

4. IO与文件处理:Java核心类库提供了丰富的IO和文件处理类,包括File、InputStream、OutputStream、Reader、Writer、FileReader、FileWriter等。

5. 网络编程:Java核心类库提供了网络编程相关的类,包括Socket、ServerSocket、URL、URLConnection等。

6. 多线程编程:Java核心类库提供了多线程编程相关的类,包括Thread、Runnable、ThreadLocal、Lock、Semaphore、CountDownLatch等。

7. 反射和动态代理:Java核心类库提供了反射和动态代理相关的类,包括Class、Method、Constructor、Field、Proxy等。

除了以上几个方面的内容,Java核心类库还包括了许多其他的类和接口,例如日期和时间处理、数字和数学计算、异常处理、国际化和本地化等。

Java核心类库是Java开发的基础和核心,掌握Java核心类库的使用对于Java程序员来说是非常重要的。

什么是双亲委派模型?为什么要使用双亲委派模型?

1、什么是双亲委派模型 ?

双亲委派机制:在加载一个类的时候首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派(如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归),请求最终将到达顶层的类加载器。 如果最顶层的类加载器(启动类加载器)可以完成类加载任务,就成功返回,无法加载该类时,再一层一层向下委派给子类加载器加载。

2、为什么要使用双亲委派模型 ?

双亲委派保证每一个类在各个类加载器中都是同一个类,一个非常明显的目的就是保证java官方的类库<JAVA_HOME>\lib和扩展类库<JAVA_HOME>\lib\ext的加载安全性,不会被开发者覆盖,说白了就是保证类的唯一性。

如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类。

例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载(用双亲委派模型的话任何类就只会有一个类加载器来进行加载,因为他会先向上委派,再向下委派,直到找到唯一的类加载器来进行加载这个类),因此Object类在程序的各种类加载器环境中都是同一个类。

如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的。如果想自定义类加载器,就需要继承ClassLoader,并重写findClass,如果想不遵循双亲委派的类加载顺序,还需要重写loadClass。

扩展:一个类的唯一性由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实例ID作为唯一标识)。比较两个类是否相等(包括Class对象的equals()、isAssignableFrom()、isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。

说一下 JVM 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是jconsole 和 jvisualvm 这两款视图监控工具。

jconsole:Java自带的一款图形化工具,可以用来监控JVM的运行状态、内存使用情况、线程情况等,并提供了丰富的分析和诊断工具,例如线程分析、堆转储、MBean操作等。jconsole可以方便地与Java程序进行交互,并支持多种插件,例如Visual GC、JMX Console等

jvisualvm:Java自带的一款可视化工具,可以用来监控JVM的运行状态、内存使用情况、线程情况等,并提供了丰富的分析和诊断工具,例如CPU分析、内存分析、线程分析等。jvisualvm可以方便地与Java程序进行交互,并支持多种插件,例如Visual GC、Java Flight Recorder等。

说一下 JVM 的作用?

在开发Java程序时,我们通常使用Java编译器将Java源代码编译为字节码,然后将字节码交给JVM来执行。在执行过程中,JVM会将字节码转换为机器码,并在内存中分配和管理Java程序所需的各种资源,例如对象、线程、堆栈等。同时,JVM还提供了垃圾回收机制来自动回收不再使用的内存,保证Java程序的内存使用效率和安全性。

因此,Java源代码编译为字节码是由Java编译器完成的,而JVM的主要作用是在运行时解释执行字节码,并提供Java程序运行所需的各种资源和服务。

说一下堆栈的区别?

堆和栈是两种不同的内存分配区域,它们在内存管理和使用方式上有很大的区别。

1. 堆(Heap):堆是Java程序运行时动态分配的内存区域,用于存储Java对象和数组。堆的大小可以通过JVM参数进行设置,堆的大小直接影响Java程序的性能和稳定性。堆是共享的,所有线程都可以访问同一个堆,因此需要进行同步处理。

2. 栈(Stack):栈是Java程序运行时的一种数据结构,用于存储方法调用的信息和局部变量。每个线程都有自己的栈,栈的大小是固定的,由JVM在运行时进行分配和管理。栈是线程私有的,不能被其他线程访问,因此不需要进行同步处理。

堆和栈的区别主要体现在以下几个方面:

1. 分配方式:堆是动态分配的,由JVM在运行时根据需要进行分配和管理;栈是静态分配的,由编译器在编译时进行分配和管理。

2. 内存管理:堆的内存管理由JVM进行,包括内存的分配、释放和垃圾回收等;栈的内存管理由编译器进行,包括内存的分配和释放等。

3. 分配大小:堆的大小可以通过JVM参数进行设置,可以动态调整;栈的大小是固定的,由编译器在编译时确定,无法动态调整。

4. 访问方式:堆是共享的,所有线程都可以访问同一个堆,需要进行同步处理;栈是线程私有的,不能被其他线程访问,不需要进行同步处理。

骚戴理解:静态变量放在方法区 ,静态的对象还是放在堆。

JAVA类加载过程/类装载的执行过程/java类加载机制/JVM加载Class文件的原理机制?

类加载的过程主要分为三个部分:(加链初,验准解)

  • 加载
  • 链接
  • 初始化

而链接又可以细分为三个小部分:

  • 验证
  • 准备
  • 解析

骚戴理解:首先把class字节码文件从各个来源通过类加载器装载入内存中(加载),然后验证加载进来的字节码是否符合虚拟机规范(验证),Java虚拟机为类变量分配内存,并且赋予默认初始值(准备),虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址,也就是直接引用(解析),执行类构造器对类变量进行自定义的初始化(初始化)

1、加载

简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。

这里有两个重点:

  • 字节码来源
    • 一般的加载来源包括从本地路径下编译生成的.class文件
    • 从jar包中的.class文件
    • 从远程网络中获取
    • 动态代理实时编译
  • 类加载器
    • 启动类加载器
    • 扩展类加载器
    • 应用类加载器
    • 自定义类加载器

注:为什么会有自定义类加载器?

  • 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
  • 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

2、验证

主要是为了保证加载进来的字节码是否符合虚拟机规范

  • 对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
  • 对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
  • 对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
  • 对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

3、准备(默认初始化)

主要是为类变量分配内存,并且赋予默认初始值

特别需要注意,初值不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。

比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456

4、解析

将常量池内的符号引用替换为直接引用的过程。

两个重点:

  • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的引用;而实例方法,实例变量的直接引用则是从实例的头引用开始算起到这个实例变量位置的偏移量

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

5、初始化(自定义初始化)

这个阶段主要是对类变量进行自定义的初始化,是执行类构造器的过程。其实就是根据程序员自己设置的值对类变量进行赋值

int a = 5;

在准备阶段就是为这个a变量分配内存空间,并且赋其默认值,也就是准备阶段a=0,因为int类型的默认值为0,然后在初始化阶段就是把程序员设置的值赋值给对应的类变量,所以把5赋值给a,a=5。

注意:类加载的几个阶段都只针对类变量,所以类变量以外的变量赋值不会在类加载过程中体现

常用的 JVM 调优的参数都有哪些?

JVM三大性能调优参数-Xms -Xmx -Xss的含义

-Xss:规定了每个线程虚拟机栈的大小

-Xms:堆的初始值

-Xmx:堆能达到的最大值

例如

-Xms2g:初始化堆大小为 2g;

-Xmx2g:堆最大内存为 2g;

常用的 JVM 调优的参数:

-XX:NewRatio=4:设置年轻的和老年代的内存比例为1:4;

-XX:SurvivorRatio=8:设置新生代Eden和Survivor 比例为8:2;

-XX:+UseParNewGC:指定使用ParNew + Serial Old垃圾回收器组合;

-XX:+UseParallelOldGC:指定使用ParNew + ParNew Old 垃圾回收器组合;

-XX:+UseConcMarkSweepGC:指定使用CMS + Serial Old垃圾回收器组合;

-XX:+PrintGC:开启打印gc信息;

-XX:+PrintGCDetails: 打印gc详细信息。

对象

对象的创建方式有哪几种?

通过 new 关键字

这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。比如 Object obj = new Object();

通过 Class 类的 newInstance() 方法

通过反射来实现,这种默认是调用类的无参构造方法创建对象。比如 Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();

通过 Constructor 类的 newInstance 方法

这和第二种方法类时,都是通过反射来实现。通过 java.lang.relect.Constructor 类的 newInstance() 方法指定某个构造器来创建对象。例如下面的指定Person类的第一个构造器来创建一个对象(getConstructors()[0]是指第一个构造器)

Person p3 = (Person) Person.class.getConstructors()[0].newInstance();

实际上第二种方法利用 Class 的 newInstance() 方法创建对象,其内部调用还是 Constructor 的 newInstance() 方法。

利用 Clone 方法

Clone 是 Object 类中的一个方法,通过 对象A.clone() 方法会创建一个内容和对象 A 一模一样的对象 B,clone 克隆,顾名思义就是创建一个一模一样的对象出来。这是浅克隆

Person p4 = (Person) p3.clone();

反序列化

序列化是把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

Java对象创建的流程

Java普通对象的创建流程,这里讨论的仅仅是普通Java对象,不包含数组和Class对象。

简单说java创建对象的流程

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过(加载、解析和初始化)。如果没有,那么须先执行相应的类加载过程,接下来虚拟机将为新创建的对象分配内存,内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),对对象头进行初始化的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。最后执行init方法是为了把对象按照程序员的意愿进行初始化

Java对象创建的流程

  • new指令

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过(加载、解析和初始化)。如果没有,那么须先执行相应的类加载过程。

  • 分配内存

接下来虚拟机将为新创建的对象分配内存。对象所需的内存的大小在类加载完成后便可完全确定。分配方式有“指针碰撞(Bump the Pointer)”和“空闲列表(Free List)”两种方式,具体由所采用的垃圾收集器是否带有压缩整理功能决定。

  • 初始化(默认初始化)

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  • 对象的初始设置

接下来虚拟机要对对象头进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如对否启用偏向锁等,对象头会有不同的设置方式。

  • <init>方法(自定义初始化)

在上面的工作都完成了之后,从虚拟机的角度看,一个新的对象已经产生了,但是从Java程序的角度看,对象还没创建完,执行init方法是为了把对象按照程序员的意愿进行初始化(应该是将构造函数中的参数赋值给对象的字段),这样一个真正可用的对象才算完全产生出来。

Java对象内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对其填充(Padding)。

对象头

HotSpot虚拟机的对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说,查找对象的元数据信息并不一定要经过对象本身)。如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度。

骚戴扩展:

元数据:在Java中,元数据是指描述Java程序中各种元素的数据,包括类、方法、字段等。元数据提供了关于这些元素的信息,例如它们的名称、类型、访问修饰符、注解等。

Java中的元数据主要有以下几种:

1. 注解:注解是一种用于描述程序元素的元数据,可以用于描述类、方法、字段等。注解使用@符号作为前缀,可以包含元素和默认值,可以通过反射在运行时获取注解信息。

2. 反射:反射是一种机制,可以在运行时获取类的信息,包括类的名称、方法、字段、构造函数等。通过反射,可以获取类的元数据信息,并进行动态操作。

3. JavaBeans:JavaBeans是Java平台中一种标准的组件模型,用于描述可重用的Java组件。JavaBeans包含一组属性、方法和事件,可以通过JavaBeans API获取组件的元数据信息。

4. XML描述文件:Java中的一些框架和工具,例如Spring、Hibernate等,使用XML文件来描述程序元素的信息,包括类、方法、字段等。这些XML文件可以被解析器解析,从而获取程序元素的元数据信息。

总的来说,Java中的元数据是描述程序元素的数据,包括注解、反射、JavaBeans和XML描述文件等。这些元数据提供了关于程序元素的信息,可以通过反射等机制来获取和操作。

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中定义的各种类型的字段内容,无论从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会虚拟机默认的分配策略参数和字段在Java源码中定义的顺序影响(相同宽度的字段总是被分配到一起)。

对齐填充

对齐填充部分并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是说,对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

分配内存的两种方式

类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java 堆是否规整,有指针碰撞和空闲列表两种方式

指针碰撞

如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。

空闲列表

如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录哪些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

分配内存的的时候要处理并发安全问题

在实际的开发过程中,会经常的创建对象。在并发的情况下创建对象会出现并发安全的问题,例如多个并发执行的线程创建对象,分配内存的时候,有可能在Java堆中的同一个位置申请(也就是并发的时候同时有两个不同的对象分配的内存是同一块内存),这就需要对这部分内存空间进行加锁或者采用CAS等操作保证线程安全,即保证该区域只分配给一个线程。作为虚拟机,必须保证线程安全。通常来讲虚拟机采用两种方式保证线程安全

CAS + 失败重试

CAS(Compare and Swap)是一种并发编程中常用的同步原语,用于实现多线程之间的同步操作。CAS操作包含三个操作数:内存位置V、期望值A和新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会进行任何操作。CAS操作是一种乐观锁策略,它假设执行操作的线程在操作期间不会被其他线程干扰,因此可以避免使用传统锁带来的性能问题,如果因为冲突导致操作失败就重试,直到成功为止

TLAB

TLAB(Thread-Local Allocation Buffer)是Java虚拟机中的一种优化技术,用于提高对象的分配效率。TLAB是每个线程独有的一个内存区域,用于存放线程私有的对象。当一个线程需要分配对象时,它会先在自己的TLAB中查找是否有足够的空间,如果有就直接在TLAB中分配对象,否则就会在堆中分配对象。

使用TLAB的好处是可以避免线程之间的竞争,提高对象分配的效率。由于每个线程都有自己的TLAB,因此多个线程同时分配对象时不会相互干扰。此外,由于TLAB是线程私有的,因此也可以避免不必要的内存竞争和锁竞争。

如果没有使用TLAB,多个并发执行的线程创建对象,分配内存的时候,有可能在Java堆中的同一个位置申请,这就需要对这部分内存空间进行加锁或者采用CAS等操作保证线程安全,即保证该区域只分配给一个线程。

骚戴理解: 当该线程创建的对象大于TLAB中的剩余内存或者TLAB的内存已用尽时,再采用CAS + 失败重试的方式分配内存。

对象的访问方式(对象的定位)

Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有句柄和指针访问两种方式。

指针访问:在Java虚拟机内部,对象是通过指针来访问的。每个对象在内存中都有一个唯一的地址,可以通过指针来直接访问对象的实例变量和方法。

句柄访问: Java虚拟机还支持句柄访问方式。在句柄访问方式中,Java虚拟机使用一个句柄来代表对象,句柄包含了对象的地址以及对象类型信息。当需要访问对象时,先通过句柄获取对象的地址,然后再通过地址访问对象的实例变量和方法。

在Java虚拟机中,对象的访问方式是由具体的实现来决定的。在一些较老的Java虚拟机中,可能会使用句柄访问方式,而在一些较新的Java虚拟机中,则普遍采用指针访问方式。另外,Java虚拟机还提供了一些针对对象的定位技术,例如TLAB、对象头指针等,可以进一步提高对象访问的效率。

垃圾回收机制

简述Java垃圾回收机制

在java中,程序员是不需要手动的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时才会触发执行,扫描那些没有被任何引用的对象并将它们进行回收。

GC是什么?为什么要GC?

GC是什么?

GC 是垃圾收集的意思(Gabage Collection)

为什么要GC(垃圾回收的目的)?

垃圾回收(Garbage Collection,GC)是一种自动内存管理机制,它的主要目的是在程序运行时自动识别和回收不再使用的内存空间,从而防止内存泄漏和内存溢出等问题。具体来说,垃圾回收的主要目的有以下几点:

1. 避免内存泄漏:在程序运行过程中,如果一些对象已经不再被引用,但是仍然占用着内存空间,这就是内存泄漏。垃圾回收可以自动检测和回收这些无用的对象,从而避免内存泄漏。

2. 避免内存溢出:内存溢出是指程序在运行时需要的内存空间超过了系统所能提供的内存空间,导致程序崩溃。垃圾回收可以及时回收不再使用的内存空间,从而避免内存溢出。

3. 提高程序性能:手动管理内存空间需要程序员自己负责内存的分配和回收,容易出现错误,而且会占用程序员的大量时间和精力。垃圾回收可以自动管理内存空间,减轻程序员的负担,提高程序的开发效率和性能。

总之,垃圾回收的主要目的是使程序更加健壮、稳定和高效。

什么时候进行垃圾回收?

垃圾回收是自动进行的,它在程序运行时会周期性地扫描堆内存中的对象,识别出不再被引用的对象并回收它们所占用的内存空间。具体来说,垃圾回收的时机和方式取决于所采用的垃圾回收算法和实现方式。

一般来说,垃圾回收的时机有以下几种:

1. 定时回收:程序运行一段时间后,定期触发垃圾回收,回收不再使用的内存空间。

2. 空闲回收:当程序处于空闲状态时,触发垃圾回收,回收不再使用的内存空间。

3. 增量回收:将垃圾回收过程分为多个阶段,在程序运行时逐步回收不再使用的内存空间,减少垃圾回收对程序性能的影响。

4. 分代回收:将堆内存分为多个代,每个代采用不同的回收策略,根据对象的存活时间和大小进行回收,提高垃圾回收的效率。

总之,垃圾回收的时机和方式是由具体的垃圾回收算法和实现方式决定的,开发者无需手动控制垃圾回收的时机。

垃圾回收的优点和原理?

垃圾回收的优点?

1. 方便:相较于手动管理内存,使用垃圾回收的编程语言可以让程序员更加专注于应用程序的逻辑和功能设计,而不必关注内存分配和释放的细节。这大大简化了开发过程,提高了生产力。

2. 安全:手动管理内存很容易出现内存泄漏或悬挂指针等问题,导致程序崩溃或运行不正常。使用垃圾回收可避免这些问题,因为垃圾回收器可以自动处理内存的分配和释放,并确保对象只有在没有引用时才被释放。

3. 高效:与手动管理内存相比,垃圾回收通常具有更好的性能。虽然垃圾回收的实现可能会消耗一些额外的 CPU 和内存资源,但它可以防止因错误的内存管理而导致的严重性能问题,从而使代码更加健壮且易维护。

4. 可伸缩性:由于垃圾回收器负责处理内存管理,它对于非常大型的应用程序和高并发系统非常有效,减少了手动调整内存使用量的难度。

总之,垃圾回收是一项非常重要的技术,它让开发者可以将精力和注意力集中在编写稳定、高效和可靠的代码上。

垃圾回收的原理?

垃圾回收的基本原理是通过追踪对象引用的情况来判断哪些对象可以标记为垃圾,然后由垃圾回收器自动将这些垃圾对象从内存中清除。以下是垃圾回收的一些主要算法:

  1. 标记-清除算法:该算法分两个阶段,第一个阶段是标记所有还在使用的对象,第二个阶段则是清除所有未被标记的对象。这种方法可能会产生内存碎片,增加了内存管理的复杂性。

  2. 复制算法:该算法将可用内存分成两块,每次只使用其中的一块,当垃圾收集时,将正在使用的内存中的存活对象复制到另一块中,并清除之前的内存块。这种算法易于实现,但是不适合大型对象和频繁的内存分配。

  3. 标记-整理算法:与标记-清除算法类似,但它会先进行标记,在移动存活对象时紧凑堆以消除碎片。这种算法需要更多的操作,但是能够更好地处理内存碎片问题。

以上算法都有其优缺点,因此实际应用中可能会采用多种垃圾回收算法,根据不同情况选择最适合的方法。

垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

垃圾回收器可以马上回收内存吗?

垃圾回收器并不总是能够立即回收内存,这取决于具体的垃圾回收算法和虚拟机实现。一般情况下,垃圾回收器会在管理堆内存时按照一定的规则进行内存回收,而不是等到内存用尽才触发垃圾回收。

有什么办法主动通知虚拟机进行垃圾回收?

在某些情况下,应用程序可以通过显式地调用System.gc()Runtime.getRuntime().gc()方法向虚拟机发出建议性的垃圾回收指令,即通知虚拟机进行垃圾回收。但是,由于Java编程语言规范只保证这种方法是建议性的,因此不能确保在任何情况下都会触发垃圾回收。

除了手动触发垃圾回收以外,垃圾收集器还有一些其他的触发条件,如堆大小超过一定阈值、对象的生命周期结束等,垃圾收集器会自动启动。虚拟机通常根据系统的空闲时间、CPU使用率、内存分配速度等情况来控制垃圾回收时机,从而最大限度地减少对应用程序的影响。

Java 中都有哪些引用类型?

强引用

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的。因此强引用是造成 Java 内存泄漏的主要原因之一。

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M。

public class TestOOM {
	private static List<Object> list = new ArrayList<>();
	public static void main(String[] args) {
	     testSoftReference();
	}
	private static void testSoftReference() {
		for (int i = 0; i < 10; i++) {
			byte[] buff = new byte[1024 * 1024];
			SoftReference<byte[]> sr = new SoftReference<>(buff);
			list.add(sr);
		}
		
		System.gc(); //主动通知垃圾回收
		
		for(int i=0; i < list.size(); i++){
			Object obj = ((SoftReference) list.get(i)).get();
			System.out.println(obj);
		}
		
	}
	
}

打印结果

我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。
这里就说明了在内存不足的情况下,软引用将会被自动回收。

弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

private static void testWeakReference() {
		for (int i = 0; i < 10; i++) {
			byte[] buff = new byte[1024 * 1024];
			WeakReference<byte[]> sr = new WeakReference<>(buff);
			list.add(sr);
		}
		
		System.gc(); //主动通知垃圾回收
		
		for(int i=0; i < list.size(); i++){
			Object obj = ((WeakReference) list.get(i)).get();
			System.out.println(obj);
		}
	}

虚引用

虚引用(Phantom Reference)是Java中四种引用类型之一,也是最弱的一种引用类型。虚引用的作用是帮助对象在被垃圾回收器回收时,可以在回收之前进行必要的清理工作。虚引用并不能通过它访问对象本身,也不能通过它访问对象的任何属性或方法,因为它的get()方法总是返回null。虚引用主要用于管理堆外内存,例如NIO Direct Memory中的内存。当虚引用所引用的对象被垃圾回收器回收时,虚引用会被放入一个ReferenceQueue中,以便在必要时进行清理工作。虚引用一般与ReferenceQueue一起使用,通过检查ReferenceQueue中的引用对象,可以确定对象已经被回收,从而进行相关的清理工作。虚引用通常由java.lang.ref.PhantomReference类来实现。

public class PhantomReference<T> extends Reference<T> {
    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * <code>null</code>.
     *
     * @return  <code>null</code>
     */
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

引用队列(ReferenceQueue)

引用队列(ReferenceQueue)是Java中用于管理引用对象的队列。当一个对象被垃圾回收器回收时,如果该对象有一个与之关联的引用队列,那么该引用对象就会被放入该引用队列中。通过引用队列,可以确定对象是否被垃圾回收器回收,从而进行相关的清理工作。

引用队列通常与弱引用、软引用和虚引用一起使用。当弱引用、软引用或虚引用所引用的对象被垃圾回收器回收时,会将该引用对象放入与之关联的引用队列中。通过检查引用队列中的引用对象,可以确定对象已经被回收,从而进行必要的清理工作。

引用队列的使用非常灵活,可以根据不同的需求来选择不同的引用类型和清理工作。例如,对于一些需要进行资源释放的对象,可以使用虚引用和引用队列来进行资源的清理工作。

骚戴理解:注意引用队列并不可以和强引用一起使用,与软引用、弱引用不同,虚引用必须和引用队列一起使用。

JVM中的永久代中会发生垃圾回收吗

jdk1.8以前

在JDK 8之前,JVM中的永久代(PermGen)是用于存储类信息、常量池等的区域,它是堆内存的一部分,但是它的垃圾回收机制与堆内存不同。

在JDK 8之前的版本中,永久代中也会发生垃圾回收。永久代中的垃圾回收主要针对已经加载的类信息和常量池等不再使用的数据,它们被认为是永久存在的,但实际上也可能会被回收。在永久代中发生垃圾回收的触发条件与堆内存中的垃圾回收触发条件不同,它们有着不同的垃圾回收机制和垃圾回收算法。在永久代中发生垃圾回收时,可能会导致类的卸载和加载,从而影响程序的性能和稳定性。因此,在JDK 8之前的版本中,需要根据实际情况来调整永久代的大小和垃圾回收机制,以提高程序的性能和稳定性。

骚戴扩展:在JDK 8之前的版本中,永久代(PermGen)是用于存储类信息、常量池等的区域,它是堆内存的一部分,但是它的垃圾回收机制与堆内存不同。永久代的垃圾回收机制和垃圾回收算法与堆内存不同,主要包括以下两种:

1. 标记-清除算法

永久代的垃圾回收机制使用的是基于标记-清除算法的垃圾回收机制。该算法分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会遍历永久代中的所有对象,将所有存活的对象进行标记。在清除阶段,垃圾回收器会清除所有未标记的对象,从而释放它们占用的内存空间。

2. Full GC

永久代的垃圾回收算法与堆内存不同,它使用的是Full GC(全垃圾回收)算法。Full GC是一种非常耗时的垃圾回收算法,它会停止应用程序的所有线程,然后遍历整个堆内存和永久代,将所有未被引用的对象进行回收。Full GC的触发条件与堆内存不同,它通常在永久代已满或者类加载器卸载时触发。

由于永久代的垃圾回收机制和垃圾回收算法与堆内存不同,因此在JDK 8之前的版本中,需要根据实际情况来调整永久代的大小和垃圾回收机制,以提高程序的性能和稳定性。

jdk1.8以后

在JDK 8及以后版本中,永久代被移除,取而代之的是元空间(Metaspace),它是使用本机内存来存储类信息、常量池等的区域,与JDK 8之前的永久代(PermGen)不同。元空间的默认情况下内存空间是使用的操作系统的内存空间,而不是Java虚拟机的堆内存空间。元空间的大小可以通过设置JVM参数来控制,例如使用"-XX:MaxMetaspaceSize"参数来指定元空间的最大大小。元空间的垃圾回收机制也与永久代不同,它使用的是基于标记-清除算法的垃圾回收机制。

骚戴扩展:在JDK 8及以后版本中,永久代(PermGen)被移除,取而代之的是元空间(Metaspace),它是使用本机内存来存储类信息、常量池等的区域。元空间的垃圾回收机制和垃圾回收算法与永久代不同,主要包括以下两种:

1. 基于标记-清除算法的垃圾回收机制

元空间的垃圾回收机制使用的是基于标记-清除算法的垃圾回收机制,与永久代相同。该算法分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会遍历元空间中的所有对象,将所有存活的对象进行标记。在清除阶段,垃圾回收器会清除所有未标记的对象,从而释放它们占用的内存空间。

2. 基于Compressed Class Space的垃圾回收机制

元空间还使用了一种基于Compressed Class Space的垃圾回收机制。Compressed Class Space是一个用于存储类元数据的区域,它使用了一种压缩算法来减少元数据的内存占用。在元空间中,Compressed Class Space是一个可选的区域,它可以根据需要动态地分配和释放内存空间。当Compressed Class Space中的元数据不再被应用程序使用时,垃圾回收器会将其进行回收,从而释放内存空间。

由于元空间的垃圾回收机制和垃圾回收算法与永久代不同,因此在JDK 8及以后版本中,不需要再调整元空间的大小和垃圾回收机制,从而简化了程序的管理和维护。

骚戴理解:无论是永久代还是元空间都会发生垃圾回收机制,但是两者使用的垃圾回收机制和算法不完全一样

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?

新生代垃圾回收器和老年代垃圾回收器都有哪些?

新生代回收器:Serial、ParNew、Parallel Scavenge

老年代回收器:Serial Old、Parallel Old、CMS

整堆回收器:G1

新生代垃圾回收器和老年代回收器有什么区别?

新生代和老年代是Java虚拟机中的两个重要的内存区域,它们的垃圾回收器有以下几个区别:

1. 垃圾回收算法不同

新生代主要使用的是复制算法,将内存空间分为两个相等的区域,每次只使用其中一个区域,当这个区域满了之后,将其中的存活对象复制到另一个区域中,然后清空原来的区域。这样就保证了每次垃圾回收后都有一块内存是空闲的,可以直接使用。而老年代主要使用的是标记-清除算法或标记-整理算法,将存活的对象标记出来,然后清除未标记的对象或将存活的对象整理到一起,从而释放内存空间。

2. 内存分配策略不同

新生代中的对象一般都是短时间存活的,因此采用了一种快速分配内存的方式,即使用指针碰撞或空闲列表等方式分配内存。而老年代中的对象一般都是长时间存活的,因此采用了一种更为复杂的内存分配方式,即使用分代分配策略或空间分配器等方式进行内存分配。

3. 垃圾回收时机不同

新生代中的垃圾回收一般是在新生代内存空间满了之后触发,而老年代中的垃圾回收一般是在老年代内存空间满了之后触发。此外,老年代中还会根据各种垃圾回收算法的特点和应用程序的实际情况,定期触发Full GC(全垃圾回收)来回收内存空间。

4. 垃圾回收的效率不同

新生代中的垃圾回收一般比老年代中的垃圾回收效率高,因为新生代中的对象一般都是短时间存活的,垃圾回收器只需要处理一部分对象,而老年代中的对象一般都是长时间存活的,垃圾回收器需要处理大量对象,所以效率相对较低。

Java会存在内存泄漏吗?请简单描述

是的,Java也可能存在内存泄漏问题。内存泄漏是指程序中申请的内存空间没有被正确释放,导致这些内存空间一直占用着系统的内存,最终导致系统内存不足或者程序运行变得缓慢。在Java中,内存泄漏通常是由以下几个原因引起的:

1. 对象没有被正确释放

在Java中,程序员通常使用new操作符来创建一个对象,并在使用完该对象后使用null或者手动调用对象的finalize()方法来释放对象所占用的内存空间。如果程序员没有正确地释放对象,那么这些对象会一直占用内存空间,最终导致内存泄漏。

2. 长生命周期的对象持有短生命周期对象的引用

在Java中,如果一个长生命周期的对象持有一个短生命周期对象的引用,那么这个短生命周期对象在使用完后可能无法被正确释放,从而导致内存泄漏。这种情况通常发生在使用缓存、线程池等技术时。

3. 资源没有被正确关闭

在Java中,程序员通常使用try-catch-finally语句块来处理文件、网络连接、数据库连接等资源。如果程序员没有正确地关闭这些资源,那么这些资源会一直占用内存空间,最终导致内存泄漏。

为什么新生代使用“复制算法”,老年代使用“标记-整理法”?

新生代使用“复制算法”是因为新生代中的对象生命周期较短,大部分对象都很快被回收,因此采用“复制算法”可以更快速地回收内存。复制算法将新生代分为两个相等的空间,每次只使用其中一个空间,当这个空间用完后,将其中还存活的对象复制到另一个空间,然后清空当前使用的空间,这样就可以保证当前空间中只有存活的对象。这种方式的优点是实现简单、运行高效,缺点是浪费了一部分内存空间。

老年代使用“标记-整理法”是因为老年代中的对象生命周期较长,很少被回收,因此采用“标记-整理法”可以更好地利用内存空间。标记-整理法是将所有存活的对象标记后,将它们向一端移动,然后清除端边界外的所有内存。这种方式的优点是可以更好地利用内存空间,缺点是实现比较复杂、运行效率相对较低。

骚戴理解

为什么新生代使用“复制算法”

因为新生代里绝大部分都是垃圾对象,可以使用复制算法将小部分存活对象复制到另一个区域,然后留下来的都是垃圾对象,可以一网打尽,一下子清除掉。因为存活的对象少,所以“复制”的次数少;虽然留下来的垃圾对象多,但是可以一网打尽,所以“删除”的次数也少。就像你有一个文件夹,里面有 1000 张图片,其中只有 3 张是有用的,你要删除余下的 997 张垃圾图片,你会怎么删除呢?你肯定不会一张一张的去删除 997 张垃圾图片,这样需要删除 997 次,你显然认为把 3 张有用的图片复制出去,然后将整个文件夹干掉来的快。这也就是为什么新生代使用复制算法合适、使用标记清除算法不合适。

为什么老年代使用“标记-整理法”

因为老年代里都是些“老不死”的对象,假设你有一个文件夹,里面有 1000 张图片,其中 997 张是有用的,你会怎样删除这其中的 3 张垃圾图片呢?你肯定不会将 997 张有用的图片复制出去,然后将整个文件夹干掉,因为复制的代价太大了,耗时久,而且 997 张图片的位置都变了,反应在 java 对象上,就是 997 个 java 对象相互之间引用的地址都得换个遍。相反,你会挨个去删除 3 张垃圾图片,因为删除次数少,也不需要大量移动文件。所以老年代适合使用标记清除算法、不适合使用复制算法( 移动 997 张图片,就为了可以整体一次删除 3 个垃圾图片,好傻 )

三种GC触发的情况?

Minor GC触发的条件

Minor GC(Young GC)是指对新生代进行垃圾回收的过程。Minor GC触发的条件通常包括以下几个方面:

1. Eden区满:当Eden区没有足够的空间分配新对象时,就会触发Minor GC。在Minor GC中,会对Eden区和Survivor区进行垃圾回收,将存活的对象复制到另一个Survivor区中。

2. Survivor区满:当Survivor区没有足够的空间容纳从Eden区复制过来的存活对象时,就会触发Minor GC。在Minor GC中,会对Eden区和两个Survivor区进行垃圾回收,将存活的对象复制到另一个Survivor区中。

需要注意的是,Minor GC只清理新生代中的对象,不会清理老年代中的对象。当新生代中的对象经过多次Minor GC之后,仍然存活下来的对象就会被移动到老年代中,等待Full GC进行回收。因此,Minor GC的频率通常比Full GC高,但每次回收的内存空间相对较小。

触发MajorGC的条件

Major GC(Full GC)是指对整个堆(包括新生代和老年代)进行垃圾回收的过程。Major GC触发的条件通常包括以下几个方面:

1. 老年代空间不足:当老年代中没有足够的空间容纳新分配的对象时,就会触发Major GC。在Major GC中,会对整个堆进行垃圾回收,包括新生代和老年代。

2. 永久代空间不足(对于使用永久代的虚拟机):永久代用于存放类信息、常量池等数据,当永久代空间不足时,就会触发Major GC。在Major GC中,会对整个堆进行垃圾回收,包括新生代、老年代和永久代。

3. System.gc()方法被显式调用:调用System.gc()方法可以建议虚拟机进行垃圾回收,但并不能保证立即触发Major GC。虚拟机可以根据当前堆的状态和垃圾回收策略来决定是否进行Major GC。

4. 空间分配担保失败:在进行Minor GC时,虚拟机会对Survivor区进行空间分配担保,以保证Minor GC的安全性。如果Survivor区的可用空间不足以容纳所有存活的对象,虚拟机会直接将这些对象移动到老年代中。如果老年代也没有足够的空间容纳这些对象,就会触发Major GC。

需要注意的是,Major GC的频率相对较低,但每次回收的内存空间相对较大,会导致较长的停顿时间。因此,在实际应用中,应该尽量避免触发Major GC,可以通过调整堆的大小、调整垃圾回收策略等方式来优化内存管理。

触发FullGC的条件

Full GC(Full Garbage Collection)是指对整个堆(包括新生代、老年代和永久代)进行垃圾回收的过程。Full GC的触发条件比较复杂,通常包括以下几个方面:

1. 老年代空间不足:当老年代中没有足够的空间容纳新分配的对象时,就会触发Full GC。在Full GC中,会对整个堆进行垃圾回收,包括新生代、老年代和永久代。

2. 永久代空间不足(对于使用永久代的虚拟机):永久代用于存放类信息、常量池等数据,当永久代空间不足时,就会触发Full GC。在Full GC中,会对整个堆进行垃圾回收,包括新生代、老年代和永久代。

3. System.gc()方法被显式调用:调用System.gc()方法可以建议虚拟机进行垃圾回收,但并不能保证立即触发Full GC。虚拟机可以根据当前堆的状态和垃圾回收策略来决定是否进行Full GC。

4. 空间分配担保失败:在进行Minor GC时,虚拟机会对Survivor区进行空间分配担保,以保证Minor GC的安全性。如果Survivor区的可用空间不足以容纳所有存活的对象,虚拟机会直接将这些对象移动到老年代中。如果老年代也没有足够的空间容纳这些对象,就会触发Full GC。

5. 系统空间不足:如果系统空间不足,就会导致虚拟机无法为堆分配足够的空间,从而触发Full GC。

6. 对象年龄达到阈值:当一个对象在Survivor区中存活了一段时间后,就会被移动到另一个Survivor区中。当一个对象在Survivor区中存活了一定次数(默认为15次),就会被晋升到老年代中。如果老年代没有足够的空间容纳这些对象,就会触发Full GC。为了避免这种情况,可以通过调整对象晋升的阈值来控制Minor GC的频率。

需要注意的是,Full GC的频率相对较低,但每次回收的内存空间相对较大,会导致较长的停顿时间。因此,在实际应用中,应该尽量避免触发Full GC,可以通过调整堆的大小、调整垃圾回收策略等方式来优化内存管理。

JVM怎么判断一个对象是否是垃圾对象?

有两种方法,分别是引用计数法和可达性分析算法

1、引用计数法

在对象中添加一个引用计数器,如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。

弊端:如果一个对象A持有对象B,而对象B也持有一个对象A(两个对象相互引用),那发生了类似操作系统中死锁的循环持有,这种情况下A与B的counter恒大于1,会使得GC永远无法回收这两个对象

解析:在对象头处维护一个counter,每增加一次对该对象的引用计数器自加,如果对该对象的引用失联,则计数器自减。当counter为0时,表明该对象已经被废弃,不处于存活状态。这种方式一方面无法区分软、虛、弱、强引用类别。另一方面,会造成死锁,假设两个对象相互引用始终无法释放counter,永远不能GC。

2、可达性分析算法

通过一系列被GC Roots引用的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则说明该对象是不可达的,即为垃圾对象。如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那么该对象会被标记为可回收的垃圾对象。如果被标记的对象覆盖了finalize()方法,那么JVM会将该对象放入F-Queue队列中,并在稍后的某个时间执行finalize()方法。执行finalize()方法的线程优先级很低,因此finalize()方法的执行时间是不确定的。执行finalize()方法可能会让对象重新与引用链相连,因此在执行finalize()方法之前,JVM会对F-Queue中的对象进行一次“自救”操作,即将这些对象移动到可达对象集合中,从而避免了这些对象被回收。在执行finalize()方法后,如果对象仍然没有被引用,那么该对象会被回收。如果对象在finalize()方法中重新与引用链相连,那么该对象会被移出F-Queue队列,不会被回收。

可以作为GC Roots的对象有哪些?

  1. 虚拟机栈中引用的对象:虚拟机栈中的本地变量表中引用的对象,包括各个线程的栈帧中引用的对象和静态变量中引用的对象。

  2. 方法区中类静态属性引用的对象:方法区中类静态属性引用的对象,如静态变量、常量等。

  3. 方法区中常量引用的对象:方法区中常量引用的对象,如字符串常量池中的字符串对象。

  4. 本地方法栈中JNI引用的对象:本地方法栈中JNI(Java Native Interface)引用的对象。

骚戴扩展:finalize()方法是Object类中定义的一个方法,它的作用是在对象即将被垃圾回收器回收之前被调用,用于完成一些资源释放、清理等工作。finalize()方法是Java语言提供的一种机制,用于在对象被回收之前进行一些清理工作,比如关闭文件、释放资源等。但是,由于finalize()方法的执行时间是不确定的,可能会导致垃圾回收器的延迟,因此不建议在finalize()方法中进行复杂的操作。

finalize()方法的执行时机是不确定的,因为垃圾回收器的具体实现是由虚拟机厂商自行决定的。在某些情况下,finalize()方法可能永远不会被执行,比如程序在运行过程中发生了异常、程序被强制终止等情况。因此,不应该将finalize()方法作为资源释放的唯一手段,应该使用try-finally语句块等机制来确保资源的释放。

在Java 9中,finalize()方法已经被标记为过时,建议使用try-with-resources语句块、PhantomReference等机制来替代finalize()方法完成资源释放、清理等工作。

如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

不一定会立即释放对象占用的内存。当一个对象的所有引用都被置为null时,该对象变为不可达对象,但是垃圾收集器并不会立即回收该对象的内存。相反,垃圾收集器会在某个时刻执行垃圾回收操作,将不可达对象回收,并释放其占用的内存。

具体来说,垃圾收集器的回收时机是不确定的,它受到多种因素的影响,如内存使用情况、垃圾回收器的类型、垃圾回收器的配置等。在某些情况下,垃圾收集器可能会立即执行回收操作,释放对象占用的内存;而在其他情况下,垃圾收集器可能会延迟回收操作,直到内存使用达到一定阈值或者系统资源充足时才执行回收操作。

因此,程序员不应该依赖于对象引用被置为null后立即释放内存的行为,而应该通过避免创建不必要的对象、手动调用System.gc()等方式来优化内存使用。

finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?

finalize()方法什么时候被调用?

finalize()方法是Object类中定义的一个方法,它的作用是在对象即将被垃圾回收器回收之前被调用,用于完成一些资源释放、清理等工作。finalize()方法是Java语言提供的一种机制,用于在对象被回收之前进行一些清理工作,比如关闭文件、释放资源等。但是,由于finalize()方法的执行时间是不确定的,可能会导致垃圾回收器的延迟,因此不建议在finalize()方法中进行复杂的操作。

finalize()方法的执行时机是不确定的,因为垃圾回收器的具体实现是由虚拟机厂商自行决定的。在某些情况下,finalize()方法可能永远不会被执行,比如程序在运行过程中发生了异常、程序被强制终止等情况。因此,不应该将finalize()方法作为资源释放的唯一手段,应该使用try-finally语句块等机制来确保资源的释放。

析构函数(finalization)的目的是什么?

析构函数(finalization)的目的是在对象被销毁之前执行一些清理工作,比如释放资源、关闭文件等。与Java中的finalize()方法不同,析构函数是C++语言中的概念,它是在对象被销毁时自动调用的。析构函数的执行时机是确定的,它在对象被销毁时自动调用,与垃圾回收器没有关系。

在Java中,由于有垃圾回收器的存在,对象的销毁时机是不确定的,因此Java中没有析构函数的概念。相反,Java提供了finalize()方法来完成与析构函数类似的功能,但是由于finalize()方法的执行时机不确定,因此不应该将其作为资源释放的唯一手段。

说一下 JVM 有哪些垃圾回收器?

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

新生代收集器(全部的都是复制算法):Serial、ParNew、ParallelScavenge

老年代收集器:CMS(标记-清理)、Serial Old(标记-整理)、Parallel Old(标记整理)

整堆收集器: G1(一个Region中是标记-清除算法,2个Region之间是复制算法) 同时,先解释几个名词:

  • 并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态
  • 并发(Concurrent):用户线程和垃圾收集线程同时执行
  • 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)

1.Serial收集器

Serial收集器是Java虚拟机中的一种垃圾收集器,它主要用于收集年轻代中的垃圾对象。它是一种单线程的垃圾收集器,它会暂停应用程序的执行,然后扫描年轻代中的对象,将不再被引用的对象标记为垃圾对象,并将它们回收。

Serial收集器的优点是简单高效,适用于单核处理器和小内存环境下的应用程序。但是,由于它是单线程的,所以它的垃圾收集效率较低,会导致应用程序的停顿时间较长。

在Java虚拟机中,可以通过设置参数来选择使用Serial收集器。例如,可以使用以下命令行参数来启用Serial收集器:

-XX:+UseSerialGC

此外,还可以通过设置以下参数来调整Serial收集器的行为:

-XX:NewRatio:设置年轻代和年老代的比例,默认值为2,表示年轻代占堆内存的1/3。

-XX:SurvivorRatio:设置Eden区和Survivor区的比例,默认值为8,表示Eden区占年轻代内存的8/10,Survivor区占年轻代内存的1/10。

-XX:MaxTenuringThreshold:设置对象进入年老代的年龄阈值,默认值为15,表示对象在年轻代中经过15次Minor GC后,会被移到年老代中。

2.ParNew收集器

ParNew收集器是一种年轻代垃圾收集器,它是Serial收集器的多线程版本。与Serial收集器类似,ParNew收集器也是使用标记-复制算法来回收年轻代中的垃圾对象,但它可以使用多线程来加速垃圾收集的过程。

ParNew收集器的优点是可以利用多核处理器的优势,提高垃圾收集效率,并且可以通过设置参数来控制线程数,以适应不同的硬件环境。此外,它还可以与CMS收集器配合使用,提供更高效的垃圾收集能力。

在Java虚拟机中,可以通过以下命令行参数来启用ParNew收集器:

-XX:+UseParNewGC

此外,还可以通过设置以下参数来调整ParNew收集器的行为:

-XX:ParallelGCThreads:设置垃圾收集的线程数,默认值为CPU核心数。

-XX:MaxTenuringThreshold:设置对象进入年老代的年龄阈值,默认值为15。

-XX:SurvivorRatio:设置Eden区和Survivor区的比例,默认值为8,表示Eden区占年轻代内存的8/10,Survivor区占年轻代内存的1/10。

总之,ParNew收集器是一种高效的年轻代垃圾收集器,可以在多核处理器上发挥优势,提供更快速的垃圾收集能力。

3.Parallel Scavenge 收集器

Parallel Scavenge收集器是一种年轻代垃圾收集器,它是一种多线程的收集器,与ParNew收集器类似,它也是使用标记-复制算法来回收年轻代中的垃圾对象。

Parallel Scavenge收集器的主要特点是注重吞吐量,即在尽可能短的时间内完成垃圾收集,以最大化应用程序的运行时间。它可以通过控制垃圾收集线程的数量和优先级来实现高吞吐量的垃圾收集。

与其他收集器不同,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,而不是尽可能减少停顿时间。因此,它的停顿时间可能会比其他收集器长一些,但是它可以在更短的时间内完成垃圾收集,从而提高应用程序的吞吐量。

在Java虚拟机中,可以通过以下命令行参数来启用Parallel Scavenge收集器:

-XX:+UseParallelGC

此外,还可以通过设置以下参数来调整Parallel Scavenge收集器的行为:

-XX:ParallelGCThreads:设置垃圾收集的线程数,默认值为CPU核心数。

-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,默认值为200毫秒。

-XX:GCTimeRatio:设置垃圾收集时间占总时间的比例,默认值为99,表示垃圾收集时间占总时间的1%。

总之,Parallel Scavenge收集器是一种注重吞吐量的垃圾收集器,可以在多核处理器上发挥优势,提供更高的应用程序吞吐量。

4.Serial Old收集器

在Java虚拟机中,Serial Old收集器是一种传统的、单线程的垃圾收集器,它主要用于收集老年代的垃圾对象。它采用标记-清除算法,首先标记需要回收的对象,然后清除这些对象,释放内存空间。

Serial Old收集器的优点是简单、高效,适用于小型应用程序和客户端应用程序。但是,它的缺点是无法充分利用多核CPU的优势,因为它是单线程的,不能并行处理垃圾回收任务。此外,它会导致应用程序暂停,因为在垃圾回收期间,应用程序必须等待垃圾回收完成,才能继续执行。

在Java 8之前,Serial Old收集器是默认的老年代收集器。但是,随着Java虚拟机的不断发展,现在更多的应用程序使用并发收集器或G1收集器来管理垃圾对象。

5.Parallel Old收集器

Parallel Old收集器是Java虚拟机中的一种并行垃圾收集器,它主要用于收集老年代的垃圾对象。与Serial Old收集器不同,Parallel Old收集器可以利用多核CPU的优势,使用多线程并行处理垃圾回收任务,从而提高垃圾回收的效率。

Parallel Old收集器采用标记-整理算法,首先标记需要回收的对象,然后将存活的对象移动到一端,然后清理另一端的对象,释放内存空间。由于采用了并行处理,Parallel Old收集器可以在短时间内完成垃圾回收任务,减少应用程序暂停的时间。

Parallel Old收集器适用于大型应用程序和服务器应用程序,可以在多核CPU上充分利用并行处理的优势。但是,它的缺点是在垃圾回收期间,会占用大量的CPU资源,可能会影响应用程序的性能。此外,它也可能会导致应用程序暂停,因为在垃圾回收期间,应用程序必须等待垃圾回收完成,才能继续执行。

6.CMS收集器

CMS收集器是Java虚拟机中的一种并发垃圾收集器,它主要用于收集老年代的垃圾对象。与Parallel Old收集器不同,CMS收集器可以在应用程序运行的同时进行垃圾回收,减少应用程序暂停的时间。

CMS收集器采用标记-清除算法,首先标记需要回收的对象,然后清除这些对象,释放内存空间。由于采用了并发处理,CMS收集器可以在应用程序运行的同时进行垃圾回收,减少应用程序暂停的时间。

CMS收集器适用于大型应用程序和服务器应用程序,可以在应用程序运行的同时进行垃圾回收,减少应用程序暂停的时间。但是,它的缺点是在垃圾回收期间,会占用一定的CPU资源,可能会影响应用程序的性能。此外,由于采用了标记-清除算法,CMS收集器可能会导致内存碎片问题,需要进行额外的处理来解决。

8.G1收集器

G1收集器是Java虚拟机中的一种并发垃圾收集器,它主要用于收集堆内存中的垃圾对象。与CMS收集器不同,G1收集器可以在应用程序运行的同时进行垃圾回收,减少应用程序暂停的时间。

G1收集器采用分代收集的思想,将堆内存分为多个小块,每个小块称为一个区域。它采用标记-整理算法,在每个区域内进行垃圾回收,然后将存活的对象复制到另一个区域。由于采用了并发处理和分代收集的思想,G1收集器可以在应用程序运行的同时进行垃圾回收,减少应用程序暂停的时间。

G1收集器适用于大型应用程序和服务器应用程序,可以在应用程序运行的同时进行垃圾回收,减少应用程序暂停的时间。它还可以根据应用程序的需求动态调整垃圾回收的时间和区域大小,以达到最优的性能。但是,它的缺点是在垃圾回收期间,会占用一定的CPU资源,可能会影响应用程序的性能。此外,由于采用了标记-整理算法,G1收集器不会产生内存碎片问题。

关于gc的选择

除非应用程序有非常严格的暂停时间要求,否则请先运行应用程序并允许VM选择收集器(如果没有特别 要求。使用JVM提供给的默认gc就好)。 如有必要,调整堆大小以提高性能。 如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起点:

  1. 如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+ UseSerialgc的串行收集器。
  2. 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+UseSerialgc的串行收集器。
  3. 如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受一秒或更长时间的暂停,则让VM选择收集器或使用-XX:+ UseParallelgc选择并行收集器
  4. 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选择具有-XX:+ UseG1gc。(值得注意的是JDK9中CMS已经被Deprecated,不可使用!移除该选项)
  5. 如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集的时间。
  6. 如果响应时间是高优先级,或使用的堆非常大,请使用-XX:UseZgc选择完全并发的收集器。(值得注意的是JDK11开始可以启动Zgc,但是此时Zgc具有实验性质,在JDK15中[202009发布]才取消实验性质的标签,可以直接显示启用,但是JDK15默认gc仍然是G1)
  7. 这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处 理器的数量和速度。
  8. 如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。
  9. 如果性能 仍然不足,尝试使用其他收集器

总体原则:减少STOP THE WORD时间,使用并发收集器(比如CMS+ParNew,G1)来减少暂停时间, 加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。

JVM 有哪些垃圾回收算法(回收机制)?

标记-清除算法

在Java虚拟机中,标记-清除算法是一种用于垃圾回收的算法。它分为两个阶段:标记阶段和清除阶段。

在标记阶段,垃圾收集器会遍历堆内存中的所有对象,标记所有存活的对象,即被引用的对象。这个过程通常从根对象开始,通过引用链遍历所有可达对象,并将它们标记为存活对象。

在清除阶段,垃圾收集器会遍历整个堆内存,清除所有未被标记的对象,即垃圾对象。

标记-清除算法的优点是简单、容易实现,适用于大型对象和长时间存活的对象。但是,它的缺点是会产生内存碎片问题,因为清除后的内存空间不连续,无法分配大型对象。此外,它还会导致应用程序暂停,因为在垃圾回收期间,应用程序必须等待垃圾回收完成,才能继续执行。

复制算法

在Java虚拟机中,复制算法是一种用于垃圾回收的算法。它将堆内存分为两个区域,每次只使用其中一个区域,称为“活动区域”,而另一个区域则称为“闲置区域”。当活动区域满时,垃圾收集器会将存活的对象复制到闲置区域,然后清空活动区域,以便下一次对象分配。

复制算法的优点是简单、高效,适用于大多数应用程序。由于每次只使用其中一个区域,因此不会产生内存碎片问题。此外,复制算法还可以与分代收集结合使用,将堆内存分为多个代,每个代使用不同的垃圾回收算法,以达到最优的性能。

但是,复制算法的缺点是需要两倍的内存空间,因为每个对象都需要复制到另一个区域。此外,由于复制算法每次只处理一半的对象,因此不适用于大型对象和长时间存活的对象。

标记-整理(压缩)算法

标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

优点:解决了标记-清理算法存在的内存碎片问题。

缺点:仍需要进行局部对象移动,一定程度上降低了效率。

分代算法

根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。当前商业虚拟机都采用分代收集的垃圾收集算法

分代算法详细内容看下面一题

介绍一下分代回收机制

JVM分代回收策略

JVM分代回收策略就是Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,永久代,不过永久代在JDK1.8永久移除了,被元空间取代了

新生代

新生代主要是用来存放新生的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。一般采用的 GC 回收算法是复制算法。 新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。当JVM无法为新建对象分配内存空间的时候(Eden区满的时候),JVM触发MinorGc。因此新生代空间占用越低,MinorGc越频繁。MinorGC 触发机制是Eden区满的时候,JVM会触发MinorGC。

详细过程

  • 绝大多数刚刚被创建的对象会存放在 Eden 区(如果新创建的对象占用内存很大则直接分配给老年代)
  • 当 Eden 区第一次满的时候,当Eden区内存不够的时候就会触发一次Minor GC进行一次垃圾回收。首先将 Eden 区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1 是空的。
  • 下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden 和 S0 区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0 变为空。
  • 再下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden 和 S1区中所有垃圾对象清除,并将存活对象复制到 S0,此时 S1 变为空。
  • 如此反复在 S0 和 S1 之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。

骚戴扩展:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁, 当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值)来设置。

老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

我们可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记-整理算法或标记-清除的回收算法。老年代的对象比较稳定,所以MajorGC不会频繁执行。MajorGC的耗时比较长,因为要先整体扫描再回收,MajorGC会产生内存碎片。为了减少内存损耗,一般需要合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM。

永久代

永久代(Permanent Generation)是Java虚拟机中的一块内存区域,用于存储类、方法、常量等信息。在Java 8及以前的版本中,永久代是堆内存的一部分,它有一个固定的大小,一旦被占满,就会导致OutOfMemoryError异常。

永久代中存储的信息包括:

- 类的元数据信息,如类名、访问修饰符、字段、方法等。
- 字符串常量池。
- 静态变量。

由于永久代的大小是固定的,而且存储的信息会随着应用程序的运行而不断增加,因此容易导致OutOfMemoryError异常。为了解决这个问题,Java 8及以后的版本中,永久代被移除,取而代之的是元空间(Metaspace)。

元空间是Java虚拟机中的一块内存区域,用于存储类、方法、常量等信息。与永久代不同的是,元空间的大小不再是固定的,它可以根据需要动态调整大小。此外,元空间还可以使用本地内存,从而减少堆内存的使用。

元空间中存储的信息与永久代类似,包括类的元数据信息、字符串常量池、静态变量等。但是,元空间中的类元数据信息不再存储在虚拟机的堆内存中,而是存储在本地内存中。这样可以避免堆内存的碎片问题,提高应用程序的性能。

骚戴理解:方法区是堆内存的一部分,其中包括永久代(Permanent Generation),用于存储类的元数据信息、静态变量、常量等数据。在Java 8及以前的版本中,永久代是方法区的一部分,它有一个固定的大小,一旦被占满,就会导致OutOfMemoryError异常。在Java 8及以后的版本中,方法区被移除,取而代之的是元空间(Metaspace)

CMS垃圾回收器

1、什么是CMS?

Concurrent Mark Sweep。看名字就知道,CMS是一款并发、使用标记-清除算法的gc。CMS是针对老年代进行回收的GC。

2、CMS有什么用?

CMS以获取最小停顿时间为目的。在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。

3、CMS垃圾清理的过程

总体来说CMS的执行过程可以分为以下几个阶段:

  1. 初始标记(STW initial mark)
  2. 并发标记(Concurrent marking)
  3. 并发预清理(Concurrent precleaning)
  4. 重新标记(STW remark)
  5. 并发清理(Concurrent sweeping)
  6. 并发重置(Concurrent reset)

初始标记 :在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”(STW)机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出 GC Roots 能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

并发标记 :在初始标记的基础上继续向下追溯标记(从 Gc Roots 的直接关联对象开始遍历整个对象图的过程),并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿,可以与垃圾收集线程一起并发运行。

并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。并发可中止的预清理阶段。这个阶段其实跟上一个阶段做的东西一样,也是为了减少下一个STW重新标记阶段的工作量。增加这一阶段是为了让我们可以控制这个阶段的结束时机,比如扫描多长时间(默认5秒)或者Eden区使用占比达到期望比例(默认50%)就结束本阶段。

重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"根对象"开始向下追溯,标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。(由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录)

并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

4.CMS缺点

  1. CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。
  2. 需要更多的CPU资源。从上面的图可以看到,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切 换是不靠谱的。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。当然,多核多CPU也是未来的趋势!
  3. CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。

总得来说,CMS回收器减少了回收的停顿时间,但是降低了堆空间的利用率。

5.啥时候用CMS

如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。

好文参考:详解CMS垃圾回收机制 - 简书

猜你喜欢

转载自blog.csdn.net/qq_50954361/article/details/131375126