JVM介绍
jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。
jvm包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。
JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
JDK、JRE、JVM三者关系
JRE(Java Runtime Environment),也就是java平台。所有的java程序都要在JRE环境下才能运行。
JDK(Java Development Kit),是开发者用来编译、调试程序用的开发包。JDK也是JAVA程序需要在JRE上运行。
JVM(Java Virtual Machine),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
JVM原理及执行程序的过程
JVM原理
JVM是Java核心和基础,在Java编译器和os平台之间的虚拟处理器。它可以在上面执行Java的字节码程序。Java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台的机器码,通过特定平台运行。
代码编译执行过程
源码编译:通过Java源码编译器将Java代码编译成JVM字节码(.class文件)。
类加载:通过ClassLoader及其子类来完成JVM的类加载。
类执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行。
JVM执行程序的过程如下图所示:
.java文件
即使用Java语言编写的程序
.class文件
* Java程序经过Java编译器进行编译,生成.class文件,java编译一个类时,如果这个类所依赖的类还没有被编译,
* 编译器会自动编译所依赖的类,然后引用。如果java编译器在指定的目录下找不到该类所依赖的类的 .class文件或者 .java源文件,就会报Can't found symbol
的异常错误。
* 编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是代码出现过的(常量、类名、成员变量等)以及符号引用(类引用、方法引用,成员变量引用等);方法字节码中放的是各个方法的字节码。
类加载器
字节码校验器
解释器
JIT代码生成器
JVM类的生命周期
加载-》连接-》初始化-》使用-》卸载
参考链接
JVM内存结构
JVM内存结构图
(1)分类
总共有:堆、方法区、虚拟机栈,本地方法栈、程序计数器,直接内存(不属于JVM内存的剩余机器内存)
堆内存(Heap)
* 堆内存是Java虚拟机所管理的内存中最大的一块区域。
* Java堆被所有线程所共享。
* 在虚拟机启动时创建,主要用来存放对象实例。
* Java堆只要求逻辑上连续即可。
* 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
此外,Java堆也是垃圾收集器管理的主要区域,所以也叫“GC”堆。由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:
“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。
新生代(Young Generation)
一个Eden区,两个Survivor区(s0,s1,也称为from,to区域)。大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。
老年代(Old Generation)
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆,包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。
永久代(Perm Generation)
主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
方法区(Method Area 或者 Non-Heap非堆 或者 PermGen持久代)
* 方法区存储类信息、常量、静态变量、静态代码块、运行时常量池 等数据,是线程共享的区域。(类信息:类的版本、字段、方法、接口、构造函数等描述信息 )
* 默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize
和 -XX:MaxPermSize
参数限制方法区的大小。
* 根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
关于普通变量,静态变量,普通代码块,静态代码块的存储位置
静态成员(包括静态变量和静态代码块)存储在方法区中;
非静态成员(包括成员变量,成员方法,构造方法,构造代码块,普通代码块)存储在堆内存中。
补充:字符串在常量池中(jdk1.7后常量池不在方法区中,而是在堆中)。
栈(Thread)
* 主要用于方法的执行。
* java栈、本地方法栈、程序计数器是运行时线程私有的内存区域。
Java虚拟机栈
* Java虚拟机栈的生命周期与线程相同。
* 虚拟机栈描述的是Java方法执行的内存模型。
每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、引用类型和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的。
JVM对虚拟机栈区域两种异常状况的规定:
1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2)如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
异常介绍:
StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
本地方法栈
Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。
java虚拟机的多线程是通过线程轮流切换并分配CPU的时间片的方式实现的,因此在任何时刻一个处理器(如果是多核处理器,则只是一个核)都只会处理一个线程,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,因此这类内存区域为“线程私有”的内存。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令。各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)
程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
作用:
(1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制.
(2)用于多线程切换CPU执行权的情况下,保护现场和获得执行权时恢复现场。
直接内存
直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存。
由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。
(2)控制参数
参数 | 含义 |
---|---|
-Xms | 设置堆的最小空间大小。 |
-Xmx | 设置堆的最大空间大小。 |
-XX:NewSize | 设置新生代最小空间大小。 |
-XX:MaxNewSize | 设置新生代最大空间大小。 |
-XX:PermSize | 设置非堆区最小空间大小。 |
mSize | 设置非堆区最大空间大小。 |
-Xss | 设置每个线程的堆栈大小。 |
注:没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。有以下公式:
老年代空间大小 = 堆空间大小 - 年轻代空间大小
JVM内存分配过程
1)JVM 会试图为相关Java对象在Eden中初始化一块内存区域。
2)当Eden空间足够时,内存申请结束;否则到下一步。
3)JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收)。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。
4)Survivor区被用来作为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区。
5)当Old区空间不够时,JVM 会在Old区进行完全的垃圾收集(0级)。
6)完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory”错误。
类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。
一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
根加载器(bootstrap class loader)
它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。
扩展类加载器 (extensions class loader)
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
系统类加载器(system class loader)
被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
类加载器加载类的一般步骤
类加载过程
加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段:
验证
检查加载的class文件的正确性和安全性
验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式验证
主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。
例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
元数据验证
对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证
最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证
主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
准备
为类变量分配存储空间,并设置类变量初始值。类变量随类型信息存放在方法区中,生命周期很长,使用不当容易造成内存泄漏。
注:类变量就是static变量;初始值指的是类变量类型的默认值而不是实际要赋的值
解析(可选)
jvm将常量池内的符号引用转换为直接引用
符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
初始化
执行类变量赋值和静态代码块
准备阶段和初始化阶段的区别
例,如果类中有语句:private static int a = 10
,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
使用
卸载
类加载时机
类加载机制
全盘负责
所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派
所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
双亲委派模型
工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
优势
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
JVM垃圾回收机制(GC)
简介
垃圾回收机制是由垃圾收集器Garbage Collection GC来实现的,GC是后台的守护进程。它的特别之处是它是一个低优先级进程,但是可以根据内存的使用情况动态的调整他的优先级。因此,它是在内存中低到一定限度时才会自动运行,从而实现对内存的回收。这就是垃圾回收的时间不确定的原因。
流程
(1)判断哪些对象是垃圾
通过引用计数法、可达性分析法来进行分析;
引用计数法:通过对引用的遍历,找到对应的实例,让对应的实例计数加 1 ,如果引用取消,或者指向null,实例的引用减 1 。把找到的引用都遍历一遍之后,如果发现有对象实例的计数是0。那么这个对象 就是垃圾对象了。在通过垃圾回收算法对其进行 回收即可。缺点:无法回收互相引用的实例。
可达性分析法:这个算法类似于树的遍历,简单来说,按照一定的规则说明那些可以作为一个根节点(GC root),然后以这些根节点去访问其引用的对象,被访问的对象又会有其他对象的引用。这个路径称作引用链,但凡是在引用链上的对象,都是可用的。注意,引用连的起始点都是GC root 。虽然有其他对象存在类似于引用链的结构,但是,起始点不是GC root的那些,都是垃圾,可以被回收的。
一般情况下,都是使用的可达性分析法去查找垃圾类实例。
可以作为GC Root的对象:
栈中的引用对象,如
A a = new A();
方法区中的类静态属性引用的对象,如B类中的成员变量
private static A a;
方法区中常量引用的对象,如B类中的成员变量
private static final A a;
栈中JNI中引用的对象
(2)垃圾回收算法
有:标记-清除、复制算法、标记整理、分代收集算法
标记清除
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。遍历了两次内存空间(第一次标记,第二次清除)。
优缺点:
标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
复制算法
将内存分为四块,新生代(Eden),幸存代(s0,s1),老年代。有五种内存分配策略。类的升级流程就是Eden -> Survivor -> 老年代。
算法流程:
(1)首次GC时,将Eden中可以使用的对象移动到s1(to),将Eden和另一块s0(from)中的内存全部清除。
(2)将新生成的类实例优先分配到Eden,分配不下时,放到s1。进行GC时,将s1中满足一定条件(如对象年龄达到一个阈值)的对象分配到老年代中。将本次GC存活下来的分配到s0中,清除Eden和s1。依次循环
缺点:(浪费空间)Survivor中每次都会浪费一个Survivor的内存没有使用,所以为了减少浪费,一般将Eden的内存扩大,Survivor的内存设置的小一点,例如默认设置的是8:1:1。
标记整理
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往空闲空间移动,并更新对应的指针。
优缺点:
(1)标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,解决了内存碎片的问题。
(2)遍历了两次内存空间,而且进行的对象的移动,效率较以上两种算法低。
分代回收
当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。如:
对象迁移老年代的条件:每次GC时,当前存活的对象,“年龄”+1,默认是当“年龄”达到16的时候就会放到老年代里。
GC的作用域:方法区和堆;主要是堆。
GC回收时间
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
a) 年老代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;
JVM内存分配原则
(1)对象优先分配到Eden区域
(2)大对象直接分配到老年代(大对象指对象中有很大的数组或者字符串)
(3)长时间存活的对象存放在老年代
(4)动态对象年龄判定:JVM并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才可以进入老年代,如果Survivor空间中年龄相同的所有对象的总空间>=本servivor中的一半,那么年龄>=本年龄的对象可以直接进入老年区;
(5)空间分配原则:简单来说,就是在发生Minor GC(在新生代进行GC)情况下,为了防止发生在Minor GC后,Eden有大量存活的对象,导致survivor不能全部存入,这时需要老年代去担保,把这些对象放入老年代,但是要确保老年要存的下。
1)在发生Minor GC之前,检查老年区的可用的连续空间是否是大于新生代(Eden)的所有对象的总空间,如果是,直接全部晋升老年代,保证Minor GC的安全;
2)如果不行,就检查HandlePromotionFailure(可以手工设定)参数是否允许担保失败,允许的话,直接分配。不能的话,发生一次full GC(或者是Major GC 在老年代进行GC)。
为什么不直接进行full GC ,因为速度慢。而且经常GC也效果不大,因为老年代都是一些长期存活的对象。
垃圾回收器
CMS、G1(G1是目前最好的收集器)
CMS垃圾收集器
CMS(Concurrent Mark Sweep)一种以获得最短停顿时间为目标的收集器,非常适用B/S系统。CMS 已经在 JDK 9 中被标记为废弃(deprecated)。
收集过程
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,需要暂停所有的工作线程。
并发标记
从 GC Roots 开始对堆进行可达性分析,找出活对象,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
优缺点
(1)对CPU资源要求敏感
CMS 回收器过分依赖于多线程环境,默认情况下,开启的线程数为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS 对用户本身的操作的影响将会很大,因为要分出一半的运算能力去执行回收器线程。
(2)CMS无法清除浮动垃圾
浮动垃圾指的是CMS清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除。
(3)CMS垃圾回收会产生大量空间碎片
CMS 使用的是标记-清除算法,所有在垃圾回收的时候回产生大量的空间碎片。
指定收集器:
-XX:+UseConcMarkSweepGC
G1垃圾收集器
G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。
G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。
运行过程
初始标记
标记 GC Roots 直接关联的对象,需要暂停所有工作线程 。
并发标记
从 GC Roots 开始对堆进行可达性分析,找出活对象。
重新标记
重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。
筛选回收
首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。这个阶段可以与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的。
使用算法: 复制、标记-整理
指定收集器: -XX:+UseG1GC(JDK 7u4 版本后可用)
JVM报错
OOM报错的四种情况
Java堆溢出:heap
Java堆内存主要用来存放运行过程中所以的对象,该区域OOM异常一般会有如下错误信息;
java.lang.OutofMemoryError:Java heap space
此类错误一般通过Eclipse Memory Analyzer分析OOM时dump的内存快照就能分析出来,到底是由于程序原因导致的内存泄露,还是由于没有估计好JVM内存的大小而导致的内存溢出。
栈溢出:stack
栈用来存储线程的局部变量表、操作数栈、动态链接、方法出口等信息。如果请求栈的深度不足时抛出的错误会包含类似下面的信息:
java.lang.StackOverflowError另外,由于每个线程占的内存大概为1M,因此线程的创建也需要内存空间。操作系统可用内存-Xmx-MaxPermSize即是栈可用的内存,如果申请创建的线程比较多超过剩余内存的时候,也会抛出如下类似错误:
java.lang.OutofMemoryError: unable to create new native thread
运行时常量溢出
运行时常量保存在方法区,存放的主要是编译器生成的各种字面量和符号引用,但是运行期间也可能将新的常量放入池中,比如String类的intern方法。
如果该区域OOM,错误结果会包含类似下面的信息:
java.lang.OutofMemoryError: PermGen space
方法区溢出
方法区主要存储被虚拟机加载的类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。理论上在JVM启动后该区域大小应该比较稳定,但是目前很多框架,比如Spring和Hibernate等在运行过程中都会动态生成类,因此也存在OOM的风险。
如果该区域OOM,错误结果会包含类似下面的信息:java.lang.OutofMemoryError: PermGen space
强引用、软引用、弱引用、虚引用
强引用——基本是用到的95%都是强引用
当内存不足 jvm开始垃圾回收 对于强引用的对象 就算是出现oom也不会对该对象进行回收 死也不收
在java中最常见的就是强引用 把一个对象赋给一个引用变量 这个引用变量就是强引用 当一个对象被强引用变量引用时,他处于不可达状态 他是不可能被垃圾回收的 这也是强引用造成内存泄漏的主要原因之一
对于一个普通的对象 如果没有其他的引用关系 只要超过引用的作用域或者将相应的强引用赋值为null一般认为就是可以被回收的。
软引用——当内存充足时不会回收 内存不足时回收
弱引用——不管内存是否够用 只要有gc一定回收
弱引用需要用java.lang.ref.WeakReference类来实现 他比软引用的生存期更短
对于只有弱引用的对象来说 只要垃圾回收机制一运行 不管JVM的内存空间是否足够 都会回收该对象占用的内存
虚引用
需要java.lang.ref.PhantomReference类来实现,与其他引用都不同,虚引用并不会决定对象的生命周期。
软引用和弱引用的区别
软引用一般用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
弱引用对象相比较软引用,要更加无用一些,它拥有更短的生命周期。当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
适用场景补充:
假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取则会严重影响性能,如果一次性全部加载到内存中又可能造成内存溢出,此时使用软引用来解决这个问题。
设计思路是:用一个HashMap来保存图片的路径和相应图片对象的软引用之间的映射关系 在内存不足时 JVM会自动回收这些内存图片对象所占用的空间 从而避免oom的问题。
Map
> imageCache=new HashMap >();