复盘-Java-JVM

前言:Java基础知识其实看了很多遍,但知识太多零散没有一个完整的体系,本系列记录Java大部分基础知识,太深的知识不会做过多表述和说明(因为已经涉及到硬件方面的知识),有错误或者表述有误的地方还请大佬指点,一起进步!

基本概念

  Java是编程语言,Java虚拟机简称JVM,是运行Java代码的假想计算机,由一套字节码指令集、一组寄存器(程序计数器)、栈、堆、垃圾回收器和存储方法域组成,这里可以理解为组成JVM内存区域,也就是运行时数据区域。 寄存器:属于计算机硬件,它可以存储二进制代码,一组寄存器由多个触发器组成,一个触发器能存储一位二进制代码,多个触发器能存储n位,寄存器主要分为两大类,一种是基本寄存器,只能并行输入,然后并行输出;另一种是移位寄存器,可以并行输入,然后串行输出,也可以串行输出,然后并行输入,比较灵活,是基于移位脉冲的作用下对二进制代码进行左移或者右移,而在JVM虚拟机中,这里的寄存器起到的作用就是程序计数器,也就是计数寄存器。

运行过程

  由我们编写的java代码,保存后形成后缀.java的Java源文件,通过Java编译器(Java安装位置bin目录下的javac.exe程序),生产出相应的.Class文件(字节码文件),而字节码文件又通过Java虚拟机中的解释器(Java安装位置bin目录下的java.exe,这里还有一种说法也叫编译器),从源文件到java运行程序需要经过两层转换,如下:

Java源文件(.java) —> 编译器(javac.exe)----->字节码文件(.class)
字节码文件(.class)—> 解释器(java.exe) —> 机器码

在这里插入图片描述

Java之所以具备跨平台的特性,就是因为JVM本身就是虚拟机,就像Win系统和Mac系统,要想使用Linux系统可以搭建虚拟机一样,因为每种平台因为硬件芯片、计算方式有细微变化,解释器是不同的,但是利用JVM虚拟机可以在转换为同一种机器码,可以理解为中转,当程序运行,虚拟机进行实例化,启动多个程序就存在多个虚拟机实例。程序与虚拟机实例同生共死,多个虚拟机实例之间不能共享。图下所示就是构建一个完整虚拟机实例调用流程图:

在这里插入图片描述
虚拟机实例中线程

  这里说的线程是指在虚拟机运行的程序实例过程中一个线程。一个程序可以并发执行多个线程。HotSpot JVM中的Java线程与操作系统原生线程同生共死。当线程中本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好后会创建操作系统原生线程。当Java线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到可用的CPU上运行。当原生线程初始化完毕,就会调用Java线程的run()方法。当线程结束时会释放操作系统原生线程和Java线程。

HotSpot JVM后台运行的主要系统线程如下(复制):

系统线程作用
虚拟机线程这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
周期性任务线程这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
GC线程这些线程支持 JVM 中不同的垃圾回收活动。
编译器线程这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

内存区域

在这里插入图片描述
JVM内存按线程分三大区域:线程私有(Thread Private)区域、线程共享(Thread Public)区域,直接内存,线程私有里面包括程序计数器、虚拟机栈和本地方法栈,这三块区域就是不同接口方法执行调用时需要记录保存,所以必须保证其私有性,内部调用互不干扰,线程共享包括堆(heap),方法区(Method Area),堆也被称为共享堆,也属于共享内存,一个线程传递给另一个线程(多线程)时,有两种方法:共享内存和消息传递,共享内存就是用到共享堆,而消息传递会利用到Erlang和JoCaml。这里不长篇大论了,有兴趣的大佬可以延伸了解下。直接内存是在Java堆外、直接向系统申请的内存(不通过JVM线程),速度比堆快,由于直接使用系统内存,受限于系统内存。
备注:在一般情况下,堆是线程共享内存的区域,栈是线程私有的区域;在堆中主要存放对象实例(new对象),栈中主要存放各种基本数据类型、对象的引用,但这种说法不是绝对的,因为在多线程高并发的场景下需要保证线程高效稳定运行,这时候用到TLAB空间,给当前线程单独使用。

线程私有数据区域的生命周期与线程相同依赖用户线程的启动/结束,和创建/销毁(在HotSpot内,每个线程都与操作系统的本地线程直接映射,因此这部分区域的存/亡跟随本地线程的生/死)。
线程共享区域随虚拟机的启动/关闭,而创建/销毁。
直接内存并不是JVM运行时数据区的一部分,但也会频繁使用:在JDK1.4引入的NIO提供了基于Channel与Buffer的IO方式,通过调用Native函数库直接向系统申请,分配成堆外内存,然后使用DirectByteBuffer对象为这块内存的引用去调用,这样就避免了在Java堆和Native堆来回复制数据,因此在一些场景中可以显著提高性能。
在这里插入图片描述
程序计数器(线程私有)
   一块较小的内存空间,是当前线程所执行的字节码行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为"线程私有"的内存。在执行调用方式时,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是Native方法,则为空。
这个内存区域是唯一一个在虚拟机中没有OutOfMemoryError(OOM)的区域。

虚拟机栈(线程私有)
   是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法返回等信息。每一个方法从调用直至执行完成的过程。就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁-无论方法是正常完成还是异常完成(抛出了方法内未被捕获的异常)都算作方法结束。

在这里插入图片描述
本地方法栈(线程私有)
   本地方法栈和虚拟机栈(Stack Frame)作用类似,区别是虚拟机栈为执行Java方法服务,而本地方法栈则为Native方法服务,如果一个VM实现使用C-linkage模型来支持Native调用,那么该栈将会是一个C栈,但HotSpot VM直接把本地方法栈和虚拟机栈合二为一。

堆(线程共享)-运行时数据区
   被线程共享的内存区域,创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集最重要的内存区域。由于现代VM采用分代收集算法,因此堆从GC的角度还可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代。

方法区/永久代(线程共享)
   我们常说的永久代(Permanet Generation),用于存储被JVM加载的类信息、常量、静态变量、即时编译器后的代码等数据。HotSpot VM把GC分代收集扩展至方法区,这样HotSpot的垃圾收集器就可以像管理堆一样管理这部分内存,而不必为方法区开发专门的内存管理器
   JDK1.7前版本是用永久代来实现方法区,到jdk8版本后就是用元空间来实现方法区,而方法区只是Java定义的规范(概念),方法区因为是永久代,存放的对象数据很少改变,所以是很少出现垃圾回收的场景,性价比较低,一般方法区回收两部分:
     1.废弃常量:废弃的常量与堆回收比较类似,只需要指导该常量是否在其他地方被使用。
     2.无用的类:该类的所有实例被回收、加载该类的ClassLoader也被回收;该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类。

方法区的垃圾回收是用到堆里的GC分代垃圾回收思想实现方法区,而因为回收开销小方法区是用Full GC进行回收。Major GC一般指的是针对堆内存的老年代GC回收,而Full GC指的是对新生代、老年代和方法区的GC回收,说法不同回收的范围也不同,但使用的算法相同(还是使用堆内存的老年代算法)

   运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

JVM运行时堆内存
   堆从GC的角度还可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代
在这里插入图片描述
新生代
   存放新生的对象。一般占据整个堆内存的1/3空间。由于频繁创建对象,新生代会频繁触发Minor GC进行垃圾回收。新生代又分为Eden区、From Survivor、To Survivor三个区。

   Eden区:新对象出生地(如果对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发Minor GC,对新生代进行一次垃圾回收。


   From Survivor区和To Survivor区:在GC开始的时候,对象只会存在于Eden区和From Survivor区,To Survivor区是空的,一次Minor GC过后,Eden区和From Survivor区存活的对象会移动到To Survivor区中,然后会清空Eden区和From Survivor区,并对存活的对象年龄+1,如果对象的年龄达到15,则直接分配到老年代。Minor GC完成后,From Survivor区和To Survivor区的位置互换(From Survivor变成To Survivor,To Survivor变成From Survivor),如此往复。如果To Survivor无法足够存储某个对象,则将这个对象存储到老年代。
   新生代采用复制算法(Copying)(注意不需要标记,后面详解)

老年代
   主要存放应用程序中生命周期长的内存对象。
   随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(Major GC回收速度比Minor GC慢很多,据说10倍左右),但因为老年代存放对象比较稳定一般不会去频繁执行。在进行Major GC前一般都先进行一次Minor GC,使得有新生代对象晋升入老年代。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次Major GC进行垃圾回收腾出空间。
   老年代采用标记-清除算法(Marki-Sweep)和标记-整理(压缩)算法(Mark-Compact)(注意都需要标记,后面详解)

JVM方法区
   《Java虚拟机规范》中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。"但对于HotSpot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
方法区看作是独立于Java堆的内存空间,所以这边我把它单独拿出来整理说明
      ● 方法区(Method Area)与堆一样,是线程共享的内存区域
      ● 方法区在JVM启动的时候被创建,并且它的实际物理内存空间中和堆区一样都可以允许不连续的。
      ● 方法区的大小,跟堆内存一样,可以调整内存空间大小做扩展。
      ● 方法区的大小决定了系统可以保存多少个类文件,如果系统定义了太多的类,导致运行时方法区溢出,虚拟机同样会抛出OOM内存溢出:java.lang.OutOfMemoryError:PermGenspace(1.8之前)或者java.lang.OutOfMemoryError:Metspace(1.8之后)
      ● 关闭JVM就会释放这个区域的内存

HotSpot中方法区的演进
      ● 在JDK7之前,方法区被称为永久代(永久代实现了方法区)。JDK8开始,使用元空间取代永久代(元空间实现了方法区)。本质上,方法区和永久代并不等价。仅是对HotSpot而言。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit / IBM J9不存在永久代概念。
      而到了JDK8废弃永久代概念,改用了与JRockit、J9一样在本地内存中实现的元空间(Metaspace)
      元空间的本质与永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
      永久代、元空间两者不只是名字变了,内部结构也调整了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

引用类型

   强引用:在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量引用时,它处于可达状态,不可能被垃圾回收机制回收,即便该对象以后永远都不会用到JVM也不会回收。因此强引用是造成Java内存泄露的主要原因之一。
   软引用:需要用SoftReference类来实现,对于只有软引用对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在内存敏感的程序中。
   弱引用:需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。
   虚引用:需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

垃圾回收与算法

如何确定垃圾

   引用计数法:在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,一个对象如果没有任何与之关联的引用,即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。引用计数法因为有循环引用的问题,即两个对象实例间互为循环依赖,那么两个对象无法被回收,引用计数无法为空。在该算法中解决循环引用有三种:
      ● 明确知道这里会存在循环引用,在合适的位置主动断开环中的一个引用, 让对象得到回收。
      ● 使用WeakReference弱引用。
      ● 使用xcode检测循环引用。

   可达性分析法:为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC Roots”对象作为起点搜索。如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不等价于是可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

标记清除算法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标记和清除。标记阶段标记处所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
但问题是当清除后会造成内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
      注意:该算法用于方法区/永久代的垃圾回收
在这里插入图片描述

复制算法(Copying)
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的不存货的内存清掉。该算法虽然内存效率高,不易产生碎片,但问题是可用内存被压缩到了原本的一般,且存活对象增多的话,Copying算法效率会大大降低。
      注意:该算法用于堆内存中新生代的垃圾回收
在这里插入图片描述
标记整理算法(Mark-Compact)
结合了以上两个算法的思想,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,但标记后不是先清理对象,而是将存活对象移向内存的一端。然后再清除边界外的对象。
      注意:该算法用于堆内存中老年代和方法区/永久代的垃圾回收
在这里插入图片描述
分代收集算法(思想)
分代收集法(思想)是目前大部分JVM所采用的方法,其核心思想是根绝对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为新生代(Young/New Generation)和老年代(Tenured/Old Generation)。新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,老年代的特点是每次垃圾回收时只有少量对象需要被回收,因此可以根据不同区域选择不同的算法。

分区收集算法
注意分区收集算法不是分代收集算法,分区算法是将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间,根据目标停顿世家,每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。

GC垃圾收集器

    堆内存被划分为新生代和老年代两部分,新生代主要使用复制算法;老年代主要使用标记-整理回收算法和标记-清除算法,因此JVM针对新生代和老年代分别提供了多种不同的垃圾收集器,JDK1.6中Sun HotSpot虚拟机的垃圾收集器如下:
在这里插入图片描述
新生代:Serial垃圾收集器(单线程、复制算法)
最基本垃圾收集器,使用复制算法,曾经是JDK1.3之前新生代唯一的垃圾收集器。Serial是一个单线程的收集器,它不但只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程收集效率,因此Serial垃圾收集器依然是JVM运行在Client模式下默认的新生代垃圾收集器。

JVM的Server模式和Client模式
JVM Server模式与Client模式启动,最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。Server模式下应用的启动速度会比Client模式大概慢10%,但运行速度比Client VM要快至少10倍。
由于服务器的CPU、内存和硬盘都比客户端配置强大,所以程序部署后都应该以Server模式启动,以获取较好的性能;

Server:启动慢,编译更完全,编译器是自适应编译器,效率高,针对服务端应用优化,在服务器环境中最大化程序执行速度而设计;在JVM中默认-Xms是128M,-Xmx是1024M。
Client:启动快,内存占用少,编译快,针对桌面应用程序优化,为在客户端环境中减少启动时间而优化;在JVM中默认-Xms是1M,-Xms是64M。
可以通过运行命令行窗口:java -version来查看JVM默认运行是什么模式。
Server:
如图所示,HotSpot虚拟机采用Server模式启动的。也能看到该虚拟机是64位。一般64位虚拟机只支持Server模式,JDK文件目录下没有Client目录(实测也是)在这里插入图片描述
Client:
如图所示,HotSpot虚拟机采用Client模式启动的。
在这里插入图片描述
两者真正的区别在编译器级别上:
Client模式虚拟机编译器不尝试执行由编译器在服务器虚拟机上执行更复杂的优化,但在交换过程中,它需要较少的时间来分析和编译一段代码。这意味着Client模式可以更快地启动,并仅需要一个较小的内存占用。
Server模式虚拟机包含一个先进的自适应编译器支持许多C++编译器的优化进行优化,同样的类型及优化不能用传统的编译器完成,比如积极的内联在虚拟方法调用。这是一个竞争和性能优势,静态编译器。自适应优化技术在它的方法时非常灵活的,通常优于甚至先进的静态分析和编译技术。

新生代:ParNew垃圾收集器(Serial+多线程)
ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。[Parallel:平行的]
ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多JVM运行在Server模式下新生带的默认垃圾收集器。

新生代:Parallel Scavenge收集器(多线程复制算法、高效)
Parallel Scavenge是多线程复制算法的新生代收集器,与ParNew收集器不同的是它能控制程序的回收吞吐量,即:
吞吐量(Thoughput) = CPU用于运行用户代码的时间 / CPU总耗时;
吞吐量(Thoughput) = CPU用于运行用户代码的时间 / (CPU运行用户代码时间 + 垃圾收集时间)
自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

老年代:Serial Old收集器(单线程标记整理算法)
Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程收集器,使用标记-整理算法,这个收集器也主要是运行在Client模式下默认JVM收集器。
在Server模式下有两个用途:
   1.在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
   2.作为老年代中使用CMS收集器的后备垃圾收集方案。
新生代Serial与老年代Serial Old搭配垃圾收集过程图:

在这里插入图片描述
新生代Parallel Scavenge/ParNew与老年代Serial Old搭配垃圾收集过程图:
在这里插入图片描述
老年代:Parallel Old收集器(多线程标记整理算法)
Parallel Old收集器是新生代Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。
在JDK1.6之前,新生代使用Parallel Scavenge收集器只能搭配老年代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和老年代Parallel Old收集器的搭配策略。使用该算法,新生代的复制算法会暂停所有用户线程,老年代采用的标记-整理算法会暂停所有用户线程。

在这里插入图片描述
老年代:CMS收集器(多线程标记清除算法)
Concurrent Mark Sweep(CMS)收集器是一种老年代垃圾收集器,其最主要目标是实现最短垃圾回收停顿时间,和其他老年代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互频繁的程序提高用户体验。但使用该收集器会产生大量的内存碎片。
CMS工作机制相比其他的垃圾收集器来说更复杂,整个过程分为4个阶段:
    初始标记:标记GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
    并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
    重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
    并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,<font color=''red>所以总体上看CMS收集器和内存回收和用户线程是一起并发执行。
在这里插入图片描述
堆内存:G1收集器(分区收集算法)
Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比于CMS收集器,G1收集器两个最突出的改进是:
    1.基于标记-整理算法,不产生内存碎片。
    2.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获取最高的垃圾收集效率。

参考文档:https://my.oschina.net/u/4392049/blog/3899011

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值