万字总结超详细JVM
中高级程序员必备技能
项目管理,性能调优
JVM作用
Java 虚拟机负责装载字节码到其内部,解释/编译为对应平台上的机器码 指令执行,每一条java指令,java虚拟机中都有详细定义,如怎么取操作数, 怎么处理操作数,处理结果放在哪儿。
现在的JVM不仅可以执行java字节码文件,还可以执行其他语言编译后的 字节码文件,是一个跨语言平台
jvm负责把编译后的字节码转换为机器码
jvm内部构造
1.类加载部分(ClassLoader)
负责把硬盘上字节码加载到内存中(运行时数据区)
2.运行时数据区(RuntimeData Area)
负责存储运行时产生的各种数据 类信息 对象信息 方法信息
3.执行引擎(Execution Engine)
负责将字节码转换为机器码
4.本地方法接口(Native Interface)
调用本地方法( 1.启动线程start0() 2.object类中的hashCode()–对象的内存地址 )
垃圾回收部分
一.类加载系统
类加载系统,负责将硬盘上的字节码文件加载到jvm中,生成类的Class对象,存储在方法区
类就是一个模版
类加载过程
加载
以二进制文件流进行读取,在内存中为类生成Class对象
- 通过类名(地址)获取此类的二进制字节流.
- 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构.
- 在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数 据的访问入口
链接
1.验证: 检验被加载的类是否有正确的内部结构,并和其他类协调一致;
验证文件格式是否一致:class文件在文件开头有特定的文件标识(字节 码文件都以CAFEBABE标识开头);主,次版本号是否在当前java虚拟机接收 范围内. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 java 语言规范的要求,例如这个类是否有父类;是否继承浏览不允许被继承的类 (final 修饰的类)…
2.准备阶段: 则负责为类的静态属性分配内存,并设置默认初始值;
不包含用final修饰的static常量,在编译时进行初始化
3.解析:将类中的符号引用替换成直接引用(符号引用是Class文件的逻辑符号, 直接引用指向的方法区中某一个地址)
初始化
初始化是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化, 主要对类变量进行初始化。初始化阶段就是执行底层类构造器方法()的 过程。
初始化阶段主要是为类中静态成员进行赋值,因为类加载执行完初始化阶段,才说明类加载完成.
类在哪些情况下会被加载?
1.类被使用的情况下,调用类中的静态成员(方法,变量)
2.new 类的对象
3.在类中执行main方法
4.对类进行反射操作:Class.forName(“地址”)
5.初始化子类会导致父类初始化
类在哪些情况不会被加载?
1…引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量 是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致类加载
public final static int NUMBER = 5 ; //不会导致类初始化,被动使用
public final static int RANDOM = new Random().nextInt() ; //会导致类加载
//静态常量 final static int a=0;
//静态变量 final int a=0;
2.构造某个类的数组时不会导致该类的初始化
Student[] students = new Student[10] ;
类加载器分类
类加载器就是实际负责读取类的功能
站在JVM的角度看
类加载器可以分为两种:
- 引导类加载器(启动类加载器BootstrapClassLoader). (不是用java写的,是用c/c++),负责读取加载java中杜策部分系统库
- 其他所有类加载器,这些类加载器由java语言实现,独立存在于虚拟机外部,并 且全部继承自抽象类java.lang.ClassLoader(用来读取写的应用程序)
站在java开发人员的角度来看
类加载器就应当划分得更细致一些,保持三层类加载器.
引导类加载器(启动类加载器 BootStrapClassLoader)
这个类加载器使用C/C++语言实现,嵌套在JVM内部.它用来加载java核心类库. 负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器. 出于安全考虑,引用类加载器只加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath 参数锁指定的路径中存储放的类.
扩展类加载器(Extension ClassLoader)
Java 语言编写的,由sun.misc.Launcher$ExtClassLoader 实现. 派生于ClassLoader类. 从java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的jar放在此目录下,也 会自动由扩展类加载器加载
应用程序类加载器(系统类加载器 ApplicationClassLoader)
Java 语言编写的,由sun.misc.Launcher$AppClassLoader 实现. 派生于ClassLoader类. 加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类. 该类加载器是程序中默认的类加载器. ClassLoader 类,它是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)
加载自己开发的应用程序类
双亲委派机制
Java 虚拟机对class文件采用的是按需加载的方式,也就是说当需要该类时才会 将它的class 文件加载到内存中生成class对象.而且加载某个类的class文件 时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.
当加载一个类时,总是让父级类加载器加载,确保把系统中类优先加载,直到父类加载器找不到类时,再逐级向下,让子级类加载器加载.如果子级也找不到,最终抛出异常
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请 求委托给父类的加载器去执行.
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终 将到达顶层的启动类加载器.
- 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完 成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制. 如果均加载失败,就会抛出ClassNotFoundException异常。
思考:我们自己创建一个名为java.lang的包,再创建一个名为String的类,当我们 new String()时,会将加载创建核心类库中的String对象还是创建我们自己创建 的String 类对象?
双亲委派优点?
安全,可避免用户自己编写的类替换Java的核心类,如java.lang.String.
如何打破双亲委派机制
Java 虚拟机的类加载器本身可以满足加载的要求,但是也允许开发者自定义类 加载器。
在ClassLoader 类中涉及类加载的方法有两个,loadClass(String name), findClass(String name),这两个方法并没有被final修饰,也就表示其他子类可以 重写.
重写findClass 方法 我们可以通过自定义类加载重写方法打破双亲委派机制, 再例如tomcat等都有自己定义的类加载器
二.JVM 运行时数据区
运行时数据区组成概述
JVM的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从Java虚 拟机规范,Java8 虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几 个运行时数据区域:
1.程序计数器
JVM中的程序计数寄存器(ProgramCounter Register)这里翻译为程序计 数器更容易理解. 程序计数器用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读 取下一条指令
用来记录每一个线程执行的指令位置,是执行速度最快的,是线程私有的(每一个线程都会一个程序计数器,生命周期与线程同步),此区域不会出现内存溢出(不够用)的情况,也不会出现垃圾回收
1它是一块很小的内存空间,也是运行速度最快的存储区域.
2在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与 线程生命周期保持一致.
3程序计数器会存储当前线程正在执行的Java方法的JVM指令地址.
4它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
2.虚拟机栈
栈的基本概念 :栈是运行时的单位,即栈解决程序的运行问题,即程序如何执行,或者说如何处 理数据.调用方法,方法入栈,运行结束出栈.一个方法就是一个栈帧,在栈帧中存储局部变量,运行结果,虚拟机栈也是线程私有的,线程之间互隔离.
Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java栈.每个线程在创 建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调 用.Java 虚拟机栈是线程私有的.主管Java程序的运行,它保存方法的局部变量(8 种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回
是用来运行java自己写的方法
栈区域也不存在垃圾回收,但是会存在内存溢出问题.
栈的特点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器. JVM直接对java栈的操作只有两个:调用方法入栈.执行结束后出栈. 对于栈来说不存在垃圾回收问题,栈中会出现异常,当线程请求的栈深度大于虚拟机所允许的深度时,会出现 StackOverflowError.
栈的运行原理
JVM直接对java栈的操作只有两个,就是对栈帧的入栈和出栈,遵循先进后出/ 后进先出的原则.
在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方 法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(CurrentFrame),与当前栈 帧对应的方法称为当前方法(CurrentMethod),定义这个方法的类称为当前 类(Current Class).
执行引擎运行的所有字节码指令只针对当前栈帧进行操作.
如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶 端,成为新的当前栈帧.
不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中 引用另一个线程的栈帧(方法).
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果 给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧. Java 方法有两种返回的方式,一种是正常的函数返回,使用return指令,另一种是 抛出异常.不管哪种方式,都会导致栈帧被弹出.
栈帧的内部结构
每个栈帧都存着:
1.局部变量表(LocalVariables)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局 部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量, 则存的是指向对象的引用。(例如 int a=10)
2.操作数栈(OperandStack)(或表达式栈)
栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程 中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可 以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。(例如 a+b)
3.方法返回地址(RetuenAddress)(或方法正常退出或者异常退出的定义)
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保 存一个方法返回地址
3.本地方法栈
1.Java 虚拟机栈管理java方法的调用,而本地方法栈用于管理本地方法的调用.
2.本地方法栈也是线程私有的.
3.允许被实现成固定或者是可动态扩展的内存大小.内存溢出方面也是相同的. 如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出 StackOverflowError.
4.本地方法是用C语言写的.
5.它的具体做法是在Native Method Stack 中登记native方法,在Execution Engine 执行时加载本地方法库
本地方法栈是用来执行调用的本地方法,不会存在垃圾回收,会出现内存溢出问题
4.堆
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例都应当在运行时分 配在堆上.
堆也是Java内存管理的核心区域.是JVM管理的最大一块内存空间.
堆内存的大小是可以调节. 例如:-Xms:10m(堆起始大小)-Xmx:30m(堆最大内 存大小)
所有的线程共享Java堆
堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
堆是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.
堆的作用是用来存储java语言产生的对象的.是运行时数据区中最大的一块内存空间,空间大小可以设置,堆空间是所有线程共享的,堆空间是垃圾回收的重点区域.堆中没有被使用到的垃圾对象会被垃圾回收器(Garbage Collection)回收
堆空间区域划分
Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为Eden(伊甸园)区+Survivor(幸存者)区(survivor0区+survivor1区)
为什么分区(代)?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫 描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
可以将不同生命周期的对象存储在不同的区域,针对不同的区域采用不同的垃圾回收算法,使得垃圾回收策略更加优化.
对象创建内存分配过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如 何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考 虑GC执行完内存回收后是否会在内存空间中产生内存碎片.
- new的新对象先放到伊甸园区,此区大小有限制.
- 当伊甸园的空间填满时,程序又需要创建对象时,JVM的垃圾回收器将对伊甸园区进 行垃圾回收,将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.
- 然后将伊甸园区中的剩余对象移动到幸存者0区.
- 如果再次出发垃圾回收,此时上次幸存下来存放到幸存者0区的对象,如果没有回收, 就会被放到幸存者1区,每次会保证有一个幸存者区是空的.
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区.
- 什么时候去老年代呢?默认是15次,也可以设置参数,最大值为15-XX:MaxTenuringThreshold= 在对象头中,它是由4位数据来对GC年龄进行保存的,所以最大值为1111, 即为15。所以在对象的GC年龄达到15时,就会从新生代转到老年代。
- 在老年代,相对悠闲,当老年代内存不足时,触发GC,进行老年代的内存清理.
- 若老年代执行了GC之后发现依然无法进行对象存储,会对整堆进行GC, 之后依然无法进行对象存储, 就会产生OOM异常. Java.lang.OutOfMemoryError:Java heap space
新创建的对象都存储在伊甸园区,当垃圾回收时,将还被使用的对象转移至某一个survivor区,将伊甸园区的垃圾对象进行清除,当下一次垃圾回收时,将伊甸园区存活的对象与当前正在使用的survivor区存活的对象转移到另一个survivor区(每次会空闲一个survivor区),当一个对象经历过15次垃圾回收后,仍然存活,那么就把该对象移动到老年代,老年代只有在空间不足时才会进行垃圾回收,当回收后内存仍然不足时,会触发FULL GC(整堆收集), 应尽量避免.当整堆收集后,仍然不够使用,那么会出现内存溢出异常Java.lang.OutOfMemoryError:Java heap space(OOM)
堆空间的参数设置
官网地址: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC处理日
JVM调优
可以根据程序具体的使用场景,对运行时数据区的各种空间大小进行调整,例如堆,方法区,对垃圾回收器进行选择.
5方法区
方法区主要是用来存储加载类的信息,方法区的大小也是可以设置的.方法区也会进行垃圾回收,方法区也可能会出现内存溢出问题.
方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、 class/method/field 等元数据、static final 常量、static 变量、即时编译器编译 后的代码等数据。 Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部 分,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的 就是要和堆分开.
所以,方法区看做是一块独立于java堆的内存空间
方法区在JVM启动时被创建,并且它的实际的物理内存空间中和Java堆区一样都 可以是不连续的. 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误 关闭JVM就会释放这个区域的内存.
方法区,栈,堆的交互关系
方法区大小设置
Java 方法区的大小不必是固定的,JVM可以根据应用的需要动态调整.
1.元数据区大小可以使用参数-XX:MetaspaceSize指定
2.方法区一旦沾满 就会触发FullGC(整堆收集).
3.因此为了减少FullGC那么这个-XX:MetaspaceSize可以设置一个较高的值.
方法区的垃圾回收
方法区的垃圾回收,是对类信息进行回收,类信息如果不再被使用,类信息也可以被卸载.
-
有些人认为方法区(如Hotspot虚拟机中的元空间或者永久代)是没有垃圾 收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的, 提到过可以不要求虚拟机在方法区中实现垃圾收集。
-
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相 当苛刻。但是这部分区域的回收有时又确实是必要的.
下面也称作类卸载 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被 使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
1.该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子 类的实例。 (该类产生的对象都不存在)
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加 载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。 (该类的Class对象不再被使用)
3.该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通 过反射访问该类的方法。(加载该类的类加载器也被回收)
三.本地方法接口
是虚拟机中专门用来调用本地方法的接口.
1.什么是本地方法
简单来讲,一个NativeMethod就是一个java调用非java代码的接口,一个 Native Method 是这样一个java方法:该方法的底层实现由非Java语言实现, 比如C。这个特征并非java特有,很多其他的编程语言都有这一机制 在定义一个native method 时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。 关键字native可以与其他所有的java标识符连用,但是abstract除外。
java中被native关键字修饰的方法,没有方法体,不是用java语言实现的方法,用c/c++在操作系统底层实现的方法.
例如:1.Object类中的hashCode()—>获取对象的内存地址,涉及到读取内存.; 2.IO中读取文件(输入需要操作硬盘) read0();3.启动线程 start0() 启动线程就是将这个线程注册到操作系统;
2.为什么要使用NativeMethod
与java环境外交互 有时java应用需要与java外面的环境交互,这是本地方法存在的主要原因。你 可以想想java需要与一些底层系统,如某些硬件交换信息时的情况。本地方法 正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需 去了解java应用之外的繁琐细节。
java属于应用层语言,有时候需要对硬件系统资源进行调用,此时就不方便,系统资源不允许应用层程序直接调用.那就需要通过本地方法调用操作硬件资源
四.执行引擎
1.执行引擎是Java虚拟机核心的组成部分之一。
2.JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在 操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只 是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。
3.那么,如果想要让一个Java程序运行起来,执行引擎(ExecutionEngine) 的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单 来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
注意区分概念:
1.前端编译:从Java程序员-字节码文件的这个过程叫前端编译.
2.执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里的是后端 编译)
java程序执行过程中涉及两次编译:
第一次: .java文件(源代码 通过调用jdk中的编译器)—>.class文件 (前端编译)
第二次:通过执行引擎 将字节码编译为机器码 (后端编译)
什么是解释器?什么是JIT编译器?
将字节码转换为机器码有两种方式:
解释器(解释执行):当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方 式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。 对字节码逐行进行解释翻译,重复性代码也是每次都需要解释执行,效率低
JIT(Just In Time Compiler)编译器(编译执行):就是虚拟机将源代码一次性直接编译 成和本地机器平台相关的机器语言,但并不是马上执行。对某一段字节码进行整体编译,然后存储在方法区,若以后使用时,就不需要编译,效率高.编译器会针对执行过程中的热点代码进行编译,并且缓存起来,
为什么Java是半编译半解释型语言?
程序开始运行时,解释器可以立即发挥作用,投入使用,而编译器虽然执行效率高,但是前期需要对热点代码进行跟踪和编译,需要消耗时间.
起初将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出 可以直接生成本地代码的编译器。现在JVM在执行Java代码的时候,通常都会 将解释执行与编译执行二者结合起来进行。
原因: JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此 避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解 释器在运行时采用逐行解释字节码执行程序的想法。 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的 内容“翻译”为对应平台的本地机器指令执行,执行效率低。 JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区 的JIT 代码缓存中(执行效率更高了)。 是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要 根据代码被调用执行的频率而定。 JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其 直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。 一个被多次调用的方法,或者是一-个方法体内部循环次数较多的循环体都可以 被称之为“热点代码”。 目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
JIT 编译器执行效率高为什么还需要解释器?
1.当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立 即执行。 2.编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译 为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取 一个平衡点。
垃圾回收
什么是垃圾?
垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一 直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出.
就是一个对象不再被任何的引用所指向.垃圾对象如果不清理,新的对象可能没有足够空间,可能会导致内存溢出问题.
早期垃圾回收
在早期的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等语言都使用了自动垃圾回收的思 想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为 了现代开发语言必备的标准。
优点:解放了程序员
缺点:会占用一些内存空间(垃圾不是出现后立即回收),降低了程序员管理内存的能力.
应该关心哪些区域的回收?
对象,堆,频繁回收年轻代,较少回收老年代
方法区 类信息卸载 整堆收集时会进行回收 FULL GC
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收,其中,Java堆是垃圾收集器的工作重点
从次数上讲: 频繁收集Young区 较少收集Old区 基本不收集元空间(方法区)
内存溢出与内存泄漏
内存溢出:内存溢出(OutOfMemory,简称OOM)是指应用系统中存在无法回收的 内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出.(内存不够用)
内存泄漏 :内存泄漏也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但 是GC又不能回收他们的情况,才叫内存泄漏。 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可 用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutofMemory异常,导致程序崩溃。(系统中用不到的,但是又不能回收的对象).
例如:
1.单例模式
单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部 对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
2.一些提供close()的资源未关闭导致内存泄漏 数据库连接 dataSourse.getConnection(),网络连接socket和io连接必须 手动close,否则是不能被回收的。
Stop the World
垃圾回收时会经历两个阶段: 1.标记阶段 2.回收阶段
在标记和回收时,需要我们用户线程进行暂停,不暂停会在标记和回收时出现错标和漏标.
Stop-the-World,简称 STW,指的是GC事件发生过程中,会产生应用程序的 停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的 感觉,这个停顿称为STW。
可达性分析算法中枚举根节点(GCRoots)会导致所有Java执行线程停顿,为 什么需要停顿所有 Java 执行线程呢?
1.分析工作必须在一个能确保一致性的快照中进行
2.一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
3.如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保 证,会出现漏标,错标问题
4.被 STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉 像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
5.越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正 常的工作线程全部停掉
垃圾回收算法
分为垃圾标记阶段和垃圾回收阶段
垃圾标记阶段算法
将虚拟机中不再被任何引用所指向的对象标记出来,在垃圾回收阶段就会将标记出来的对象进行回收.
垃圾标记阶段:主要是为了判断对象是否是垃圾对象
1.在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需 要区分出内存中哪些是有用对象,哪些是垃圾对象。只有被标记为己经是垃圾对 象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我 们可以称为垃圾标记阶段
2.那么在JVM中究竟是如何标记一个垃圾对象呢?简单来说,当一个对象已经 不再被任何引用指向时,就可以宣判为垃圾对象。
3.判断对象是否为垃圾对象一般有两种方式:引用计数算法和可达性分析算法。
1.引用计数算法(存在缺陷,没有被虚拟机所使用)
设计思想:在对象中维护一个整数计数器变量,当有引用指向对象时,计数器加1;相反就减1(引用断开);
1.引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的情况。
2.对于一个对象A,只要有任何一个引用指向了对象A,则对象A的引用计数器 就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0, 即表示对象A不可能再被使用,可进行回收。
3.优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
4.缺点: 耗时占空间,无法解决循环应用的情况.
1.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3.引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命 缺陷,导致在.Java的垃圾回收器中没有使用这类算法。
2.可达性分析算法(根搜索算法)
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
1.相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效 等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题, 防止内存泄漏的发生。
2.相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的 垃圾收集通常也叫作追踪性垃圾收集(TracingGarbageCollection)
可达性分析实现思路
从一些可以成为GC Roots的对象开始向下查找,只要某一个对象与GC Roots对象有联系,可以判断对象是被使用的,与GC Roots对象引用链没有任何关系的对象,可以视为垃圾对象.
所谓"GCRoots”根就是一组必须活跃的引用 其基本思路如下:
1.可达性分析算法是以根(GCRoots)为起始点,按照从上至下的方式搜索被根 对象所连接的目标对象是否可达。
2.使用可达性分析算法后,内存中的存活对象都会被根直接或间接连接着,搜索 所走过的路径称为引用链(ReferenceChain)
3.如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡, 可以标记为垃圾对象。
GCRoots可以是哪些元素?
1.虚拟机栈中引用的对象 比如:各个线程被调用的方法中使用到的参数、局部变量等。
2.方法区中类静态属性引用的对象,比如:Java类的引用类型静态变量
3.所有被同步锁synchronized持有的对象
4.Java 虚拟机内部的引用。 基本数据类型对应的 Class 对象,一些常驻的异常对象(如: NullPointerException、OutofMemoryError),系统类加载器。
那些对象可以作为GC Roots(根对象):1.虚拟机栈中(被调用的方法)所使用的对象2.类中的静态属性3.虚拟机中使用的系统类对象
对象的 finalization 机制
finalize() 方法 Object 类中 finalize() 源码 protected void finalize() throws Throwable { }
对象销毁前的回调方法:finalize();此方法是在对象被回收前,由虚拟机自动调用,在对象被回收前,需要执行的一些操作,就可以在此方法中编写,此方法可以在子类中重写,此方法只会被调用一次(第一次被判定为垃圾,要对其回收,调用finalize(),对象有可能又被引用了,对象就不能被回收,当下一次被判定为垃圾对象时,就不会调用此方法)
Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁 之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调 用这个对象的finalize()方法,一个对象的finalize()方法只被调用一次。 finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
理 由包括下面三点: 1.在 finalize()时可能会导致对象复活。 2.finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下, 若不发生GC,则finalize()方法将没有执行机会。 3.一个糟糕的finalize()会严重影响GC的性能。比如finalize是个死循环。
生存还是死亡?
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来 说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时 处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己, 如果这样,那么对它立即进行回收就是不合理的。为此,定义虚拟机中的对象可 能的三种状态。
如下:
可触及的:从根节点开始,可以到达这个对象。
可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及 状态。
以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可 触及时才可以被回收
final修饰变量,类 finally用于try catch异常处理中 finalize()用于对象回收
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
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中比较常见的三种垃圾收集算法是
标记-复制算法(Copying)
将内存划分多个较小的块,当发生垃圾回收时,讲一个区域中存活的对象复制到另一个区域,在另一个区域从头开始排列,清除当前垃圾回收的区域.
优点:清理之后 ,内存没有碎片
缺点:回收时,需要移动对象,所以适合小内存块且存活对象较少的情况.
它将可用内存按容量划分为两块,每次只使用其中的一块。在垃圾回收时将 正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的 内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
复制算法的应用场景:复制算法适合存活对象少,垃圾对象多,特别适合收新生代。
新生代:老年代=2:1 新生代中 eden:survivor0:survivor1=8:1:1
标记-清除算法(Mark-Sweep)
执行过程:
清除:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在 空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如 果够,就存放(也就是覆盖原有的地址)。
将被标记为垃圾的对象地址进行记录,会判断垃圾对象空间是否能存储下后面分配的新对象,如果能存储下,用新对象直接覆盖垃圾对象,存活对象不发生移动.
优点:不需要移动对象
缺点:回收后内存中会出现碎片
标记-压缩(整理)算法(Mark-Compact)
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况 在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。 如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于 老年代垃圾回收的特性,需要使用其他的算法。
标记-清除算法的确可以应用在老年代中,但是在执行完内存回收后还会产 生内存碎片,所以JVM的设计者需要在此基础之上进行改进。
标记压缩算法执行过程: 将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间.
将存活的对象会移动到内存区域的一端,按顺序进行排列(压缩),清理边界以外的空间,在标记-清除的基础上进行一次内存整理
优点:回收后没有内存碎片
标记-清除和标记-压缩对比
标记-清除:不移动存活对象
标记0压缩:会移动存活对象
两者都适合与老年代于老年代对象回收
先使用标记-清除算法 当老年代空间不足 或者不能存储一个较大的对象时,在使用标记-压缩算法.
分代收集
为什么要使用分代收集
前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有 自己独特的优势和特点。分代收集应运而生。
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此, 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收 算法,以提高垃圾回收的效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相 关: 比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务 直接挂钩,因此生命周期比较长。 但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命 周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这 些对象,有些对象甚至只用一次即可回收。 目前几乎所有的GC都采用分代收集算法执行垃圾回收的,在HotSpot中,基于 分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(YoungGen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对 象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题, 通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记 清除或者是标记-清除与标记-压缩的混合实现。 分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
垃圾回收器
如果说垃圾收集算法是内存回收的方法论,那么收集器就是内存回收的实践者.
垃圾收集器没有在java虚拟机规范中进行过多的规定,可以由不同的厂商、 不同版本的JVM来实现。
由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的 垃圾回收器。从不同角度分析垃圾收集器,可以将GC分为不同的类型。
实际使用时,可以根据实际的使用场景选择不同的垃圾回收器,这也是JVM调 优的重要部分.
垃圾回收器是对垃圾回收过程的实践者,不同的虚拟机中,垃圾回收器的种类很多.
垃圾回收器分类
按线程数可以分为单线程(串行)垃圾回收器和多线程(并行)垃圾回收器
从线程数量上分:
单线程垃圾回收器(Serial) :只有一个线程进行垃圾回收,使用于小型简单的使用场景,垃圾回收时,其他用户 线程会暂停
多线程垃圾回收器(Parallel) :多线程垃圾回收器内部提供多个线程进行垃圾回收,在多cpu情况下大大提升垃 圾回收效率,但同样也是会暂停其他用户线程
从工作模式上分:
可以分为独占式和并发式垃圾回收器
独占式:垃圾回收线程在执行时,其他用户线程需要暂停(stop the world)
并发式:垃圾回收线程和永辉线程可以做到并发执行
按工作的内存区间分:
又可分为年轻代垃圾回收器和老年代垃圾回收器。
GC(垃圾收集器)性能指标
1.吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
2.垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
3.暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
4.内存占用:Java堆区所占的内存大小。
HotSpot 垃圾收集器
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线, 则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代 收集器。
Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old,CMS,G1
CMS 回收器
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿 时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并 发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
多线程,老年代收集器 开创了垃圾收集线程与用户线程并发执行的先例.
垃圾回收过程:
初始标记:StopTheWorld,仅使用一条初始标记线程对所有与 GCRoots 直 接关联的对象进行标记。 (独占执行)
并发标记:垃圾回收线程,与用户线程并发执行。此过程进行可达性分析,标 记出所有废弃对象。 (并发执行)
重新标记:StopTheWorld,使用多条标记线程并发执行,将刚才并发标记过 程中新出现的废弃对象标记出来。 (独占执行)
并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。 这个过程非常耗时。 (并发执行)
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上 说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
G1(Garbage First)回收器
既然我们已经有了前面几个强大的GC,为什么还要发布GarbageFirst(G1)GC?
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有 GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需 求,所以才会不断地尝试对GC进行优化。
G1(Garbage-First)垃圾回收器是在Java7 update 4之后引入的一个新 的垃圾回收器,是当今收集器技术发展的最前沿成果之一.
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步 降低暂停时间(pausetime),同时兼顾良好的吞吐量。
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以 才担当起“全功能收集器”的重任与期望。 G1是一款面向服务端应用的垃圾收集器。
为什么名字叫做GarbageFirst(G1)呢?
因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region) (物理上不连续的逻辑上连续的)。使用不同的Region来表示Eden、幸存者 0 区,幸存者1区,老年代等。
G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各 个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时 间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收 价值最大的Region.
由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们 给G1一个名字:垃圾优先(GarbageFirst)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备 多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具 高吞吐量的性能特征。
如下图所示,G1 收集器收集器收集过程有初始标记、并发标记、最终标记、 筛选回收,和 CMS 收集器前几步的收集过程很相似:
① 初始标记:标记出 GCRoots 直接关联的对象,这个阶段速度较快,需 要停止用户线程,单线程执行。
② 并发标记:从 GCRoot 开始对堆中的对象进行可达新分析,找出存活 对象,这个阶段耗时较长,但可以和用户线程并发执行。
③ 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。
④ 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排 序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包 含垃圾最多的区域.这就是 GarbageFirst 的由来——第一时间清理垃圾最多 的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而 是停顿用户线程。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。
查看JVM垃圾回收器设置垃圾回收器
打印默认垃圾回收器
-XX:+PrintCommandLineFlags-version
JDK 8 默认的垃圾回收器
年轻代使用 Parallel Scavenge GC
老年代使用 Parallel Old GC
打印垃圾回收详细信息
-XX:+PrintGCDetails-version
设置默认垃圾回收器
Serial 回收器
-XX:+UseSerialGC 年轻代使用Serial GC, 老年代使用Serial Old
GC ParNew 回收器
-XX:+UseParNewGC 年轻代使用 ParNew GC,不影响老年代。
CMS回收器
-XX:+UseConcMarkSweepGC 老年代使用 CMS GC。
G1回收器
-XX:+UseG1GC 手动指定使用 G1 收集器执行内存回收任务。
记、最终标记、 筛选回收,和 CMS 收集器前几步的收集过程很相似:
[外链图片转存中…(img-SKPFPRO2-1731144534453)]
① 初始标记:标记出 GCRoots 直接关联的对象,这个阶段速度较快,需 要停止用户线程,单线程执行。
② 并发标记:从 GCRoot 开始对堆中的对象进行可达新分析,找出存活 对象,这个阶段耗时较长,但可以和用户线程并发执行。
③ 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。
④ 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排 序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包 含垃圾最多的区域.这就是 GarbageFirst 的由来——第一时间清理垃圾最多 的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而 是停顿用户线程。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。
查看JVM垃圾回收器设置垃圾回收器
打印默认垃圾回收器
-XX:+PrintCommandLineFlags-version
JDK 8 默认的垃圾回收器
年轻代使用 Parallel Scavenge GC
老年代使用 Parallel Old GC
打印垃圾回收详细信息
-XX:+PrintGCDetails-version
设置默认垃圾回收器
Serial 回收器
-XX:+UseSerialGC 年轻代使用Serial GC, 老年代使用Serial Old
GC ParNew 回收器
-XX:+UseParNewGC 年轻代使用 ParNew GC,不影响老年代。
CMS回收器
-XX:+UseConcMarkSweepGC 老年代使用 CMS GC。
G1回收器
-XX:+UseG1GC 手动指定使用 G1 收集器执行内存回收任务。
-XX:G1HeapRegionSize 设置每个 Region 的大小