JDK,JRE,JVM 详解
JDK,JRE,JVM三者之间的联系
- 1.JDK是整个Java的核心,包括了Java运行环境(JRE)、Java开发工具和Java基础的类库。
- 2.JRE包括了JVM和一些基础类库
- 3.JRE 中的类库是 JDK 类库的子集,主要包含 Java 运行时所需的类和接口。
关于JDK的介绍
JDK 是 Java 开发的核心工具包,提供了丰富的工具和环境,使开发人员能够高效地开发、调试和部署 Java 应用程序。无论是从编译、运行、调试,还是生成文档、打包部署等方面,JDK 都提供了完备的支持。
JDK包含的基本组件(开发工具)包括:
- java:Java 解释器,用于执行 Java 字节码文件。
- javap:Java 反汇编器,用于反汇编 Java 字节码文件,以查看其内容。
- javadoc:Java 文档生成工具,用于根据源代码生成 API 文档。
- javah 和 javah:用于生成 Java 类和本地方法之间的头文件以及将本地方法实现的源代码。
- jdb:Java 调试器,用于在开发过程中调试 Java 程序。
- jdeps:Java 依赖分析工具,用于分析类或 JAR 文件的依赖关系。
- jconsole 和 jvisualvm:Java 监视和管理控制台,用于监视 Java 虚拟机和应用程序的性能,并进行管理操作。
- javafxpackager:用于打包 JavaFX 应用程序的工具。
- keytool:用于管理 Java 密钥和证书的工具。
Java基础的类库:
- Java 核心类库:提供了基本的数据结构、集合类、I/O 操作、网络通信等功能。
- Java 集合框架:包括 ArrayList、LinkedList、HashMap 等常用的集合类和接口。
- Java 网络编程:提供了 Socket 编程、URL 处理、HTTP 客户端等网络相关的类和接口。
- Java 多线程:包括 Thread 类、Runnable 接口以及用于线程同步的锁、条件变量等工具类。
- Java 数据库连接(JDBC):用于连接和操作数据库的类和接口。
- Java XML 和 JSON 处理:提供了处理 XML 和 JSON 数据的类和接口。
- Java 加密和安全:包括 MessageDigest、Cipher、KeyStore 等用于加密和安全操作的类和接口。
- Java 图形用户界面(GUI):JavaFX 和 Swing 是 JDK 中主要的 GUI 框架。
关于JRE的介绍
JRE( Java Runtime Environment ),Java运行环境,主要包含两个部分:JVM和Java系统类库。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的Java程序,安装JRE即可。
JRE 中的类库是 JDK 类库的子集,主要包含 Java 运行时所需的类和接口。
JRE中的类库(JAVA的系统类库):
- Java 核心类库:提供了基本的数据结构、集合类、I/O 操作等。
- Java 网络编程:包括 Socket 编程、URL 处理等网络相关的类和接口。
- Java 多线程:包括 Thread 类、Runnable 接口等线程相关的类和接口。
- Java 数据库连接(JDBC):用于连接和操作数据库的类和接口。
关于JVM的介绍(重点)
JVM 的主要作用是实现 Java 语言的跨平台特性和自动内存管理特性。它允许开发人员在不同的操作系统和硬件平台上编写一次 Java 代码,并在任何安装了 Java 虚拟机的平台上运行。JVM 还提供了垃圾回收机制,使得开发人员不必手动管理内存,从而降低了程序出错的可能性。通过执行引擎和即时编译器,JVM 还能够将 Java 字节码翻译成高效的本地机器码,以提高程序的执行速度。总的来说,JVM 是 Java 语言的核心组件,为 Java 应用程序的运行提供了必要的环境和支持。
-
类加载器(Class Loader):负责将编译后的 Java 类文件加载到 JVM 中,并将其转换为运行时数据结构。类加载器根据需要动态地加载类,实现了 Java 的动态性和可扩展性。
-
运行时数据区域:包括堆(Heap)、方法区(Method Area)、程序计数器(Program Counter)、Java 栈(Java Stack)和本地方法栈(Native Method Stack)。这些区域在运行时存储了程序的状态和数据,支持 Java 程序的运行。
-
执行引擎(Execution Engine):负责执行 Java 字节码,将其翻译为特定平台的机器码,以实现跨平台的“一次编写,到处运行”特性。执行引擎通常包括解释器(Interpreter)和即时编译器(Just-In-Time Compiler,JIT Compiler)。
-
垃圾回收器(Garbage Collector):负责自动回收不再使用的内存,防止内存泄漏和程序崩溃。垃圾回收器周期性地检查和释放不再被引用的对象,以确保内存的有效利用。
-
即时编译器(Just-In-Time Compiler,JIT Compiler):将经常执行的字节码转换为本地机器码,以提高程序的执行速度。JIT 编译器根据程序的运行情况来优化代码,从而提高程序的性能。
类加载器
类加载器(Class Loader)在 Java 虚拟机中扮演着重要的角色,负责将 Java 字节码文件(.class 文件)加载到内存中,并生成相应的 Class 对象。类加载器是 Java 虚拟机的一部分,它通过动态加载类的机制实现了 Java 程序的灵活性和动态性。
下面是对类加载器的详细介绍:
类加载的过程
类加载器的工作涉及到三个阶段:加载(Loading)、连接(Linking)和初始化(Initialization)。
1.加载
- 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
- Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
- 类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。
- 类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口
2、连接过程
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。
- 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配.
- 解析:虚拟机常量池的符号引用替换为字节引用过程。
3、初始化
- 初始化阶段是执行类构造器clinit () 方法的过程。类构造器clinit()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个类的clinit() 方法在多线程环境中被正确加锁和同步
类加载器的层次结构
Java 虚拟机支持多层次的类加载器,形成了类加载器的层次结构。
1.根类加载器(Bootstrap Class Loader)
- 引导类加载器是 Java 虚拟机的一部分,负责加载 Java 平台核心库,如 rt.jar、resources.jar 等。
- 它是用本地代码实现的,不继承自 java.lang.ClassLoader,因此在 Java 中无法直接引用。(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)
- 由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
2.扩展类加载器(Extension Class Loader):
- 扩展类加载器是 Java 虚拟机的一部分,用于加载 Java 平台的扩展库,位于
sun.misc.Launcher$ExtClassLoader 中。 - 它主要加载 $JAVA_HOME/lib/ext 目录下的 JAR 文件或者由 java.ext.dirs
系统属性指定的目录中的类库。
3.系统类加载器/应用类加载器(System/Application Class Loader)
- 负责加载应用程序类(即用户自定义的类)。
- 系统类加载器是 Java 虚拟机的一部分,负责加载应用程序的类,位于
sun.misc.Launcher$AppClassLoader 中。 - 它通常加载用户类路径(classpath)上指定的类库,包括当前工作目录和 -classpath 或者 -cp 选项所指定的目录或者JAR
文件。
4.自定义类加载器(Custom Class Loader)
- 自定义类加载器是由 Java 程序员编写的用于加载特定类型的类或资源的类加载器。
- 自定义类加载器继承自 java.lang.ClassLoader 类,并通过重写 findClass() 方法或者
defineClass() 方法来实现自定义的类加载逻辑。 - 自定义类加载器可以用于加载非标准格式的类文件、实现类的热部署、加载加密或者混淆过的类文件等特定场景。
双亲委派模型(Parent Delegation Model)
类加载器采用了双亲委派模型,即在加载类时,每个类加载器都会先委托给其父类加载器尝试加载该类。
当一个类加载器收到加载类的请求时,它首先检查自己是否已经加载了该类,如果没有,则将加载请求委派给父类加载器。
这样的层次结构保证了类的唯一性和一致性,避免了类的重复加载,同时也保证了 Java 核心类库的安全性和稳定性。
-
当我们需要加载任何一个范围内的类时,首先找到这个范围对应的类加载器
-
但是当前这个类加载器不是马上开始查找
-
当前类加载器会将任务交给上一级类加载器
-
上一级类加载器继续上交任务,一直到最顶级的启动类加载器
-
启动类加载器开始在自己负责的范围内查找
-
如果能找到,则直接开始加载
-
如果找不到,则交给下一级的类加载器继续查找
-
一直到应用程序类加载器
-
如果应用程序类加载器同样找不到要加载的类,那么会抛出ClassNotFoundException异常
验证双亲委派机制
实验1
第一步:在与JDK无关的目录下创建Hello.java
public class Hello {
public static void main(String[] args){
System.out.println("Hello world");
}
}
第二步:编译Hello.java
第三步:将Hello.class文件移动到$JAVA_HOME/jre/classes目录下
第四步:修改Hello.java
public class Hello {
public static void main(String[] args){
System.out.println("goodbye");
}
}
第五步:编译Hello.java
第六步:将Hello.class文件移动到$JAVA_HOME/jre/lib/ext/classes目录下
第七步:修改Hello.java
public class Hello {
public static void main(String[] args){
System.out.println("Tom like jerry");
}
}
第八步:编译Hello.java
第九步:使用java命令运行Hello类,发现打印结果是:Hello world
说明Hello这个类是被启动类加载器找到的,找到以后就不查找其他位置了
第十步:删除$JAVA_HOME/jre/classes目录
第十一步:使用java命令运行Hello类,发现打印结果是:goodbye
说明Hello这个类是被扩展类加载器找到的,找到以后就不查找其他位置了
第十二步:删除$JAVA_HOME/jre/lib/ext/classes目录
第十三步:使用java命令运行Hello类,发现打印结果是:Tom like jerry
说明Hello这个类是被应用程序类加载器找到的
实验2
第一步:创建假的String类
package java.lang;
public class String {
public String() {
System.out.println("嘿嘿,其实我是假的!");
}
}
第二步:编写测试程序类
@Test
public void testLoadString() {
// 目标:测试不同范围内全类名相同的两个类JVM如何加装
// 1.创建String对象
java.lang.String testInstance = new java.lang.String();
// 2.获取String对象的类加载器
ClassLoader classLoader = testInstance.getClass().getClassLoader();
System.out.println(classLoader);
}
第三步:查看运行结果是null
假的String类并没有被创建对象,由于双亲委派机制,启动类加载器加载了真正的String类
双亲委派机制的好处
避免类的重复加载:父加载器加载了一个类,就不必让子加载器再去查找了。同时也保证了在整个 JVM 范围内全类名是类的唯一标识。
安全机制:避免恶意替换 JRE 定义的核心 API
运行时数据区域
JDK 1.8以前
Jdk1.8以后
- 程序计数器(Program Counter Register):
- 每个线程都有一个程序计数器,它是当前线程执行的字节码指令的地址计数器。
- 在Java多线程环境中,程序计数器用于线程切换后恢复到正确的执行位置,因此它是线程私有的。
- Java虚拟机栈(Java Virtual Machine Stacks):
- 每个线程在创建时都会分配一个Java虚拟机栈,用于存储局部变量、方法参数、返回值和部分方法执行过程中的临时数据。
- 每个方法在执行时会创建一个栈帧(Stack Frame),栈帧包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。
- 栈帧的大小在编译时就已确定,在运行时不会改变。
- 本地方法栈(Native Method Stack):
- 本地方法栈类似于Java虚拟机栈,但是它为Native方法(使用JNI调用的本地方法)服务。
- 它也是线程私有的,用于执行Native方法时分配内存。
- Java堆(Java Heap):
- Java堆是Java虚拟机管理的内存中最大的一块区域,用于存储对象实例和数组。
- Java堆是所有线程共享的,被所有线程访问,但是在大多数情况下是线程安全的。
- Java堆在JVM启动时创建,可以通过启动参数调整大小。
- 方法区(Method Area):
- 方法区用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 方法区也是所有线程共享的,被所有线程访问,但是在大多数情况下是线程安全的。
- 在HotSpot虚拟机中,方法区被称为"永久代"(Permanent Generation),用于存储永久性的数据,如类信息、方法信息等。但是在Java 8 中,永久代被"元空间"(Metaspace)所取代。
- 运行时常量池(Runtime Constant Pool):
- 运行时常量池是方法区的一部分,用于存储编译时生成的各种字面量和符号引用。
- 运行时常量池在类加载时被创建,并在运行时动态地扩展。
- 直接内存(Direct Memory):
- 直接内存不是JVM运行时数据区域的一部分,但是它可以被Java程序使用,并且对JVM的性能有影响。
- 直接内存是通过NIO类库中的ByteBuffer分配的,它允许Java程序直接访问操作系统的本机内存,而不需要经过Java堆。
- 直接内存通常用于需要频繁进行I/O操作的场景,比如网络通信、文件操作等。
垃圾回收器
我们思考下对象被创建到被回收的全部过程。
方法区的垃圾回收的内存:
(1)方法区(jdk 1.7)/ 元空间(jdk 1.8)
- JDK1.7的 方法区 在GC中一般称为 永久代(Permanent Generation)。
- JDK1.8的 元空间 存在于本地内存,GC也是即对元空间垃圾回收。
- 永久代或元空间的垃圾收集主要回收两部分内容:废弃常量和无用的类。此区域进行垃圾收集的“性价比”一般比较低
(2)下面是堆区的垃圾回收的内存:
垃圾回收的全部过程
-
对象创建:
- 通过关键字
new
创建一个新的对象。 - 调用对象的构造函数来初始化对象的状态。
- 通过关键字
-
对象引用:
-
将对象的引用存储在变量中,或者作为其他对象的字段。
-
对象的引用使得可以通过变量或字段来访问对象。
-
对象引用分为这几种情况:
//1.对象创建:当使用 new 关键字创建一个新的对象时,将分配一个新的内存空间来存储该对象,并返回对该对象的引用。 MyClass obj = new MyClass(); //2.对象作为方法参数传递:当将对象作为参数传递给方法时,将会创建该对象的引用副本。在方法内部,该引用指向同一个对象。 public void someMethod(MyClass obj) { // 方法内部可以使用 obj 引用对象 } //3.对象存储在集合中:当对象存储在集合(如数组、列表、映射等)中时,集合将持有对对象的引用。 List<MyClass> list = new ArrayList<>(); list.add(new MyClass()); //4.对象作为字段存储在其他对象中:当对象作为另一个对象的字段存储时,将在存储该对象的对象中持有对其的引用。 class AnotherClass { MyClass obj; }
-
-
失去引用:
-
如果不再需要对象,或者对象的引用被重新赋值为
null
,对象就会失去引用。 -
失去引用意味着对象不再可达,即没有任何活动线程可以通过引用链访问到这个对象。
-
失去引用分为这几种情况:
1.引用被赋值为 null:当对象的引用被显式赋值为 null 时,对象将失去被引用的状态。 MyClass obj = new MyClass(); obj = null; // 对象失去引用 2.方法结束:当方法执行结束时,方法内的局部变量将被销毁,其中包括对对象的引用。如果没有其他引用指向对象,对象将变为不可达状态。 public void someMethod() { MyClass obj = new MyClass(); // 方法执行结束,局部变量 obj 失去引用 } 3.集合移除对象:如果对象存储在集合中,并且该集合被清空或移除了对该对象的引用,对象将失去引用。 List<MyClass> list = new ArrayList<>(); list.add(new MyClass()); list.clear(); // 对象失去引用 4.对象被重新赋值:当对象的引用被重新赋值给其他对象时,原对象将失去被引用的状态。 MyClass obj1 = new MyClass(); MyClass obj2 = new MyClass(); obj1 = obj2; // obj1 对原
-
-
垃圾回收:
- 当对象失去引用后,垃圾回收器会在适当的时机(通常是在内存不足时)对其进行回收。
- 垃圾回收器会标记不再可达的对象,并释放它们占用的内存空间。
- 如果对象的类实现了
finalize()
方法,垃圾回收器在回收对象之前会调用该方法进行清理。
-
整体过程:
public class ObjectLifecycleExample { public static void main(String[] args) { // 步骤1:创建对象 MyObject obj = new MyObject(); // 步骤2:对象引用 SomeOtherObject.someField = obj; // 步骤3:失去引用 obj = null; // 步骤4:垃圾回收 System.gc(); }} // 示例对象类 class MyObject { // 构造函数 public MyObject() { System.out.println("对象被创建了"); } // finalize() 方法 @Override protected void finalize() throws Throwable { System.out.println("对象被垃圾回收了"); } } // 其他类 class SomeOtherObject { static MyObject someField; }
垃圾回收器会在以下情况下回收对象:
- 对象不再被引用:当对象不再被任何活动线程引用时,垃圾回收器会将其识别为垃圾,并将其回收。
- 对象被引用但无法通过任何活动线程访问:即使对象仍然存在引用,但如果这些引用之间形成了环,且这些环与根对象之间没有可达路径,垃圾回收器也会将这些对象识别为垃圾并回收。
- 对象的引用被显式置为 null:如果程序员将对象的引用设置为 null,那么该对象就变得不可达,垃圾回收器会在适当的时候将其回收。
- 内存不足:当系统内存不足时,垃圾回收器会尝试回收未使用的对象,以释放内存空间供其他对象使用。
- 系统空闲时:在系统空闲时,垃圾回收器会运行以回收未使用的对象,以提高内存利用率和系统性能。
垃圾回收
大体说完成功流程,不难发现垃圾回收其实就是在对象失去引用后对其进行回收,那就有几个问题不难说出。
1.怎么判断对象是否失去引用,或者说怎么去找到失去引用的对象(垃圾判断)。
2.怎么去回收或者说采用什么应的算法来回收(垃圾清除)。
垃圾判断
判断对象存活一般有两种方式:引用计数算法 和 可达性分析算法
标记阶段:引用计数算法
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
- 引用计数器有个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法
标记阶段:可达性分析算法
可达性分析特点:
- 相较于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数法中循环引用的问题,防止内存泄漏的发生
- 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫做追踪性垃圾收集
基本思路:
- 可达性分析是以跟对象集合(GC Roots)为起始点,按从上至下的方式搜索被跟对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中存活的对象都会被根对象集合直接或间接连接着,搜索所过的路径称为引用链(Reference Chain)
- 如果目标对象没有和任何引用链相连,则是不可达的,就以为对象已经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能被跟对象集合直接或间接连接的对象才是存活对象
问题来了,哪些对象可被称为GC Roots对象呢?或者说Java中,GC Roots包含哪几类对象呢?
- 虚拟机栈中引用的对象,比如:Java线程中,当前所有正在被调用的方法的引用类型参数、局部变量等
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象,比如:字符串常量池里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用。基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器。
注意:
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话,分析结果的准确性就无法保证
- 这点也是导致GC进行时必须“Stop The World”的一个重要原因。即使是号称几乎不会停顿的CMS垃圾回收器中,枚举根节点时也是必须要停顿的
垃圾清除算法
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收、释放掉垃圾对象所占用的内存,以便有足够的可用空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾回收算法是:
- 标记 - 清楚算法(Mark - Sweep)
- 复制算法(Copying)
- 标记 - 压缩算法(Mark - Compact)
清除阶段:标记-清除算法
标记-清除(Mark - Sweep)算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:
- 标记:从GC Roots开始遍历,标记所有被引用的对象。一般是在对象头中记录是否是可达对象
- 清除:对堆内存从头到尾遍历,如果发现某个对象的对象头中没有标记为可达对象,则将其回收
优点:
- 不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效
缺点:
- 标记和清除过程的效率都不算高
- 这种方法需要使用一个空闲列表(空闲列表记录哪些内存是没有被占用状态,空闲的)来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量
- 标记清除后会产生大量不连续的内存碎片
清除阶段:标记-整理算法
标记-整理分为“标记”和“整理”两个阶段:
- 标记:和标记清除算法一样,从GC Roots开始标记所有被引用的对象
- 整理:将所有的存活对象压缩到内存的一端,按顺序排放。之后清理外边界的空间(清理垃圾)
标记-整理算法的最终效果等同于标记-清除算法执行后,再进行一次内存碎片整理,因此也可以把它称为标记-清除-压缩算法
可以看到,标记的存活对象将会被整理,按照内存地址依次排序。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销
优点:
- 消除了标记-清除算法中,内存区域分散的缺点(内存碎片)
缺点:
- 移动对象的同时,如果对象被其他对象引用,还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。即STW
清除阶段:复制算法
核心思想:
将内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的对象,交换两个内存的角色,最后完成垃圾回收
对于这种算法来说,如果存活的对象过多的话则要执行较多的复制操作,效率会变低,因此它适合存活率较低的情况。事实上在年轻代中就是使用的复制算法。
优点:
- 没有标记和清除的过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题
缺点:
- 需要两倍的内存空间,比较浪费
- 如果存活对象较多,那么复制操作就比较多,效率相对会降低
对比三种清除算法
Mark-Sweep | Nark-Compact | Copying | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但有内存碎片) | 少(没有内存碎片) | 需要额外的一半内存开销 |
移动对象 | 否 | 是 | 是 |
从效率来说,复制算法是当之无愧的老大,但是却浪费了太多内存
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除算法多了一个整理的阶段
分代收集
-
新生代(Young Generation)的回收算法
新生代的垃圾回收又称为 Young GC(YGC)、Minor GC。指发生在新生代的垃圾收集动作,因为Java对象大多都具备 朝生夕灭 的特性,所以 Minor GC非常频繁,一般回收速度也比较快。
-
老年代(Old Generation)的回收算法
老年代垃圾回收又称为 Major GC。
指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
Major GC的速度一般会比 Minor GC慢10倍以上。 -
永久代(Permanent Generation)的回收算法 (JDK1.8以前)
- 用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。
- Partical GC:只进行一部分内存区域的GC
- Full GC:针对整个内存区域进行GC
- Minor GC:针对新生代内存的GC,执行频繁,速度较快
- Major GC:针对老年代的GC,没那么频繁。速度较慢,通常由Minor GC 触发
一个对象的一生:
(1)对象诞生于新生代的伊甸区。新产生的对象的内存就是新生代中的内存
(2)第一轮GC(Minor GC)扫描伊甸区之后,就会把大量的对象回收掉。少数没有被回收的对象,就会通过标记-复制算法进入到生存区
(3)少数进入生存区的对象,再次被GC(Minor GC)扫描(对这些对象进行可达性分析)。如果发现该对象已经不可达,也就被销毁了。没有被销毁的对象,再次通过标记-复制算法,把它拷贝到另一个生存区。
(4)对象在两个生存区中经过若干次拷贝,如果还没有被回收,那么就说明这些个对象存活时间比较久,就拷贝到老年代
(5)老年代的对象也是要经过GC(Major GC)扫描的。由于老年代的对象生存时间比较长。因此扫描周期要比新生代的周期要长
(6)当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
强软弱虚引用
在java中,除了基本数据类型的变量外,其他所有的变量都是引用类型,指向堆上各种不同的对象。
在jvm中,除了我们常用的强引用(我们平时无意之间用的大都是强引用)外,还有软引用、弱引用、虚引用,这四种引用类型的生命周期与jvm的垃圾回收过程息息相关。
所有引用类型,都是抽象类java.lang.ref.Reference的子类,这个类的主要方法为get()方法:
public T get() {
return this.referent;
}
除了虚引用(因为get永远返回null),如果对象还没有被销毁,都可以通过get方法获取原有对象。
强引用
最传统的引用的定义,是指在程序代码中最普遍存在的引用赋值,即类似“Object object = new Object()”这种引用关系。强引用(Strong references)就是直接new一个普通对象,表示一种比较强的引用关系,只要还有强引用对象指向一个对象,那么表示这个对象还活着(GC Roots可达),垃圾收集器宁可抛出OOM异常,也不会回收这个对象。
软引用
软引用用于关联一些可有可无的对象,例如缓存。当系统内存充足时,这些对象不会被回收;当系统内存不足,将要发生内存溢出之前,就会回收这些对象(即使这些对象GC Roots可达),如果回收完这些对象后内存还是不足,就会抛出OOM异常。
// vm args: -Xmx36m -XX:+PrintGCDetails
public class SoftReferenceDemo {
public static void main(String[] args) throws InterruptedException {
SoftReference<User> softReference = new SoftReference<>(new User()); // 软引用
System.out.println(softReference.get());
System.gc();
TimeUnit.SECONDS.sleep(3); // wait gc thread run
System.out.println(softReference.get()); // User对象不会被回收
byte[] bytes = new byte[1024 * 1024 * 10]; // 分配一个大对象使得堆空间不足,软引用对象会在OOM之前先被回收
System.out.println(softReference.get());
}
}
在上面的例子中,第一次发生gc时,User对象不会被回收,第二次发生gc时由于堆空间不足,会先回收软引用的对象,回收完了还是空间不足,最后抛出OOM异常。
弱引用
被弱引用关联的对象只能生存到一下次垃圾回收之前。当垃圾收集器工作时,无论内存空间是否充足,都会回收掉被弱引用关联的对象。ThreadLocal中就使用了WeakReference来避免内存泄漏。
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
WeakReference<User> weakReference = new WeakReference<>(new User());
System.out.println(weakReference.get());
System.gc();
TimeUnit.SECONDS.sleep(3); // wait gc thread run
System.out.println(weakReference.get()); // null
}
}
上面的例子只要发生gc,User对象就会被垃圾收集器回收。
虚引用
- 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个虚引用对象时,在回收该对象后,就将这个虚引用加入到对联的引用队列中
- 虚引用 - 形态虚设,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收
- 也无法通过虚引用来获取被引用的对象,当试图通过虚引用的get()方法取得对象时,总是null
- 虚引用主要用来跟踪对象被垃圾回收的活动
垃圾收集器
垃圾回收器 | 工作区域 | 回收算法 | 工作线程 | 用户线程并行 | 描述 |
---|---|---|---|---|---|
Serial | 新生代 | 复制算法 | 单线程 | 否 | Client模式下默认新生代收集器。简单高效 |
ParNew | 新生代 | 复制算法 | 多线程 | 否 | Serial的多线程版本,Server模式下首选, 可搭配CMS的新生代收集器 |
Parallel Scavenge | 新生代 | 复制算法 | 多线程 | 否 | 目标是达到可控制的吞吐量 |
Serial Old | 老年代 | 标记-整理 | 单线程 | 否 | Serial老年代版本,给Client模式下的虚拟机使用 |
Parallel Old | 老年代 | 标记-整理 | 多线程 | 否 | Parallel Scavenge老年代版本,吞吐量优先 |
CMS | 老年代 | 标记-整理 | 多线程 | 是 | 追求最短回收停顿时间 |
G1 | 老年代+新生代 | 标记-整理+复制算法 | 多线程 | 是 | JDK1.9默认垃圾收集器 |
上面讲述的都说垃圾的识别以及垃圾的收集算法,那这些算法都需要由垃圾收集器去执行。
- 新生代可配置的回收器:Serial、ParNew、Parallel Scavenge
- 老年代配置的回收器:CMS、Serial Old、Parallel Old
新生代收集器
Serial 垃圾回收器
Serial收集器是最基本的、发展历史最悠久的收集器。俗称为:串行回收器
,采用复制算法
进行垃圾回收
特点
串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程。
对于并行能力较弱的单CPU计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。
它存在Stop The World问题,及垃圾回收时,要停止程序的运行。
使用-XX:+UseSerialGC
参数可以设置新生代使用这个串行回收器
ParNew 垃圾回收器
ParNew其实就是Serial的多线程
版本,除了使用多线程之外,其余参数和Serial一模一样。俗称:并行垃圾回收器
,采用复制算法
进行垃圾回收
ParNew默认开启的线程数与CPU数量相同,在CPU核数很多的机器上,可以通过参数-XX:ParallelGCThreads来设置线程数。
它是目前新生代首选的垃圾回收器,因为除了ParNew之外,它是唯一一个能与老年代CMS配合工作的。
它同样存在Stop The World问题
使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器
Parallel Scavenge 收集器(这是 JDK1.8 默认收集器)
吞吐量=代码运行时间/(代码运行时间+垃圾收集时间)
-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,可用把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间可以将MaxGCPauseMillis设置的很小,但是会导致GC频繁,从而增加了GC的总时间,降低了吞吐量。所以需要根据实际情况设置该值。
-Xx:GCTimeRatio:设置吞吐量大小,它是一个0到100之间的整数,默认情况下他的取值是99,那么系统将花费不超过1/(1+n)的时间用于垃圾回收,也就是1/(1+99)=1%的时间。
另外还可以指定-XX:+UseAdaptiveSizePolicy打开自适应模式,在这种模式下,新生代的大小、eden、from/to的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
使用-XX:+UseParallelGC参数可以设置新生代使用这个并行回收器
老年代收集器
SerialOld 垃圾回收器
SerialOld是Serial回收器的老年代回收器版本,它同样是一个单线程回收器。
用途
- 一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,
- 另一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。
ParallelOldGC 回收器
老年代ParallelOldGC回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关注吞吐量的回收器,他使用了标记压缩算法进行实现。
-
-XX:+UseParallelOldGc进行设置老年代使用该回收器
-
-XX:+ParallelGCThreads也可以设置垃圾收集时的线程数量。
CMS 回收器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置:-XX:CMSInitiatingoccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达到68%的时候,会执行CMS回收。
如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器;SerialOldGC进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。
这个过程GC的停顿时间可能较长,所以-XX:CMSInitiatingoccupancyFraction的设置要根据实际的情况。
之前我们在学习算法的时候说过,标记清除法有个缺点就是存在内存碎片的问题,那么CMS有个参数设置-XX:+UseCMSCompactAtFullCollecion可以使CMS回收完成之后进行一次碎片整理。
主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
G1收集器
G1从jdk7开始,jdk9被设为默认垃圾收集器;目标就是彻底替换掉CMS 既可以回收新生代,也可以回收老年代
- E表示伊甸区
- S表示生存区
- T表示老年代
- H表示存放大对象的区域
G1 被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器的运作大致分为以下几个步骤:
以region为单位进行回收,回收粒度更精细 ,针对新生区的region同样适用复制算法, 针对老年代的回收类似于CMS。
- 初始标记【STW】:只去找和GRoot直接相连的对象
- 并发标记:和应用程序并发执行,进行可达性分析,遍历所有对象。如果发现某个老年代region中已经没有存活对象,就直接回收
- 最终标记:修正第二步产生的误差
- 筛选回收:挑选出对象存活率低的region进行回收
垃圾回收的时机
垃圾回收分为两种,Full GC 和 Scavenge GC。
Full GC 发生在整个堆内存中,而 Scavenge GC 仅仅发生在年轻代的 Eden 区,所以我们应该尽可能地减少 Full GC 的次数,当然,对于 JVM 的调优,很多情况下也是在想办法对 Full GC 进行调优。
因为 GC 是可能会对应用程序造成影响的,所以触发 GC 也是有一定的条件的,例如:
- 当应用程序空闲时,GC 有可能会被调用,因为 GC 运行线程的优先级是相对较低的,所以当线程忙的时候,它是不会运行的,当然,内存不足的情况除外;
堆内存不足的时候,GC 会被调用。例如创建对象的时候,若此时内存不足,则会触发 GC 用来给这个对象分配合适的内存,当进行完一次 GC 之后内存还是不足,则会继续进行第二次 GC,若第二次 GC 之后内存还是不足,则一般会提示 “out of memory”异常; - System.gc() 方法会显示触发 Full GC,但是它只是对 JVM 的一个 GC 请求,至于何时触发,还是由 JVM 自行判断的。
GC 的调用开销是比较大的,所以我们需要有针对性地进行调优,一般有如下方案:
- 不要显式调用 System.gc()。此函数虽然是建议 JVM 进行 GC,但很多情况下它会触发 GC,从而增加 GC 的频率;
- 尽量减少临时对象的使用。在方法结束后,临时对象便成为了垃圾,所以减少临时变量的使用就相当于减少了垃圾的产生,从而减少了GC的次数;
- 对象不用时最好显式置为 Null。一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null 有利于 GC 收集器对垃圾的判定;
- 尽量使用 StringBuilder 来代替 String 的字符串累加。因为 String 的底层是 final 类型的数组,所以 String 的增加其实是建了一个新的 String,从而产生了过多的垃圾;
- 允许的情况下尽量使用基本类型(如 int)来替代 Integer 对象。因为基本类型变量比相应的对象占用的内存资源会少得多;
- 合理使用静态对象变量。因为静态变量属于全局变量,不会被 GC 回收;