文章目录
前言
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
JVM常见的几个问题
- 请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?知道吗?
- 谈谈JVM中,类加载器你的认识?
掌握知识点
- JVM的位置
- JVM的体系结构
- 类加载器
- 双亲委派机制
- 沙箱安全机制
- Native
- PC寄存器
- 方法区
- 栈
- 三种JVM
- 堆
- 新生区、老年区
- 永久区
- 堆内存调优
- GC
1.常用算法 - JMM
JVM学习资料网站
- Oracle 官网Java文档索引
https://docs.oracle.com/en/java/javase/index.html - openjdk,官网
http://openjdk.java.net - JVM规范
https://docs.oracle.com/javase/specs/index.html - DZone上会有IT前沿的新闻和文章,会有AI、大数据、云、数据库、DevOps、IoT、Java还有开源项目,关于Java新特性的介绍,新特性的使用都会在上面
https://dzone.com/ - Java World是一个纯Java学习网站,它里面包括很多Java文章,它不同于DZone的领域那么多,Java World只专注于Java,学习Java新特性不可或缺的网站
JVM位置
JVM相当于一个软件,建立在操作系统(也相当于一种软件)之上
即:操作系统 ——> JVM ——>字节码 (顺序由低层到高层)
因此,JVM使得java跨平台
javap命令
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath
-cp
-bootclasspath
-c 对代码进行反汇编
javap -c Math.class > math.txt
对Math的字节码文件进行反编译并存储在math.txt文件中
Class文件结构
-
Java 虚拟机是不与包括Java语言在内的任何程序语言绑定,它只与Class文件(字节码文件)这种特定的二进制文件格式所关联。
-
除Java外,如Kotlin 、clojure、Groovy、JRuby、JPython、Scala都可以编译出 Class文件。
-
虚拟机丝毫不关心Class的来源是什么语言
-
Class 文件中包含Java虚拟机指令集、符号表以及若干其他辅助信息。
JVM规范: https://docs.oracle.com/javase/specs/index.html
class文件数据结构
class文件包括:
- 指令集
- 符号表
- 其他信息
与类的对应关系:
- 一个class文件对应唯一一个类或者接口
- 一个类或者接口不一定必须定义在class文件,可能是动态代理,也就是类加载器直接生成处理出来的
class的格式:
每个class文件都是由字节流组成,每个字节含有8个二进制位。
查看字节码文件,使用idea的插件BinEd可查看
class的数据类型
- 无符号:u1、u2、u4
- 表:结尾是_info
- 数组:[ ]
//如下为calss文件的数据结构,如下格式,可通过反汇编译得出javap -c Math.class > math.txt
ClassFile {
u4 magic; //魔数值
u2 minor_version; //最低版本号(次版本号)
u2 major_version; //最高版本号(主版本号)
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
魔数
文件内容起始几个字节使用固定的内容来描述文件类型,因此称这几个字节的内容称为魔数
作用:区分文件类型
区分文件类型的方法:
- 扩展名:文件属性的角度出发,容易修改
- 魔数:文件内容的角度出发,不容易修改
字节码文件的魔术由4个字节组成(u4)
CA FE BA BE(咖啡 宝贝 便于记忆)
可通过这四个四个字节判断该文件是否是真的class文件
魔数值的利用:
文件大致分为两个类型
- 文本文件(不存在魔数值)
- 二进制文件(存在魔数值)
利用魔数可判断文件的真实类型
利用魔数可以判断上传文件的真实类型(扩展名判断存在一定的失误)
版本号
主版本和次版本规则:
- 主版本(major_version):起始值是45,每个JDK大版本发布主版本号向上加一
- 次版本(minor_version):起始值0(jdk1.6后使用较少)
jdk对应的字节码数值
J2SE 11 = 55
J2SE 8 = 52 (0x34 hex)
J2SE 7 = 51 (0x33 hex)
J2SE 6.0 = 50 (0x32 hex)
J2SE 5.0 = 49 (0x31 hex)
JDK 1.4 = 48 (0x30 hex)
JDK 1.3 = 47 (0x2F hex)
JDK 1.2 = 46 (0x2E hex)
JDK 1.1 = 45 (0x2D hex)
JVM高版本可以执行低版本的class,反之异常
JDK、JRE、JVM的关系
官网对应的图解:https://doc.oracle.com/javase/8/docs/
JDK :java开发工具包,含有JRE和各种各样的开发工具,如java、javac、javadoc、jar、javap等
JRE:java运行环境,含有JVM和各种各样的类库和接口(API),如Math、JDBC、Swing
JVM:java虚拟机,包含客户端(Client)和服务端(Server)
JDK8 的Compact Profiles
JDK8新特性(JEP161)
- javaSE API 太多了,能不能按需分配,Compact Profiles的设计目的就是为了给jre减肥
- Compact Profiles设计目的也是为了给JDK9模块化打下基础的,因为JDK9是没有JRE目录的,直接使用jlink按需生成JRE
- 官网:http://openjdk.java.net/jeps/161
紧凑的好处(Compact紧凑的含义)
- 更小的java环境需要更少的计算资源
- 一个较小的运行时环境可以更好的优化性能和启动时间
- 消除未使用的代码从安全的角度是好的
- 这些打包的应用程序可以下载速度更快
JVM的体系结构
JVM主要由三个子系统构成:
- 类加载器子系统
- 运行时数据区(内存结构)
- 字节码执行引擎
流程:
开发Java程序时,首先编写.java文件,然后将.java文件编译成字节码文件
jvm中:
通过类装 载子系统将字节码文件的内容装载到JVM的运行时数据区(运行时数据区:方法区、堆、栈、本地方法栈、程序计数器) ,在装载class文件的内容时,会将class文件的内容拆分为几个部分,分别加载到JVM运行时数据区的几个部分。
在JVM中,程序的执行是通过执行引擎进行的,执行引擎会调用本地方法的接口来执行本地方法库,进而完成整个程序逻辑的执行。
我们常说的垃圾收集器是包括在执行引擎的,在程序运行过程中,执行引擎会开启垃圾收集器,并在后台运行,垃圾收集器会不断监控程序运行过程中产生的内存垃圾信息,并根据相应的策略对垃圾信息进行清理。
注:栈、本地方法栈、程序计数器是每个线程运行时独占的,而方法区和堆是所有线程共享的,因此,栈、本地方法栈、程序计数器不会设计线程安全问题,而方法区和堆会设计线程安全问题。
如下流程:
.java源文件 ——> class文件(字节码文件)——> 类装载器(Class Loader)——> 运行时数据区(Runtime Data Area)<——> 执行引擎(Execution Engine)<——>本地方法接口(Native Interface)<——> 运行时数据区(Runtime Data Area)
本地方法接口(Native Interface)<—— 本地方法库 C/C++
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1PAaHYp-1646888759104)(C:/Users/wp990/AppData/Roaming/Typora/typora-user-images/image-20210906104517716.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWcypwGS-1646888759108)(C:/Users/wp990/AppData/Roaming/Typora/typora-user-images/image-20210906104539147.png)]
类加载器生命周期
-
加载阶段
- 引导类加载器(BootStrapClassLoader)︰ 负责加载%JAVA_HOME%/jre/lib目录下的所有jar包,或者是-Xbootclasspath参数指定的路径;
- 扩展类加载器(ExtClassLoader) ︰ 负责加载%JAVA_HOME%/jre/liblext目录下的所有jar包,或者是java.ext.dirs参数指定的路径;
- 应用类加载器(AppClassLoader) ︰ 负责加载用户类路径上所指定的类库。
- 自定义加载器: 加载非classpath下的类,加载网络传出过来的类且需要加密,实现热部署
-
链接阶段
- 验证 : 主要的作用就是校验字节码的正确性,是否符合JVM规范。
- 准备 : 为类的静态变量分配相应的内存,并赋予默认值。
- 解析 : 将程序中的符号引用替换为直接引用,这里的符号引用包括︰静态方法等。此阶段就是将一些静态方法等符号引用替换成指向数据所在内存地址的指针,这些指针就是直接引用。如果是在类加载过程中完成的符号引用到直接引用的替换,这个替换的过程就叫作静态链接过程。如果是在运行期间完成的符号引用到直接引用的替换,这个替换的过程就叫作动态链接过程。
-
初始化阶段:对类的静态变量进行初始化,为其赋予程序中指定的值,并执行静态代码块中的代码。
-
使用
-
卸载
注意:在准备阶段和初始化阶段都会为类的静态变量赋值,不同之处就是在准备阶段为类的静态变量赋予的是默认值,而在初始化阶段为类的静态变量赋予的是真正要赋予的值。
运行时数据区:(内存模型)
方法区(Method Area)
- 存储类信息、常量、静态变量、JIT(即时编译信息)。
- 线程共享,存在GC
- 类似于堆内存,因此也称为非堆
方法区也叫元空间,主要包含:运行时常量池、类型信息、字段信息、类加载器的引用、对应的Class实例的引用信息,方法区中的信息能够被多个方法共享。比如:在程序中声明的常量、静态变量和有关类信息等引用,都会存放在方法区,而这些引用所指的具体对象一般都会在堆中开辟单独的空间进行存储,也可能会在直接内存中进行存储。
常量(方法区) ----》 常量对象(堆或直接内存)
静态变量(方法区) ----》 静态变量对象(堆或直接内存)
类信息引用(方法区) ----》 类对象(堆或直接内存)
堆(heap)
- 存储类实例
- 内存最大
- 线程共享,存在GC,主要的GC回收
- 一个JVM实例只有一个堆内存
堆中主要存储的是实际创建的对象,也就是会存储通过new关键字创建的对象,堆中的对象能够被多个线程共享。堆中的数据不需要事先明确存期,可以动态的分配内存,不再使用的数据和对象由JVM中的GC机制自动回收,对JVM的性能调优一般就是对堆内存的调优。
问题引入:从JVM角度怎么理解如下代码
Student stu = new Student();
理解:创建出来的Student对象存放与JVM中的堆区域的Eden区中,而Student的引用存放于栈中。
扩展:JVM的堆内存分为年轻代和老年代,年轻代分为Eden区、两个Survivor区(S1,S2),他们之间的所占空间的默认比例是:Eden:Survivor1:Survivor2 = 8:1:1。年轻代和老年代的空间占整个堆空间的1:2。
Java栈(Java Stack,也叫虚拟栈)
- 存储当前线程运行方法的主要数据、指令、返回地址,即局部变量
- 线程私有
- 无GC
一个方法对应一块栈帧内存空区域,存储着该方法的局部变量。根据栈的数据结构先进后出(FILO),主线程(main)最先压入栈,最后出栈,也就对应着java的执行顺序。
栈一般又叫作线程栈或者虚拟机栈,一般存储的是局部变量,在Java中,每个线程都会有一个单独的栈区,每个栈中的元素都是私有的,不会被其它的栈所访问。栈中的数据大小和生存期都是确定的,存取速度比较快。
在Java中,所有的基本数据类型和引用变量(对象引用)都是栈中的,一般情况下,线程退出或者方法退出时,栈中的数据会被自动清除。
程序在执行过程中,会在栈中为不同的方法创建不同的栈帧,在栈帧中又包含:局部变量表、操作数栈,动态链接和方法出口。
本地方法栈(Native Method Stack)
- 同虚拟机栈一致,不同点在于存储的是本地方法的数据
本地方法栈相对来说比较简单,就是保存native方法进入区域的地址
例如,在java中创建线程,调用Thread对象的start()方法时,会通过本地方法start()调用操作系统创建线程的方法,此时,本地方法栈就会保存start()方法进入区域地址。
程序计数器(Program Counter Register)
程序计数器也叫PC计数器,只要存储的是下一条将要执行的命令的地址
在多线程开发中,cpu会频繁切换线程,此时的程序计数器就是记录线程运行到哪的行号
(注:产生垃圾来源只有方法区和堆,因此常说的JVM调优即调优的是方法区和堆,而堆占比非常高)
执行引擎(字节码执行引擎)
- 解释器
- 即时编译器
- 中间代码生成器——>代码优化器——>目标代码生成器
- 分析器
- 垃圾回收器
本地方法接口
本地方法接口简称JNI(Java Native Interface)
简单来讲,一个Native Method就是一个java调用非java代码的接口,一个Native Method 是这样一个java方法:该方法的底层实现由非Java语言实现,比如C。这个特征并非java特有,很多其他的编程语言都有这一机制,比如在C++ 中,你可以用extern “C” 告知C++ 编译器去调用一个C的函数。
在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序。
标识符native可以与其他所有的java标识符连用,但是abstract除外。
/**
* 本地方法
*/
public class IHaveNatives {
//abstract 没有方法体
public abstract void abstractMethod(int x);
//native 和 abstract不能共存,native是有方法体的,由C语言来实现
public native void Native1(int x);
native static public long Native2();
native synchronized private float Native3(Object o);
native void Native4(int[] array) throws Exception;
}
java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
- 与java环境外交互:
有时java应用需要与java外面的环境交互,这是本地方法存在的主要原因。 你可以想想java需要与一些底层系统,如擦偶偶系统或某些硬件交换信息时的情况。本地方法正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐细节。 - 与操作系统交互(比如线程最后要回归于操作系统线程)
JVM支持着java语言本身和运行库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至jvm的一些部分就是用C写的。还有,如果我们要使用一些java语言本身没有提供封装的操作系统特性时,我们也需要使用本地方法。 - Sun’s Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread的setPriority()方法是用Java实现的,但是它实现调用的事该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setPriority()API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。
目前该方法的是用越来越少了,除非是与硬件有关的应用,比如通过java程序驱动打印机或者java系统管理生产设备,在企业级应用已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以是用Web Service等等,不多做介绍。
本地方法库
使用native关键字进行修饰的,就是本地方法。
需要native去修饰的就是本地方法。JAVA有时候内部工作不够,需要外部库来协调你,也需要和c进行融合。JAVA和操作系统之类的进行交互时候,也需要和这些放在一起进行交互。本地方法库就是一些nativ修饰的方法的集合
Native
一、认识 native 即 JNI,Java Native Interface
凡是一种语言,都希望是纯。比如解决某一个方案都喜欢就单单这个语言来写即可。Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。
二、用 Java 调用 C 的“Hello,JNI”
我们需要按照下班方便的步骤进行:
1、创建一个Java类,里面包含着一个 native 的方法和加载库的方法 loadLibrary。HelloNative.java 代码如下:
public class HelloNative
{
static
{
System.loadLibrary("HelloNative");
}
public static native void sayHello();
@SuppressWarnings("static-access")
public static void main(String[] args)
{
new HelloNative().sayHello();
}
}
首先让大家注意的是native方法,那个加载库的到后面也起作用。native 关键字告诉编译器(其实是JVM)调用的是该方法在外部定义,这里指的是C。如果大家直接运行这个代码, JVM会告之:“A Java Exception has occurred.”控制台输出如下:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloNative in java.library.path
at java.lang.ClassLoader.loadLibrary(Unknown Source)
at java.lang.Runtime.loadLibrary0(Unknown Source)
at java.lang.System.loadLibrary(Unknown Source)
at HelloNative.<clinit>(HelloNative.java:5)
这是程序使用它的时候,虚拟机说不知道如何找到sayHello。下面既可以手动写,自然泥瓦匠是用
运行javah,得到包含该方法的C声明头文件.h
将HelloNative.java ,简单地 javac javah,如图
就得到了下面的 HelloNative.h文件 :
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloNative */
#ifndef _Included_HelloNative
#define _Included_HelloNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloNative
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloNative_sayHello
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
jni.h 这个文件,在/%JAVA_HOME%include
根据头文件,写C实现本地方法。
这里我们简单地实现这个sayHello方法如下:
#include "HelloNative.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_HelloNative_sayHello
{
printf("Hello,JNI");
}
生成dll共享库,然后Java程序load库,调用即可。
在Windows上,MinGW GCC 运行如下
gcc -m64 -Wl,--add-stdcall-alias -I"C:\Program Files\Java\jdk1.7.0_71\include" -I"C:\Program Files\Java\jdk1.7.0_71\include\include\win32" -shared -o HelloNative.dll HelloNative.c
-m64表示生成dll库是64位的。然后运行 HelloNative:
java HelloNative
终于成功地可以看到控制台打印如下:
Hello,JNI
JNI 调用 C 流程图
四、其他介绍
native是与C++联合开发的时候用的!java自己开发不用的!
使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。
这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。
native 是用做java 和其他语言(如c++)进行协作时用的,也就是native 后的函数的实现不是用java写的
native的意思就是通知操作系统,这个函数你必须给我实现,因为我要使用。所以native关键字的函数都是操作系统实现的,java只能调用。java是跨平台的语言,既然是跨了平台,所付出的代价就是牺牲一些对底层的控制,而java要实现对底层的控制,就要一些其他语言的帮助,这个就是native的作用了
Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此 Java使用native方法来扩展Java程序的功能。
可以将native方法比作Java程序同C程序的接口,其实现步骤:
1、在Java中声明native()方法,然后编译;
2、用javah产生一个.h文件;
3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);
4、将第三步的.cpp文件编译成动态链接库文件;
5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。
JAVA本地方法适用的情况
1.为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API访问
2.为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用JAVA编写的
3.为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。
双亲委派机制
抛出问题:
自己创建一个java.lang包,并在包下创建了一个自定义的String类。运行本类将会出现什么?
package java.lang;
public class Strinng{
public static void main(String[] args){
System.out.print("自定义String类");
}
}
在jdk自带的类库中也存在java.lang.String类,运行自己创建的String类将会输出如下错误:
错误:在类java.lang.String中找不到 main 方法,请将 main方法定义为:
public static void main(String[] args)
否则JavaFX应用程序类必须扩展javafx.application.Application
因此,JVM使用双亲委派机制,防止自己写的类覆盖jdk类库自带的类
作用:
- JVM提高代码安全性
- 防止JVM内存中出现多份相同的字节码
另外,使用双亲委派机制,也能防止JVM内存中出现多份相同的字节码。
例如,两个类A和B,都需要加载System类。如果JVM没有提供双亲委派机制,那么A和B两个类就会分别加载一份System的字节码,这样JVM内存中就会出现这份System字节码。相反,JVM提供了双亲委派机制的话,在加载System类的过程中,会递归的向父加载器查找并加载,整个过程会优先选用BootStrapClassLoader加载器,也就是我们通常说的引导类加载器。如果找不到就逐级向下使用子加载器进行加载。而System类可以在BootStrapClassLoader中进行加载,如果System类已经通过A类的引用加载过,此时B类也要加载System类,也会从BootStrapClassLoader开始加载System类,此时,BootStrapClassLoader发现已经加载过System类了,就会直接返回内存中的system,不再重新加载。
这样,在JVM内存中,就只会存在一份System类的字节码。
即向上委派(查找),向下加载!
自定义类加载器 —(向上委托) —》应用类加载器—(向上委托) —》扩展类加载器—(向上委托) —》引导类加载器
引导类加载器—(加载失败,向下加载) —》扩展类加载器—(加载失败,向下加载) —》应用类加载器—(加载失败,向下加载) —》自定义类加载器
当JVM加载某个类的时候,不会直接使用当前类的加载器加载该类,会先委托父加载器寻找要加载的目标类,找不到再委托上层的父加载器进行加载,直到引导类加载器同样找不到要加载的目标类,就会在自己的类加载路径中查找并加载目标类。
简单来说:双亲委派机制就是:∶先使用父加载器加载,如果父加载器找不到要加载的目标类,就使用子加载器自己加载。
GC回收
意义:回收没有指定的垃圾对象
垃圾对象
判断对象是否是可回收(是否是垃圾对象)的算法:可达性算法
我们都知道Java虚拟机在进行垃圾回收操作的时候,会先进行垃圾判定,会使用引用计数法和可达性算法来进行对象是否回收判断;
可达性算法的基本思路是通过**”GC Roots“**的对象作为起始点,从这些点开始往下搜索,搜索所走过的路径为引用链,当一个对象到“GC Roots”没有任何引用链相连,证明该对象是不可达的,即不可用,是可回收对象;
在Java中可以做GC Roots的对象包含以下几种:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象;
2、方法区中的静态变量所引用的对象;
3、方法区中常量引用的对象;
4、本地方法栈中JNI(即Native方法)引用的对象;
但是可达性算法不可达的对象就一定会死亡,会被回收吗?
即使在可达性算法中不可达的对象也不一定是非死不可的,这时候它们暂时处于“缓刑”阶段,要真正宣告它的死亡还需要经历两次的标记阶段。
第一次标记:
在对象可达性算法不可达时,进行第一次标记,并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法或者该方法被虚拟机调用过,虚拟机将这两种情况视为“没有必要去执行”。
如果该对象被判定为有必要执行finalize()方法,那么这个对象会被放置到一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalize线程去执行它。这里所谓的执行就是去触发该方法,但是并不会承诺等待它执行结束,这样做的原因是,如果对象在finalize()方法中执行缓慢,或者发生死循环,将会导致整个队列中的对象处于等待之中。
第二次标记:
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中拯救自己——只要重新与引用链上的一个对象重新建立关联即可,譬如将自己(this关键字)赋值给某个类变量或者成员变量,那么在第二次标记的时候就会被移除“即将回收”的集合;如果对象这时候还没有逃脱,那么就会被真的回收了!
示例代码:
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes, i am still alive!");
}
@Override
public void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次拯救自己
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低所以暂停0.5秒等待
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no, i am dead!");
}
//对象第二次拯救自己,但是自救失败了
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低所以暂停0.5秒等待
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no, i am dead!");
}
}
}
//执行结果:
finalize method executed!
yes, i am still alive!
no, i am dead!
注意:第二次自救失败是因为任何一个对象的finalize()方法只能执行一次,如果第二次回收,就不会执行finalize方法了!
引用计数法
所谓的引用计数法就是给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1;任何时刻计数器的值为0的对象就是不可能再被使用的。
这个引用计数法时没有被Java所使用的,但是python有使用到它。而且最原始的引用计数法没有用到GC Roots。
2、优点
-
可即时回收垃圾:在 该方法中,每个对象始终知道自己是否有被引用,当被引用的数值为0时,对象马上可以把自己当作空闲空间链接到空闲链表。
-
最大暂停时间短。
-
没有必要沿着指针查找
3、缺点
-
计数器值的增减处理 非常繁重
-
计算器需要占用很多位。
-
实现繁琐。
-
循环引用无法回收。
保守式GC与准确式GC
保守式GC
所谓保守式GC就是“不能识别指针和非指针的GC”。
对于寄存器、调用栈、全局变量空间来说,都是不明确的根。例如调用栈中,装着函数内的局部变量和参数值。而局部变量,如C语言中的int、double这样就是非指针,但是也会有像void*这样的指针。
那么保守式GC会怎么检查不明确的根呢?
1、是不是被正确对齐的值?(在32位CPU的情况下,为4的倍数)
2、是不是指着堆内?
3、是不是指向对象的开头?
当然,这些只是基本的检查项目。上面的检查方法会将一些非指针识别成指针。例如一个数值和一个地址,它们两个值相等,这个时候,那个值也可以被识别成指针。
保守式GC的优点是语言处理程序不依赖与GC。缺点为识别指针和非指针需要付出成本、错误识别指针会压迫堆、能够使用的GC算法有限。例如GC复制算法就不能使用,因为其可能会将非指针重写。
准确式GC
准确式GC能够正确识别指针和非指针的GC。正确的根的创建方法是依赖于语言处理程序的实现的。我们可以通过打标签、不把寄存器和栈等当作根的方法来实现。
其优点就是完全能够识别指针,能够使用复制算法等需要移动对象的算法。但是在创建准确式GC时,语言处理程序必须对GC进行一些支援,而且创建正确的根就必须付出一定的代价。
其实我们垃圾回收机的实现都不是仅仅用哪一种回收算法,都是将几个结合使用,特别是分代算法,后面我们会详细的介绍。
对象活动的区域
当对象创建时,会存放在堆的Eden区(Eden区不满的情况,满了会触发minor GC,这也是触发ninor GC的条件),经过一次minor GC后该对象没有被判定为垃圾对象,该对象进入Survivor区的Survivor1区,再次经历minor GC 对象任然是存活对象,该对象Survivor2,每经历一次minor GC,S1和S2来回替换,当次数达到15次时,该对象任然存活,则该对象会进入老年代,当老年代满了后则会触发 Full GC 。我们要避免Full GC的触发,因为Full GC会强制暂停当前运行的所有线程,会导致在多线程下开发下,会出现程序卡顿现象。
GC算法
标记清除算法
该算法分为标记和清除两个阶段。标记就是把所有活动对象都做上标记的阶段;清除就是将没有做上标记的对象进行回收的阶段
优点
-
实现简单
-
与保守式GC算法兼容
缺点
-
碎片化:如上图所示,在回收的过程中会产生被细化的分块,到后面,即使堆中分块的总大小够用,但是却因为分块太小而不能执行分配。
-
分配速度:因为分块不是连续的,因此每次分块都要遍历空闲链表,找到足够大的分块,从而造成时间的浪费。
-
与写时复制技术不兼容:所谓写时复制就是fork的时候,内存空间只引用而不复制,只有当该进程的数据发生变化时,才会将数据复制到该进程的内存空间。这样,当两个进程中的内存数据相同的时候,就能节约大量的内存空间了。而对于标记-清除算法,它的每个对象都有一个标志位来表示它是否被标记,在每一次运行标记-清除算法的时候,被引用的对象都会进行标记操作,这个仅仅标记位的改变,也会变成对象数据的改变,从而引发写时复制的复制过程,与写时复制的初衷就背道而驰了。
复制算法
复制算法就是将内存空间按容量分成两块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后把已经使用过的这一块一次清理掉。这样使得每次都是对半块内存进行内存回收。内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。
优点
-
优秀的吞吐量。
-
可实现高速分配:复制算法不用使用空闲链表。这是因为分块是连续的内存空间,因此,调用这个分块的大小,只需要这个分块大小不小于所申请的大小,移动指针进行分配即可。
-
不会发生碎片化。
-
与缓存兼容。
缺点
-
堆的使用效率低下。
-
不兼容保守式GC算法。
-
递归调用函数。
标记-压缩算法
1、什么是标记-压缩算法?
标记-压缩算法与标记-清理算法类似,只是后续步骤是让所有存活的对象移动到一端,然后直接清除掉端边界以外的内存。
优缺点
该算法可以有效的利用堆,但是压缩需要花比较多的时间成本。
JVM调优
在JVM中,主要是对堆(新生代)、方法区和栈进行性能调优。各个区域的调优参数如下所示。
- 堆:-Xms、-Xmx
- 新生代:-Xmn
- 方法区(元空间)︰ -XX:MetaspaceSize、-XX:MaxMetaspaceSize
- 栈(线程): -Xss
设置JVM启动参数时,需要特别注意的是方法区的参数设置。
关于方法区(元空间)的JVM参数主要有两个:-XX:MetaspaceSize和-XX:MaxMetaspaceSize。
-XX:MetaspaceSize:指的是方法区(元空间)触发Full GC的初始内存大小(方法区没有固定的初始内存大小),以字节为单位,默认为21M。达到设置的值时,会触发FullGC,同时垃圾收集器会对这个值进行修改。如果在发生Full GC时,回收了大量内存空间,则垃圾收集器会适当降低此值的大小;如果在发生Full GC时,释放的空间比较少,则在不超过设置的-XX:MetaspaceSize值或者在没设置-XX:MetaspaceSize的值时不超过21M,适当提高此值。
-XX:MaxMetaspaceSize:指的是方法区(元空间)的最大值,默认值为-1,不受堆内存大小限制,此时,只会受限于本地内存大小。
最后需要注意的是: 调整方法区(元空间) 的大小会发生Full GC,这种操作的代价是非常昂贵的。如果发现应用在启动的时候发生了Full GC,则很有可能是方法区 (元空间)的大小被动态调整了。
所以,为了尽量不让JVM动态调整方法区(元空间)的大小造成频繁的Full GC,一般将-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置成一样的值。例如,物理内存8G,可以将这两个值设置为256M