JVM是中高级程序员必备技能,可以对项目进行管理,对性能进行调优
所谓虚拟机,就是一台虚拟机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
大名鼎鼎的VMware就属于虚拟机,它是完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机典型的代表就是java虚拟机了,它专门为执行某个单个计算机程序而设计。在java虚拟机中执行的指令我们称为java字节码指令。
java虚拟机是一种执行java字节码文件的虚拟计算机,它拥有独立的 运行机制。
java技术的核心就是java虚拟机,因为所有的java程序都运行在java虚拟机内部。
Java 虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条 java 指令,java 虚拟机中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪儿。
特点:
一次编译到处运行
自动内存管理
自动垃圾回收功能
现在的JVM不仅可以执行java字节码文件,还可以执行其他语言编译后的字节码文件,是一个跨语言平台。
JVM 是运行在操作系统之上的,它与硬件没有直接的交互
1、类加载器(ClassLoader)
2、运行时数据区(Runtime Data Area)
3、执行引擎(Execution Engine)
4、本地库接口(Native Interface)
详细图
程序在执行之前先要把java代码转换成字节码(class文件),JVM首先需要把字节码通过一定的方式类加载器(ClassLoader)把文件加载到内存中的运行时数据区(Runtime Data Area),而字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行。因此需要特定的命令解析器**执行引擎(Execution Engine)将字节码翻译成底层操作系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口本地库接口(Native Interface)来实现整个程序的功能,这就是这4个组成部分的职责与功能。
而我们通常所说的JVM组成指的是运行时数据区(Runtime Data Area),因为通常需要程序员吊事分析的区域就是”运行时数据区“,或者更具体的来说就是”运行时数据区“里面的Heap(堆)模块。
java编译器编译过程中,任何一个结点执行失败就会造成编译失败。虽然各个平台的java虚拟机内部实现细节不尽相同,但是它们执行的字节码内容确实一样的。
JVM主要任务就是负责将字节码装载到其内部,解释/编译为对应平台上的机器指令执行。JVM使用类加载器(ClassLoader)装载class文件。
类加载完成后,会进行字节码校验,字节码校验通过之后JVM解释器会把字节码翻译成机器码交由操作系统执行。
但不是所有的代码都是解释执行,JVM对此作了优化,比如HotSpot虚拟机,它本身提供了JIT(Just In Time)编译器。
java编译器输入的指令流基本上是一种基于栈的指令集架构,另一种指令集架构是基于寄存器的指令集架构。
这两种架构之间的区别:
基于栈式架构的特点
设计和实现更简单,适用于资源受限的系统。
使用零地址指令方式分配,其执行过程依赖于操作栈,指令集更小,编译器容易实现。
基于寄存器式架构特点:
指令完全依赖于硬件,可移植性差。
性能优秀,执行更高效。
完成一项操作使用的指令更少。
使用 javap -v class 文件可以将 class 文件反编译为指令集。
所以由于跨平台的设计,Java 指令集都是根据栈来设计的,不同 CPU 架构不同,
所以不能设计为基于寄存器的.
优点是跨平台,指令集小,编译器容易实现.
缺点是性能下降,实现同样功能需要更多的指令
类加载器子系统负责从文件系统或者网络中加载 class 文件。 classLoader 只
负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
加载的类信息存放于一块称为方法区(元空间)的内存空间。
根据类的地址,从硬盘上读取类的信息,
将信息读入到方法区,生成Class类的对象
验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;
对文件格式的验证:class 文件在文件开头有特定的文件标识(字节
码文件都以 CA FE BA BE 标识开头);主,次版本号是否在当前 java 虚拟机接收
范围内。
对元数据的验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java 语言规范的要求,例如这个类是否有父类;是否继承浏览不允许被继承的类 (final 修饰的类)…
准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
不包含用 final 修饰的 static 常量,在编译时进行初始化
例如: public static int value = 123;
value 在准备阶段后的初始值是 0,而不是 123。
解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文
件的逻辑符号,直接引用指向的方法区中某一个地址)。
1 )创建类的实例,也就是 new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“”))
5)初始化一个类的子类(会首先初始化子类的父类)
对 static 修饰的变量或语句块进行赋值.
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
顺序是:父类 static –> 子类 static –> 父类构造方法- -> 子类构造方法
public class ClassInit{
static{
num =20;
}
static int num = 10;
public static void main(String[] args){
//num从准备到初始化值变化过程 num=0 --> num=20 --> num=10
System.out.println(num);//10
}
}
public class ClassInit{
static int num = 10;
static{
num =20;
}
public static void main(String[] args){
//num从准备到初始化值变化过程 num=0 --> num=10 --> num=20
System.out.println(num);//20
}
}
站在 JVM 的角度看,类加载器可以分为两种:
站在 java 开发人员的角度来看,类加载器就应当划分得更细致一些。自 JDK1.2 以来 java 一直保持者三层类加载器。
启动类加载器(引导类加载器)
这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库
负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器.
出于安全考虑,引用类加载器只加载存放在
扩展类加载器
是由java语言实现的 继承自ClassLoader.
从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的
jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也 会自动由扩展类加载器加载.
应用程序类加载器(系统类加载器)
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.
派生于 ClassLoader 类.
负责加载用户类
用户自定义类加载器
例如Tomcat
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会 将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委 派模式.
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请 求委托给父类的加载器去执行.
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完 成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.
如果均加载失败,就会抛出 ClassNotFoundException 异常。
1 安全,可避免用户自己编写的类动态替换 Java 的核心类,如 java.lang.String
2 避免全限定命名的类重复加载(使用了 findLoadClass()判断当前类是否已加 载)
JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自 然就有被动使用
主动使用
1.通过new关键字被导致类的初始化,这是大家经常使用的初始化一个类的方式,他肯定会导致类的加载并且初始化
2.访问类的静态变量,包括读取和更新
3.访问类的静态方法
4.对某个类进行反射操作,会导致类的初始化
5.初始化子类会导致父类的的初始化
6.执行该类的 main 函数
被动使用
1.引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量 是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导 致初始化,比如:
public final static int NUMBER = 5 ; //不会导致类初始化,被动使用
public final static int RANDOM = new Random().nextInt() ; //会导致类的初 始化,主动使用
2.构造某个类的数组时不会导致该类的初始化,比如:
Student[] students = new Student[10] ;
主动使用和被动使用的区别在于类是否会被初始化
JVM 的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚
拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几
个运行时数据区域
1、程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看 作是当前线程所执行的字节码的行号指示器。
2、Java 虚拟机栈(Java Virtual Machine Stacks)
描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
3、本地方法栈(Native Method Stack)
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地 方法栈是为虚拟机调用 Native 方法服务的。
4、Java 堆(Java Heap)
是 Java 虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配 内存.
5、 方法区(Methed Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
方法区是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应用程序的实时运行.
Java 虚拟机定义了序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁.另外一些则是与线程一一对应的.这些与 线程对应的区域会随着线程开始和结束而创建销毁.
如图:红色的为多个线程共享,灰色的为单个线程私有的,即线程间共享:堆,方法区.
线程私有:程序计数器,栈,本地方法栈
jvm中的程序计数器不是cpu中的寄存器, 可以理解为计数器.
是一块非常小的内存空间,运行速度是最快的,不会出现内存溢出情况.
作用:
是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.
任何时间一个线程都只有一个方法在执行,程序计数器记录当前线程中的方法执行的位置. 以便于cpu在切换执行时,记录程序执行的为位置.
在运行时数据区中唯一一个不会出现内存溢出的区域.
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的 字节码指令.
由于跨平台性的设计,Java 的指令都是根据栈来设计的.不同平台CPU 架构不同,所以不能设计为基于寄存器的。
基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实 现同样功能需要更过的指令集
栈是运行时的单位,而堆是存储的单位
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据.
堆解决的是数据存储的问题,即数据怎么放,放在哪儿
Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈.每个线程在创建 时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用.
Java 虚拟机栈是线程私有的.
生命周期和线程一致.
主管 Java 程序的运行,它保存方法的局部变量(8 种基本数据类型,对象的引用地
址),部分结果,并参与方法的调用和返回
举例
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM 直接对 java 栈的操作只有两个:1、调用方法,进栈 2、执行结束后出栈
对于栈来说不存在垃圾回收问题.
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
每个线程都有自己的栈,栈中的数据都以栈帧为单位存储.
在这个线程上正在执行的每个方法都各自对应一个栈帧.
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息.
不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中 引用另一个线程的栈帧(方法).
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧.Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是 抛出异常.不管哪种方式,都会导致栈帧被弹出.
局部变量表、操作数栈、动态链接、方法返回地址
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局 部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
操作数栈(Operand Stack)(或表达式栈)
栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保 存一个方法返回地址。
Java 虚拟机栈管理 java 方法的调用,而本地方法栈用于管理本地方法的调用.
本地方法栈也是线程私有的.
允许被实现成固定或者是可动态扩展的内存大小.内存溢出方面也是相同的.
如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出
StackOverflowError.
本地方法是用 C 语言写的.
它的具体做法是在 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库.
一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域
Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理的最大一块内存空间.
堆内存的大小是可以调节.
例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)
一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率.
《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的.
所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.
《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分配在堆上.
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.
Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫 描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片
new 的新对象先放到伊甸园区,此区大小有限制.
当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.
然后将伊甸园区中的剩余对象移动到幸存者 0 区.
如果再次出发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.
如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
什么时候去养老区呢?默认是 15 次,也可以设置参数,最大值为 15 -XX:MaxTenuringThreshold=
在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111,即为 15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。
在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清 理.
若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space
配置新生代与老年代在堆结构的占比(一般不会调)
在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。比 如-XX:SurvivorRatio=8
新生区的对象默认生命周期超过 15 ,就会去养老区养老
JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的 都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。
部分收集:是不完整收集整个java 堆的垃圾收集。其中又分为:
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集。
老年区收集(Major GC / Old GC):只是老年区的垃圾收集。
整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。
整堆收集出席县的情况:
System.gc();时
老年区空间不足
方法区空间不足
开发期间尽量避免整堆收集.
为什么有 TLAB(Thread Local Allocation Buffer)
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据.
由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存 空间是线程不安全的.
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度.
什么是 TLAB?
TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区, 这是一个线程专用的内存分配区域。
如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块 指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如 果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
JVM 使用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自 己的 TLAB,这样可以避免线程同步,提高了对象分配的效率。
TLAB 空间的内存非常小,缺省情况下仅占有整个 Eden 空间的 1%,也可以 通过选项-XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的 百分比大小。
官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)
-Xms:初始堆空间内存(默认为物理内存的 1/64)
-Xmx:最大堆空间内存(默认为物理内存的 1/4)
-Xmn:设置新生代的大小(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日志
jdk7之前,将字符串常量池位置在 方法区(永久代)中存储. jdk8之后 方法区又称为元空间
jdk8之后,将字符串常量池的位置 放到了堆空间. 因为方法区只有触发FUll GC时才会回收.
因为程序中大量的需要使用字符串,所以将字符串常量池的位置改变到了堆中,可以及时回收无效的字符串常量.
方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、 class/method/field 等元数据、static final 常量、static 变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。
Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看做是一块独立于 java 堆的内存空间.
方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的.
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展.
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误
关闭 JVM 就会释放这个区域的内存.
方法区,栈,堆的交互关系
Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.
方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。
通过反编译字节码文件查看.
反编译字节码文件,并输出值文本文件中,便于查看。参数 -p 确保能查看 private 权限类型的字段或方法
javap -v -p Demo.class > test.txt
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。
下面也称作类卸载
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被 使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一个 Native Method 就是一个 java 调用非 java 代码的接口
非java语言实现的,例如 C C++
在定义一个 native method 时,并不提供实现体(有些像定义一个 Java interface),因为其实现体是由非 java 语言在外面实现的。
关键字 native 可以与其他所有的 java 标识符连用,但是 abstract 除外。
Java 使用起来非常方便,然而有些层次的任务用 java 实现起来不容易(与硬件交互),或者我们对程序的效率很在意时,问题就来了。
1.与 java 环境外交互:
有时 java 应用需要与 java 外面的环境交互,这是本地方法存在的主要原因。 你可以想想 java 需要与一些底层系统,如某些硬件交换信息时的情况。本地方法 正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 java 应用之外的繁琐细节。
2.Sun’s Java
Sun 的解释器是用 C 实现的,这使得它能像一些普通的 C 一样与外部交互。jre 大部分是用 java 实现的,它也通过一些本地方法与外界交互。例如:类 java.lang.Thread 的 setPriority()方法是用 Java 实现的,但是它实现调用的事该类里的本地方法 setPriority0()。
1.执行引擎是 Java 虚拟机核心的组成部分之一。
2.JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息。
3.那么,如果想要让一个 Java 程序运行起来,执行引擎(Execution Engine) 的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。
注意区分概念:
1.前端编译:从 Java 程序员-字节码文件的这个过程叫前端编译.
2.执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里的是后端编译)。
解释器:当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方 式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT(Just In Time Compiler)编译器:就是虚拟机将源代码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行。
起初将 Java 语言定位为“解释执行”还是比较准确的。再后来,Java 也发展出 可以直接生成本地代码的编译器。现在 JVM 在执行 Java 代码的时候,通常都会 将解释执行与编译执行二者结合起来进行。
原因:
JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的 内容“翻译”为对应平台的本地机器指令执行,执行效率低。
JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区 的 JIT 代码缓存中(执行效率更高了)。
是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。
JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其 直接编译为对应平台的本地机器指令,以此提升 Java 程序的执行性能。
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以 被称之为“热点代码”。
目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。
JIT 编译器执行效率高为什么还需要解释器?
1.当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。
2.编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
1.Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语
言没有垃圾收集技术,需要程序员手动的收集。
2.垃圾收集,不是 Java 语言的伴生产物。早在 1960 年,第一门开始使用内存动态分配和垃圾收集技术的 Lisp 语言诞生。
3.关于垃圾收集有三个经典问题:
哪些内存需要回收?
频繁回收堆内存,较少回收方法区 栈,本地方法栈,程序计数器没有垃圾回收
什么时候回收?
如何回收?
4.垃圾收集机制是 Java 的招牌能力,极大地提高了开发效率。如今,垃圾收集 几乎成为现代语言的标配,即使经过如此长时间的发展,Java 的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战。
垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
想要学习 GC,首先需要理解为什么需要 GC?
1.对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
2.除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理 将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。
3.随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有 GC
就不能保证应用程序的正常进行。
在早期的 C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用 new关键字进行内存申请,并使用 delete 关键字进行内存释放。比如以下代码:
MibBridge *pBridge= new cmBaseGroupBridge(); //如果注册失败,使用 Delete 释放该对象所占内存区域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete pBridge
这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。
有了垃圾回收机制后,上述代码极有可能变成这样
MibBridge *pBridge=new cmBaseGroupBridge();
pBridge->Register(kDestroy)
现在,除了 Java 以外,C#、Python、Ruby 等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为了现代开发语言必备的标准。
优点
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发.
担忧
1.对于 Java 开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于 “自动”,那么这将会是一场灾难,最严重的就会弱化 Java 开发人员在程序出现内存溢出时定位问题和解决问题的能力。
2.此时,了解 JVM 的自动内存分配和内存回收原理就显得非常重要,只有在真正了解 JVM 是如何管理内存后,我们才能够在遇见 OutofMemoryError 时,快速地根据错误异常日志定位问题和解决问题。
3.当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收,其中,Java 堆是垃圾收集器的工作重点
从次数上讲:
频繁收集 Young 区
较少收集 Old 区
基本不收集元空间(方法区)
内存溢出与内存泄漏
溢出: 内存不够用了
泄漏: 有些对象已经在程序不被使用了,但是垃圾回收机制并不能判定其为垃圾对象,不能将其回收掉,
这样的对象越积越多, 长久也会导致内存不够用.
例如: 与数据库连接完之后,需要关闭连接通道,但是没有关闭.
io 读写完成后没有关闭
垃圾标记阶段:主要是为了判断对象是否存活
1、在堆里几乎存放着几乎所有的java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
2.那么在 JVM 中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
3.判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
1.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3.引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在.Java 的垃圾回收器中没有使用这类算法
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集。
1.相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
2.相较于引用计数算法,这里的可达性分析就是 Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)
可达性分析实现思路
所谓"GCRoots根集合就是一组必须活跃的引用
其基本思路如下:
GC Roots 可以是哪些元素
1.虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。
2.本地方法栈内 JNI(通常说的本地方法)引用的对象
3.方法区中类静态属性引用的对象,比如:Java 类的引用类型静态变量
4.方法区中常量引用的对象,比如:字符串常量池(StringTable)里的引用
5.所有被同步锁 synchronized 持有的对象
6.Java 虚拟机内部的引用。
基 本 数 据 类 型 对 应 的 Class 对 象 , 一 些 常 驻 的 异 常 对 象 ( 如 : NullPointerException、OutofMemoryError),系统类加载器。
总结
简单一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,都可以作为 GC Roots 进行可达性分析。
finalize() 方法机制
对象销毁前的回调函数:finalize();
Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调 用这个对象的 finalize()方法,一个对象的 finalize()方法只被调用一次。 finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常 在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
Object 类中 finalize() 源码
protected void finalize() throws Throwable { }
永远不要主动调用某个对象的 finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
1.在 finalize()时可能会导致对象复活。
2.finalize()方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize()方法将没有执行机会。
3.一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。
由于 finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来 说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时 处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它立即进行回收就是不合理的。为此,定义虚拟机中的对象可能的三种状态。如下:
可触及的:从根节点开始,可以到达这个对象。
可复活的:对象的所有引用都被释放,但是对象有可能在 finalize()中复活。
不可触及的:对象的 finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次。
以上 3 种状态中,是由于 finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
具体过程
判定一个对象 objA 是否可回收,至少要经历两次标记过程:
1.如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。
2.进行筛选,判断此对象是否有必要执行 finalize()方法
代码演示
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
//super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器,触发FULL GC 也不是调用后立刻就回收的,因为线程的执行权在操作系统
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃 圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是:
标记-清除算法(Mark-Sweep)
复制算法(Copying)
标记-压缩算法(Mark-Compact)
执行过程
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为 stop the world),然后进行两项工作,第一项则是标记,第二项则是清除 。
标记:Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象。(注意:标记的是被引用的对象,也就是可达对象,并非标记的是即将被清除的垃圾对象)。
清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收。
标记-清除算法的优点:
非常基础和常见的垃圾收集算法容易理解
标记-清除算法的缺点:
标记清除算法的效率不算高
在进行 GC 的时候,需要停止整个应用程序,用户体验较差
这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。(空闲列表-记录垃圾对象地址)。
注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的 地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(也就是覆盖原有的地址)。
为了解决标记-清除算法在垃圾收集效率方面的缺陷,它将可用内存按容量 划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
复制算法优点
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
复制算法缺点
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销也不小
复制算法的应用场景
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,效率较高
老年代大量的对象存活,那么复制的对象将会有很多,效率会很低
在新生代,对常规应用的垃圾回收,一次通常可以回收 70% - 99% 的内存空间。
回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
背景
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况 在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下, 而且在执行完内存回收后还会产生内存碎片,所以 JVM 的设计者需要在此基础之上进行改进。
标记压缩算法执行过程
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法与标记-清除算法的比较
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
二者的本质差异在于标记-清除算法是一种非移动式的回收算法(空闲列表记录位置),标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记-压缩算法的优点
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价
标记-压缩算法的缺点
从效率上来说,标记-整理算法要低于复制算法。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址 。
移动过程中,需要全程暂停用户应用程序。即:STW
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但 是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
为什么要使用分代收集
前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集应运而生。
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如 Http 请求中的 Session 对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命周期比较长。
但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String 对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
目前几乎所有的 GC 都采用分代收集算法执行垃圾回收的 。
在 HotSpot 中,基于分代的概念,GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过 hotspot 中的两个 survivor 的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记- 清除或者是标记-清除与标记-整理的混合实现。
1.Mark 阶段的开销与存活对象的数量成正比。
2.Sweep 阶段的开销与所管理区域的大小成正相关。
3.Compact 阶段的开销与存活对象的数据成正比。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
在默认情况下,通过 System.gc()者 Runtime.getRuntime().gc() 的调用,会显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
然而 System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)。
JVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,我们可以在运行之间调用 System.gc()。
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
由于 GC 一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现 OOM 的情况。
大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。
Javadoc 中对 OutofMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存泄漏也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的“内存泄漏”。
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutofMemory 异常,导致程序崩溃。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
单例模式
单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部 对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
一些提供 close()的资源未关闭导致内存泄漏
数据库连接 dataSourse.getConnection(),网络连接 socket 和 io 连接必须手动 close,否则是不能被回收的。
Stop-the-World,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的 感觉,这个停顿称为 STW
可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿,为什么需要停顿所有 Java 执行线程呢?
1.分析工作必须在一个能确保一致性的快照中进行
2.一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
3.如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
4.被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉
5.像是网速不快造成电影卡带一样,所以我们需要减少 STW 的发生。
6.STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件。
7.越优秀,回收效率越来越高,尽可能地缩短了暂停时间。STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象。
既偏门又非常高频的面试题:强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
在 JDK1.2 版之后,Java 对引用的概念进行了扩充,将引用分为:
强引用(Strong Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref 包中找到它们的身影。如下图,显示了这 3 种引用类型对应的类,开发人员可以在应用程序中直接使用它们.
Reference 子类中只有终结器引用是包内可见的,其他 3 种引用类型均为
public,可以在应用程序中直接使用.
强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。 无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。宁可报 OOM,也不会 GC 强引用
软引用(SoftReference)在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对 其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
在 Java 程序中,最常见的引用类型是强引用(普通系统 99%以上都是强引用), 也就是我们最常见的普通对象引用,也是默认的引用类型。
当在 Java 语言中使用 new 操作符创建一个新的对象,并将其赋值给一个变量的 时候,这个变量就成为指向该对象的一个强引用。
只要强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
只要强引用的对象是可达的,jvm 宁可报 OOM,也不会回收强引用。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者 显式地将相应(强)引用赋值为 null,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。
相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的, 在一定条件下,都是可以被回收的。所以,强引用是造成 Java 内存泄漏的主要 原因之一。
public class StrongReferenceTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer ("Hello,world");
StringBuffer str1 = str;
str = null;
System.gc();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str1);
}
软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。注意,这里的第 一次回收是不可达的对象
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。
类似弱引用,只不过 Java 虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
Object obj = new Object();// 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用
弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次 垃圾收集发生为止。在系统 GC 时,只要发现弱引用,不管系统堆空间使用是否 充足,都会回收掉只被弱引用关联的对象。
// 声明强引用
Object obj = new Object();
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //销毁强引用
弱引用对象与软引用对象的最大不同就在于,当 GC 在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC 总是进行回收。弱引用对象更容易、更快被 GC 回收。
也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果
一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。
// 声明强引用
Object obj = new Object();
// 声明引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 声明虚引用(还需要传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;
如果说收集算法是内存回收的方法论,那么收集器就是内存回收的实践者.
垃圾收集器没有在 java 虚拟机规范中进行过多的规定,可以由不同的厂商、 不同版本的 JVM 来实现。
由于 JDK 的版本处于高速迭代过程中,因此 Java 发展至今已经衍生了众多
的 GC 版本。
从不同角度分析垃圾收集器,可以将 GC 分为不同的类型。
按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。
串行回收指的是在同一时间段内只允许有一个 CPU 用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
和串行回收相反,并行收集可以运用多个 CPU 同时执行垃圾回收,因此提 升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用 "stop-the-world"机制。
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
独占式垃圾回收器(stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java 堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间。
图中展示了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
Serial 收集器是最基本的、新生代的、发展历史最悠久的收集器。
特点:单线程、简单高效,采用复制算法,对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束 (Stop The World)。
应用场景:适用于 Client 模式下的虚拟机。
Serial / Serial Old 收集器运行示意图
Serial Old 是 Serial 收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在 Client 模式下的虚拟机中。也可在 Server 模式下使用。
ParNew 收集器其实就是 Serial 收集器的多线程版本。
除了使用多线程外其余行为均和 Serial 收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
特点:多线程、ParNew 收集器默认开启的收集线程数与 CPU 的数量相同,在CPU 非常多的环境中,可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
应用场景:ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,因为它是除了 Serial 收集器外,唯一一个能与 CMS 收集器配合工作 的。
ParNew/Serial Old 组合收集器运行示意图如下:
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。
但是两者有巨大的不同点:
Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,
因此适合没有交互的后台计算,。
是 Parallel Scavenge 收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及 CPU 资源敏感的场合,都可以优先考虑
Parallel Scavenge+Parallel Old 收集器。
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直 接关联的对象进行标记。
并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。 这个过程非常耗时。
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS 的优点:
并发收集
低延迟
CMS 的弊端:
既然我们已经有了前面几个强大的 GC,为什么还要发布 Garbage First(G1)GC?
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC 就不能保证应用程序正常进行,而经常造成 STW 的 GC 又跟不上实际的需求,所以才会不断地尝试对 GC 进行优化。G1(Garbage-First)垃圾回收器是在 Java7 update 4 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一.
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
G1 是一款面向服务端应用的垃圾收集器。
为什么名字叫做 Garbage First(G1)呢?
因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region) (物理上不连续的)。使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者1 区,老年代等。
G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各 个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收 价值最大的 Region.
由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给 G1 一个名字:垃圾优先(Garbage First)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核 CPU 及大容量内存的机器,以极高概率满足 Gc 停顿时间的同时,还兼具高吞吐量的性能特征。
G1 收集器可以“ 建立可预测的停顿时间模型 ”,它维护了一个列表用于记录每个 Region 回收的价值大小(回收后获得的空间大小以及回收所需时 间的经验值),这样可以保证 G1 收集器在有限的时间内可以获得最大的回收效率。
如下图所示,G1 收集器收集器收集过程有初始标记、并发标记、最终标记、 筛选回收,和 CMS 收集器前几步的收集过程很相似:
① 初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,需
要停止用户线程,单线程执行。
② 并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活
对象,这个阶段耗时较长,但可以和用户线程并发执行。
③ 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。
④ 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First 的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而 是停顿用户线程。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器。