目录
2.1新生代垃圾收集器(ParNew、Parallel Scavenge)
JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机
虚拟机是指通过软件模拟的具有硬件功能的,运行在一个完全隔离的环境中的完成计算机系统,常见的虚拟机:JVM VMware Virtual Box.
JVM与其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
JVM是一台被定制过的现实中不存在的计算机。
JVM包括很多版本,包括:
- Sun Classic VM:时间上最早的商业java虚拟机
- Exact VM:相对于上一代Sun Classic VM,做了改进
- HotSpot VM:现在使用最广泛的虚拟机
- JRockit:专注于服务器端应用,目前在HotSpot的基础上,移植JRockit的优秀特性
- J9 JVM:目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机(在IBM自己的产品上稳定);
- Taobao JVM(国产研发):基于OpenJDK 开发了自己的定制版本AlibabaJDK,简称AJDK。是整个阿里JAVA体系的基石;
JVM运行流程
在执行之前,首先把java代码转换成字节码文件(.class),然后由类加载器将文件加载到运行数据区,因为字节码他只是JVM当中的一套指令规范,底层的操作系统并不能直接去执行,所以必须有执行引擎将字节码文件解析成底层系统指令,然后交由CPU去执行,这个过程就需要调用本地方法接口来实现程序功能。
JVM运行时数据区
运行时数据区是JVM执行java代码代码中不可或缺的重要的一环,运行时数据区主要分为五大部分:堆,方法区,虚拟机栈,本地方法栈,程序计数器
1.方法区(线程共享)
方法区又称为永久代(jdk7),元空间(jdk8),是一个线程共享区域,它主要存储已经被java虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码。
当方法区内存不够时,会抛出OutOfMemoryError异常。
2.堆(线程共享)
程序中所实例化创建出来的对象(new)都存放在堆当中。
我们常见的设置JVM参数,-Xms10m最小启动内存是正对于堆的, -Xmx10m最大运行内存也是正对于堆的。 (ms 是 memory start 简称,mx 是 memory max 的简称)
堆里面会分成新生代和老年代,新生代存储新创建的对象,经过一定GC之后还存活的对象移步至老年代。新生代还分为三个区域:一个Eden + 两个Survivor(s0/s1).
在此只是介绍堆的功能,在垃圾回收相关时会详细介绍其中各个部分工作原理。
3.虚拟机栈/java栈(线程私有)
每一个方法都会分配一块内存作为栈,存放的是方法调用层级,他的生命周期和线程生命周期一样
java栈中每一个栈帧主要包括四个部分:局部变量表,操作栈,动态连接。方法返回地址
这其中每一个方法就是一个栈帧,例如递归,不断的调用方法来解决问题,此时Java栈中就不断的压入栈帧,如果栈的内存空间较小,就会出现栈溢出的错误
4.本地方法栈(线程私有)
本地方法栈主要存储本地方法调用层级
5.程序计数器(线程私有)
保存当前执行指令的行号
程序计数器是一块比较小的内存,可以看作是当前线程所执行的字节码的行号指示器
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空
JVM类加载
类加载也是JVM执行程序的最重要的步骤之一,他的生命周期如下:
总的来说只分为三步:加载 连接 初始化
1.加载
加载是在当前classpath路径下,找到所有字节码文件(.class),读取到内存当中
2.连接
2.1验证
验证时连接的第一步,主要是验证所加载的 .class 文件是否符合规范,或者说是不是我们所需要的字节码文件。
验证的选项有:
- 文件格式的验证
- 字节码的验证
- 符号引用的验证
2.2准备
准备阶段是正式为类变量(类中由static关键字所修饰的变量)分配内存并设置类变量的初始值,这些内存都在方法区中分配
比如此时有这样一行代码:
public static int value = 123;
它是初始化 value 的 int 值为 0,而非 123
2.3解析
解析阶段就是java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
例如, 在Java中, 一个Java类将会编译成一个class文件。在编译时, Java类并不知道所引用的类的实际地址, 因此只能使用符号引用来代替。 比如org.simple.People类引用了org.simple.Language类, 在编译时People类并不知道Language类的实际内存地址, 因此只能使用符号org.simple.Language来表示Language类的地址。解析阶段就是将实际的Language类地址赋给引用。
3.初始化
初始化阶段就是java虚拟机真正的开始执行类中所编写的java代码 ,初始化完成之后,一个真正的对象就会被创建出来。
双亲委派模型
在java虚拟机的角度看,只存在两种类加载器:一种是启动类加载器,这个类加载器是由c++语言编写,属于虚拟机的一部分,另一种就是其他所有类的加载器,这些是由java语言编写,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
1.什么是双亲委派模型
如果一个类加载器收到了加载类的请求,首先不会自己尝试去加载这个类,而是向上请求父类加载器去完成,每一个加载器都是如此工作,一直到最顶层的启动类加载器。从启动类加载器开始,只有当父类无法加载这个请求的,便会将请求发送到子类,子类尝试加载。
- 启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。
- 扩展类加载器。加载 lib/ext 目录下的类。
- 应用程序类加载器:加载我们写的应用程序。
- 自定义类加载器:根据自己的需求定制类加载器
2.双亲委派的优点
- 避免重复加载类:例如A类和B类都继承于C类,当A类加载的时候C类也就加载了,到时候B类加载的时候就可以不用重复加载
- 安全性:使用双亲委派模型可以保证java核心API不被篡改。例如,用户自己定义了object类,他和java提供的类名相同,如果没有双亲委派模型。用户可能直接调用自己定义的object类,会出现不安全问题。
垃圾回收机制
1.死亡对象的判断
1.1引用计数算法
给对象增加一个引用计数器,如果该对象被引用,那么计数器加一,当引用失效时,引用减一,如果任何时刻计数器都为0时,那么此时就可以说这个对象已经死亡。
虽然引用计数法简单,但是它无法解决循环引用的问题。所以java中一般不适用这个算法
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进行垃圾回收
System.gc();
}
}
创建出test1 ,test2,并且让他们的属性互相引用,然后此时将对象test1,test2都置为空,那么test1.instance和test2.instance则会永远访问不到自己所指向的对象,此时就是内存泄漏
1.2 可达性分析算法
思想:通过一系列被称为"GC Roots"对象作为起点,从这些节点开始向下搜索,搜索的路径称为 '引用链' ,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。
可作为GC Roots对象的是:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native方法)引用的对象。
2.垃圾回收算法
2.1标记-清除算法
标记清除算法是最基础的垃圾回收算法啊,首先将要回收的对象全部标记出来,标记完成之后统一回收所标记的对象
标记清除算法的不足:
- 效率不高
- 标记清除之后可用内存空间太过于零散,不利于下次的使用
2.2复制算法
复制算法就是将内存空间二等分,然后一半用于存储使用,另一半不用来存储对象,是用来清除死亡对象。前半部分需要回收死亡对象,此时会把还存活的对象统一复制到内存所划分的另一半区域,然后将存有死亡对象的半个内存直接清空。
虽然这个算法相比于标记清楚算法来说,效率要高很多,但是它每次只是利用内存空间的1/2,所以相对来说,内存的利用率还是比较低。复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
2.3标记-整理算法
标记整理算法和标记清除算法的过程一样,不同的是当把所有的死亡对象标记出来,但是后续不是全部清除,而是让存活的对象都往前移动,是所有的存活对象连续,相应的未使用的内存空间也就连续起来,为下一次使用打好基础
2.4分代算法
分代算法和前三种不一样,并不是一个具体的算法,而是将堆内存划分未新生代,和老年代,不同的区域使用不同的算法,高效的执行垃圾回收。
在新生代中,每次垃圾回收都有大量的对象死去,只有少量存活,因此我们采用复制算法,而老年代的对象存活的时间较长,存活率高,没有额外额内存空间对他进行担保,就必须采用标记-清除或者标记-整理算法。
新生代:
新生代中又划分为几个不同区域,一个Eden + 两个Survivor(s0/s1),Eden区主要存储新创建出来的对象,当Eden内存满了之后就开始GC,将死亡的对象清除,将存活的对象放在S0区,然后开始第二轮的GC,,将存活对象放入S0,然后S0 ,S1交换位置,如此循环工作。
老年代:
在新生代每次GC之后还存活的对象,那么它存活的年龄就加一,在新生代存活年龄到达默认值(一般默认为15次)之后,对象会转移到老年代。
垃圾收集器
上面我们讲述了判断死亡的算法和垃圾回收的算法,这些都是理论,那么垃圾收集器就是对这些算法的具体实现。
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
这些是垃圾收集器的不同版本,又分为不同分代的回收器,如果两个回收器存在连线,那么就说明两者可以搭配使用。此处需要先明白三个概念:
- 并行:指多条垃圾收集线程并行工作,用户线程仍处于等待状态
- 并发:指用户线程与垃圾收集线程同时工作,用户程序继续运行,而垃圾回收程序运行在另一个CPU
- 吞吐量:就是CPU运行用户代码时间和CPU总消耗时间的比值
1.串行垃圾收集器
为单线程环境设计的只有一个线程的垃圾收集器,工作时会暂停所有的用户线程
1.1新生代的垃圾收集器(Serial)
这个收集器是一个古老的单线程收集器,他的单线程并不单单只是一个线程工作,更多的是说明是他在垃圾回收的时候,必须停止其他工作线程,直到他收集结束(Stop The World,译为停止整个程序,简称 STW)。
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
Serial的优势在于简单高效,没有线程交互的开销,只是进行垃圾回收,可以获得很高的单线程收集效率。
1.2老年代垃圾收集器(Serial Old)
Serial Old 是 Serial的老年代版本,同样是单线程工作,使用的是标记整理算法这个收集器也主要是运行在Client默认的老年代垃圾收集器,在老年代中,也充当CMS收集器的后备垃圾收集方案。
2.并行垃圾收集器
多个垃圾收集器并行工作,此时用户线程是暂时停止的。
2.1新生代垃圾收集器(ParNew、Parallel Scavenge)
ParNew :实际上就是Serial的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包
括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
ParNew是运行在多线程下的新生代内存当中
Parallel Scavenge:是一个新生代的垃圾收集器,使用的是复制算法,又是一个多线程版本
2.2老年代垃圾收集器(Parallel Old)
Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
3.并发清除回收器(CMS)
CMS收集器是一种获取最短回收停顿时间的收集器,这类应用重视服务的响应速度,希望STW时间较小,提供给用户良好的体验。
CMS收集器是基于"标记-清除"算法的,但是其步骤更加复杂一些:
- 初始标记:只是标记一下GC Roots能关联的对象,速度很快,但是仍要暂停所有工作线程
- 并发标记:进行GC Roots的跟踪过程,和用户线程一起执行,不需要暂停工作线程,主要标记过程,标记全部对象
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有用户线程。
- 并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程,基于标记结果,直接清除对象。
CMS的优点: 并发收集停顿低。
CMS的缺点:并发执行对CPU资源消耗较大,标记算法会产生碎片
4.G1垃圾收集器(唯一一款全区域的垃圾回收器)
G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的
region块,然后并行的对其进行垃圾回收。
G1垃圾回收器在清除实例所占的内存之后,还会内存压缩。
一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。 G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象
新生代垃圾收集:
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代垃圾收集 :
- 初始标记:只是标记GC Roots能直接关联的对象
- 并发标记:进项GC Roots跟踪的过程
- 最终标记:修正并发标记期间,因程序运行而导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进项价值最大化的回收
垃圾回收器不断升级,最终目标就是减少STW时间。
总结:一个对象的起始和终止
一个对象的一生:我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。 有一天Eden区中的人实在是太多了,我就被迫去了 Survivor区的 “From” 区(S0 区),自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 “From” 区,有时候在 Survivor 的 “To” 区(S1 区),居无定所。 直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。 于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。 在老年代里,我生活了很多年(每次GC加一岁)然后被回收了。