JVM

在这里插入图片描述在这里插入图片描述

程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。

Java 虚拟机栈(描述的是Java方法执行的动态内存模型)
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈(压栈和弹栈的指令)、常量池引用(常量池中保存中所有类型使用到的类型、字段、方法的字符引用)等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
栈帧:每个方法执行,都会创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表,操作数栈,动态链接,方法出口等。
局部变量表:1.存放编译器可知的各种基本数据类型,引用类型,returnAddress类型。2.局部变量表的内存空间在编译器完成分配,当进入一个方法时,这个方法需要在帧分配多沙内存是固定的,在方法运行期间是不会改变局部变量表的大小。

在这里插入图片描述

本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。(就是使用非Java语言实现的方法,但是通常我们指的一般为C或者C++,因此这个栈也有着C栈这一称号)


所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”),包括成员变量也是放在堆中。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:新生代(Young Generation)老年代(Old Generation)。
堆不需要连续内存,并且可以动态增加其内存(所以运行速率比栈慢),增加失败会抛出 OutOfMemoryError 异常。
在JDK1.7之后,永久代的静态变量和运行时常量池被合并到了堆中。

方法区
用于存放已被加载的类信息、常量、静态变量(现在静态变量是不是已经放在堆中了?)、即时编译器编译后的代码等数据。即(存放着类的版本,字段,方法,接口和常量池
和堆一样不需要连续的内存,并且可以动态扩展(速率慢),动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
在这里插入图片描述

在这里插入图片描述
运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。

直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

JVM 的运行原理

public class JVMCase {

	// 常量
	public final static String MAN_SEX_TYPE = "man";

	// 静态变量
	public static String WOMAN_SEX_TYPE = "woman";

	public static void main(String[] args) {
		
		Student stu = new Student();
		stu.setName("nick");
		stu.setSexType(MAN_SEX_TYPE);
		stu.setAge(20);
		
		JVMCase jvmcase = new JVMCase();
		
		// 调用静态方法
		print(stu);
		// 调用非静态方法
		jvmcase.sayHello(stu);
	}


	// 常规静态方法
	public static void print(Student stu) {
		System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge()); 
	}


	// 非静态方法
	public void sayHello(Student stu) {
		System.out.println(stu.getName() + "say: hello"); 
	}
}

class Student{
	String name;
	String sexType;
	int age;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	
	public String getSexType() {
		return sexType;
	}
	public void setSexType(String sexType) {
		this.sexType = sexType;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

Java运行以上代码之后,JVM 的整个处理过程如下
1.JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。
2.JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。
3.class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值
处理。
在这里插入图片描述
4.完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器 方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 ()方法。
在这里插入图片描述
5. 执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 student 对象,对象引用 student 就存放在栈中。
在这里插入图片描述
6. 此时再次创建一个 JVMCase 对象,调用 sayHello 非静态方法,sayHello 方法属于对象 JVMCase,此时 sayHello 方法入栈,并通过栈中的student 引用调用堆中的 Student 对象;之后,调用静态方法 print,print 静态方法属于 JVMCase 类,是从静态方法中获取,之后放入到栈中,也是通过student 引用调用堆中的 student 对象。
在这里插入图片描述

JVM中的堆栈和数据结构中的堆栈的区别。
内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。

垃圾收集
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失。

判断一个对象是否可被回收

  1. 引用计数算法(现在已废除)
    为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
    实现简单,判定效率也很高,JVM没有选用最主要的原因是很难解决对象之间相互循环应用的问题。A.instance = B;B.instance = A;

  2. 可达性分析算法
    以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。如果从一个对象没有到达根对象的路径,或者说从根对象开始无法引用到该对象,该对象就是不可达的。
    Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
    虚拟机栈中局部变量表中引用的对象
    本地方法栈中 JNI 中引用的对象
    方法区中类静态属性引用的对象
    方法区中的常量引用的对象
    即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已经执行过finalize(),虚拟机将会认为“没有必要执行”。

  3. 方法区的回收
    因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。(JDK1.8已经不用永久代,把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。)
    主要是对常量池的回收和对类的卸载。
    为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
    类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
    该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
    加载该类的 ClassLoader 已经被回收
    该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

  4. finalize()
    类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
    当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救(应该就是因为调用了finalize()方法实现自救)。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。

引用类型

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。

Java 提供了四种强度不同的引用类型。

  1. 强引用
    被强引用关联的对象不会被回收。
    使用 new 一个新对象的方式来创建强引用。
    Object obj = new Object();
  2. 软引用
    被软引用关联的对象只有在内存不够的情况下才会被回收
    使用 SoftReference 类来创建软引用。
    Object obj = new Object();
    SoftReference sf = new SoftReference(obj);
    obj = null; // 使对象只被软引用关联
  3. 弱引用
    被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
    使用 WeakReference 类来创建弱引用。
    Object obj = new Object();
    WeakReference wf = new WeakReference(obj);
    obj = null;
  4. 虚引用
    又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
    为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知
    使用 PhantomReference 来创建虚引用。
    Object obj = new Object();
    PhantomReference pf = new PhantomReference(obj);
    obj = null;

如何回收这些对象

自动性
Java 提供了一个系统级的线程来跟踪每一块分配出去的内存空间,当 JVM 处于空闲循环时,垃圾回收器线程会自动检查每一块分配出去的内存空间,然后自动回收每一块空闲的内存块。

不可预期性
我们很难确定一个没有被引用的对象是不是会被立刻回收掉,因为有可能当程序结束后,这个对象仍在内存中。

垃圾收集算法

  1. 标记 - 清除
    在这里插入图片描述
    标记要回收的对象,然后清除。
    不足:
    标记和清除过程效率都不高
    会产生大量不连续的内存碎片,导致无法给大对象分配内存。
  2. 标记 - 整理
    在这里插入图片描述在这里插入图片描述
    让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
    3.复制
    将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
    主要不足是只使用了内存的一半。
    什么时候触发:当jvm无法为一个新的对象分配空间时会触发Minor GC,比如当Eden区满了。

现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

1.Eden区
Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。
2.Survival from to
Survival区与Eden区相同都在Java堆的年轻代。Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survivalfrom区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代。
3.年老代
年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。

堆=新生代+老年代,不包括永久代(方法区)。

  1. 分代收集
    现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
    一般将堆分为新生代和老年代。
    新生代使用:复制算法
    老年代使用:标记 - 清除 或者 标记 - 整理 算法

垃圾收集器
在这里插入图片描述
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

单线程与多线程:单线程(一个线程只能被一个cpu执行)指的是垃圾收集器只使用一个线程,而多线程使用多个线程;

串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

  1. Serial 收集器
    在这里插入图片描述
    Serial 翻译为串行,也就是说它以串行的方式执行。
    它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
    它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。(垃圾容量不是太大,执行速度可以接受)
    但是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

  2. ParNew 收集器
    在这里插入图片描述
    它是 Serial 收集器的多线程版本。
    它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。

  3. Parallel Scavenge 收集器
    与 ParNew 一样是多线程收集器。
    其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
    停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
    缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
    可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

  4. Serial Old 收集器
    在这里插入图片描述

是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。(但是不好,因为Parallel Scavenge 时基于吞吐量优先的收集器,适合在Server服务端应用,Serial Old会拖慢Parallel Scavenge 的速度。)
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

  1. Parallel Old 收集器
    在这里插入图片描述

是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

  1. CMS 收集器
    CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法
    分为以下四个流程:
    初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
    并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
    重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
    并发清除:不需要停顿。
    在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
    具有以下缺点:
    吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
    无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
    标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

  2. G1 收集器
    G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
    堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
    在这里插入图片描述在这里插入图片描述

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
在这里插入图片描述
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
初始标记
并发标记
最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

通过 JVM 工具查询当前 JVM 使用的垃圾回收器类型,首先通过 ps 命令查询出经常 ID,再通过 jmap -heap ID 查询出 JVM 的配置信息。

三、内存分配与回收策略

Minor GC 和 Full GC
Minor GC:回收新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存分配策略

  1. 对象优先在 Eden 分配
    大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

  2. 大对象直接进入老年代
    大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
    经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
    可以通过参数–XX:PetenureSizeThreshold 设置直接被分配到老年代的最大对象,如果分配的对象超过了设置的阈值,对象就会直接被分配到老年代。这样做的好处就是可以减少新生代的垃圾回收。

  3. 长期存活的对象进入老年代
    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。(大概是15)

  4. 动态对象年龄判定
    虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

  5. 空间分配担保
    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
    如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

Java虚拟机每次使用新生代中的Eden和其中一块Survivor(From),在经过一次Minor GC后,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上(这里使用的复制算法进行GC),最后清理掉Eden和刚才用过的Survivor(From)空间。将此时在Survivor空间存活下来的对象的年龄设置为1,以后这些对象每在Survivor区熬过一次GC,它们的年龄就加1,当对象年龄达到某个年龄(默认值为15)时,就会把它们移到老年代中。
在新生代中进行GC时,有可能遇到另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代;
在这里插入图片描述

Minor GC的触发条件
其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC
单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。

Full GC 的触发条件
1 调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2 老年代空间不足:老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
3 空间分配担保失败:使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
4 JDK 1.7 及以前的永久代空间不足:在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5 Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

GC 性能衡量指标

吞吐量:
这里的吞吐量是指应用程序所花费的时间和系统总运行间的比值。我们可以按照这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时 +GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。

停顿时间:
指垃圾回收器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾回收器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾回收器,系统的吞吐量也很可能会降低。

垃圾回收频率:
通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

查看 & 分析 GC 日志

-XX:+PrintGC 输出 GC 日志
-XX:+PrintGCDetails 输出 GC 的详细日志
-XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径

打印GC日志
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs

JVM 不仅承担了 Java 字节码的分析(JIT compiler)和执行(Runtime),同时也内置了自动内存分配管理机制。

前端编译:.java 文件被编译成 .class 文件的过程(javac)
运行时编译:JIT 或解释器会将字节码转换成机器码

在这里插入图片描述

四、类加载机制
在这里插入图片描述

类是在运行期间第一次使用时动态加载的,而不是一次性加载。因为如果一次性加载,那么会占用很多的内存。
类的生命周期为以下七个阶段

类加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。
1 加载:加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
通过类的完全限定名称获取定义该类的二进制字节流。(二进制字节流: 从 ZIP 包读取,从网络中获取,运行时计算生成,如动态代理技术)
将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
2 验证:确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3 准备:类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

 public static int value = 123;

如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。

public static final int value = 123;

4 解析:将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
5 初始化初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() (clinit是类的构造方法,init是类对象的构造方法)方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
在准备阶段,已经为类变量分配了系统所需的初始值,并且在初始化阶段,根据程序员通过程序进行的主观计划来初始化类变量和其他资源。
<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。

1.	public class Test {
2.	    static {
3.	        i = 0;                // 给变量赋值可以正常编译通过
4.	        System.out.print(i);  // 这句编译器会提示“非法向前引用”
5.	    }
6.	    static int i = 1;
7.	}

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

类初始化时机
1.主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
(1)遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。
(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
(5)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为REFgetStatic, REFputStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
2.被动引用
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
(1)通过子类引用父类的静态字段,不会导致子类初始化(System.out.println(SubClass.value); //value 字段在 SuperClass 中定义)
(2)通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。(SuperClass[] sca = new SuperClass[10]);
(3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。

最初,虚拟机中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。

了提高热点代码的执行效率,在运行时,即时编译器(JIT)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。

JVM 中的 JIT 会在运行时有选择性地将运行次数较多的方法编译成二进制代码,直接运行在底层硬件上。

即时编译器类型

在 HotSpot 虚拟机中,内置了两个 JIT,分别为 C1 编译器和 C2 编译器。
C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序。
C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。
根据各自的适配性,这两种即时编译也被称为 Client Compiler 和 Server Compiler。

Java7 引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们也可以通过参数“-client”“-server” 强制指定拟机的即时编译模式。(Java8默认开启,参数无作用)

开启C2:关闭分层编译(-XX:-TieredCompilation)
开启C1:XX:TieredStopAtLevel=1

分层编译将 JVM 的执行状态分为了 5 个层次:
第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;
第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;
第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;
第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;
第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

到了 Java9,AOT 编译器被引入。和 JIT 不同,AOT 是在程序运行前进行的静态编译,这样就可以避免运行时的编译消耗和内存消耗,且 .class 文件通过AOT 编译器是可以编译成 .so 的二进制文件的。

到了 Java10,一个新的 JIT 编译器 Graal 被引入。Graal 是一个以 Java 为主要编程语言、面向 Java bytecode 的编译器。与用 C++ 实现的 C1 和 C2 相比,它的模块化更加明显,也更容易维护。Graal 既可以作为动态编译器,在运行时编译热点方法;也可以作为静态编译器,实现 AOT 编译。

热点探测

在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法” 。
虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。

方法调用计数器
用于统计方法被调用的次数,方法调用计数器的默认阈值在C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通过 -XX: CompileThreshold

回边计数器
用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,C1 默认为 13995,C2 默认为 10700,可通过-XX: OnStackReplacePercentage=N来设置。
建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译。在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语言。

编译优化技术

1. 方法内联
调用一个方法通常要经历压栈和出栈。调用方法是将程序执行顺序转移到存储该方法的内存地址,将方法的内容执行完后,再返回到执行该方法前的位置。
这种执行操作要求在执行前保护现场并记忆执行的地址,,执行后要恢复现场,并按原来保存的地址继续执行。因此,方法调用会产生一定的时间和空间方面的开销。
方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。

JVM 会自动识别热点方法,并对它们使用方法内联进行优化。
通过 -XX:CompileThreshold 来设置热点方方法的阈值。
设置阈值的大小:-XX:MaxFreqInlineSize=N 默认在325字节
不是经常执行的代码,默认在35字节以下进行内联。

-XX:+PrintCompilation // 在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions // 解锁对 JVM 进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对 JVM 进行诊断
-XX:+PrintInlining // 将内联方法打印出来

热点方法的优化
1.通过设置 JVM 参数来减小热点阈值或增加方法体阈值,以便更多的方法可以进行内联,但这种方法意味着需要占用更多地内存;
2.在编程中,避免在一个方法中写大量代码,习惯使用小方法体;
3.尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类型检查。

逃逸分析
逃逸分析(Escape Analysis)是判断一个对象是否被外部方法引用或外部线程访问的分析技术,编译器会根据逃逸分析的结果对代码进行优化。

栈上分配
在 Java 中默认创建一个对象是在堆中分配内存的,而当堆内存中的对象不再使用时,则需要通过垃圾回收机制回收,这个过程相对分配在栈中的对象的创建和销毁来说,更消耗时间和性能。这个时候,逃逸分析如果发现一个对象只在方法中使用,就会将对象分配在栈上。

锁消除
如果在局部方法中使用了线程安全容器或者锁,因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。

标量替换
逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换。

-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8 默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析

-XX:+EliminateLocks 开启锁消除(jdk1.8 默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除

-XX:+EliminateAllocations 开启标量替换(jdk1.8 默认开启,其它版本未测试)
-XX:-EliminateAllocations 关闭就可以了

类加载器分类
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher ExtClassLoader)实现的。它负责将系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型
先父类加载,父类加载不了再自己加载。
好处是使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如: java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

refvar是Java中9大基本数据类型的一种,是面向对象世界中的引用变量,也叫引用句柄。
作为一个引用变量,不管它是指向包装类,集合类,字符串类,还是自定义类,refvar均占用4B的空间。
无论refobj是对么小的对象,最小占用的存储空间是12B(用于存储基本信息,称为对象头),但由于存储空间分配必须是8B的倍数,所以初始分配的空间至少是16B。

在这里插入图片描述
1.对象头
占用12个字节,存储内容包括对象标记和类元信息。
对象标记在64位JVM上占用8个字节,称为“Mark word”.
类元信息存储的是指向它的类元数据的首地址,占用4个字节。
2.实例数据
存储本类对象的实例成员和所有可见的父类成员变量。
3.对齐填充
对象的存储空间分配单位是8个字节,如果一个占用大小为16个字节的对象,增加一个成员变量byte类型,此时需要占用17个字节,但是也会分配24个字节进行对齐填充操作。

JVM工具:"/替换为空格,CSDN显式不了"
jps:虚拟机进程状况工具
jps [options]/[hostid]
jstat:虚拟机统计信息监视工具
jstat [option vmid [interval \ [s|ms]\ [count]] ]
jinfo: Java配置信息工具
jinfo [option] pid
jmap:Java内存映像工具
jmap [option] vmid
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具
jstack [option] vmid

Class类文件的结构:
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里。实际上它并不一定以磁盘文件的形式存在。
Class文件是一组以8位字节为基础单位的二进制流。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
常量池
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),从1开始计算。
常量池主要存放两大类常量:字面量和符号引用。
字面量:文本字符串,final类型常量值
符号引用:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
访问标志:
这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
类索引,父类索引和接口索引集合
Class文件中由这三项数据来确定这个类的继承关系。
类索引用于确定这个类的全限定名。
父类索引用于确定这个类的父类的全限定名。除了Object类外,所有Java类的父类索引都不为0.
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按Implements语句后的顺序从左到右排列在接口索引集合中。

常用的 JVM 调优的参数

-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。

查看 JVM 堆内存分配

java -XX:+PrintFlagsFinal -version | grep HeapSize 
jmap -heap 17284

在这里插入图片描述

在这里插入图片描述
在 JDK1.7 中,默认情况下年轻代和老年代的比例是 1:2,我们可以通过**–XX:NewRatio** 重置该配置项。
年轻代中的 Eden 和 To Survivor、From Survivor 的比例是 8:1:1,我们可以通过 -XX:SurvivorRatio 重置该配置项。

JDK1.7 中如果开启了 -XX:+UseAdaptiveSizePolicy 配置项,JVM 将会动态调整 Java 堆中各个区域的大小以及进入老年代的年龄,–XX:NewRatio 和 -XX:SurvivorRatio 将会失效,而 JDK1.8 是默认开启 -XX:+UseAdaptiveSizePolicy 配置项的。

JVM调优方向

1.GC频率
2.内存
3.吞吐量
4.延时

具体调优方法:
1.调整堆内存空间减少Full GC:

java -jar -Xms4g -Xmx4g heapTest-0.0.1-SNAPSHOT.jar

2.调整年轻代减少MinorGC

java -jar -Xms4g -Xmx4g -Xmn3g heapTest-0.0.1-SNAPSHOT.jar

3.设置Eden、Survivor区比例
默认开启AdaptieSizePolicy,动态调整Eden和Survivor区比例(根据GC过程中统计的GC时间、吞吐量、内存占用量)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值