JVM的初步了解

时间:2022-10-03 14:20:28

之前有了解过jvm的知识,但是都是一知半解,很多东西还是没有很清晰的概念,今天就梳理一下.

jvm模型

JVM的初步了解

程序计数器

程序计数器(Program Counter Register)JVM中运行的每个线程都有自己的PC寄存器,当该线程执行的方法是本地方法时,PC寄存器保存的值为undefined;否则保存的是JVM内当前正在执行的指令的地址。

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型。

本地方法栈

本地方法栈(Native Method Stack)可以理解为和虚拟机栈所作用基本一致,只是本地方法栈用来执行Native方法服务,在有些虚拟机中(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。本地方法栈区域也会抛出*Error和OutOfMemoryError异常。

方法区

方法区(Method Area)是各个线程共享的内存区域;它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
对于习惯了了HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区称为“永久代”(Permanent Generation).在1.8PermGen被移除,由metaspace代替..对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的在方法区中还有一块区域是运行时常量池(Runtime Constant Pool);运行时常量池相对于Class文件常量池的一个重要特征是具备动态性;
Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

直接内存区

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
在JDK 1.4中新加入了NIO类,引入了一种基于Channel与Buffe的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java堆大小的限制;但是会受到本机总内存大小以及处理器寻址空间的限制。如果各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),会导致动态扩展时出现OutOfMemoryError异常。需要限制直接内存可以使用: -XX:MaxDirectMemorySize=xxM限制;这个值默认是0;不限制大小

jvm模型不同版本不一样.这里以hotspot(build 25.91-b14, mixed mode) 为例.
在jdk1.8中hotspot取消了永久区,使用元空间代替.
通过visualvm可以看到heap分为以下几个部分

heap(堆)

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域
由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、Survivor 0/Survivor 1空间等。细分堆只是为了更好地回收内存,或者更快地分配内存;
Java堆可以处于物理上不连续的内存空间中;如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

Eden:对象在新生代Eden区中分配.当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC.
Survivor 0/Survivor 1:当回收时, 将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor上,最后清理掉 Eden和刚才用过的Survivor空间.
Old:大对象直接进老年代(- XX: PretenureSizeThreshold参数令大于这个设置值的对象直接进老年代),以此避免Eden和Survivor之间的大量复制.多次minor gc后仍然存活的会被复制到老年代.
Metaspace:使用本地内存存放类的元数据.( -XX:MaxMetaspaceSize=10m来设置最大值)

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每”熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
Eden,Survivor0,Survivor1合称为YoungGen(新生代),采用复制算法;
“标记- 整理”(Mark- Compact)算法,标记过程仍然与”标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动.
oracle在java7里做了一个非常重要的改变-string pool被分配在heap.这意味着你再也不会被限制在一个单独的固定大小的内存区域.所有的字符串分配在堆里,和大多数的普通对象一样.这样在调整你的程序时你只需要管理堆大小.
所有在string pool的string都会在没有引用指向他们的时候被gc回收.

垃圾回收机制

引用计数

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象(不是引用)都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1,但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
缺点: 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.

可达性分析

通过一些列称为“GC Root”的对象作为起点,从这些接点向下搜索,搜索的路径称为引用链,当一个对象到GC root没有任何引用链相连时,则证明对象不可用。

JAVA中可以作为GC roots的对象包括:
1. 虚拟机栈中引用的对象(本地变量表)
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中引用的对象(Native对象)

一些常见的垃圾回收算法

标记-清除(Mark-Sweep)

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

复制(Copying)

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低,而且能够使用的内存缩减到原来的一半

标记-压缩(Mark-Compact)

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。

增量

增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾,也可理解为把堆栈分成一小块一小块,每次仅对某一个块进行垃圾收集。这会造成较小的应用程序中断时间,使得用户一般不能觉察到垃圾收集器正在工作。

分代回收

复制收集器的缺点是:每次收集时,所有的标记对象都要被拷贝,从而导致一些生命周期很长的对象被来回拷贝多次,消耗大量的时间。而分代收集器则可解决这个问题,分代收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。JVM生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象(非短命对象)将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。

HotSpot(JDK 7)虚拟机提供的几种垃圾收集器

串行Serial/Serial Old

Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

并行ParNew

ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

并行Parallel Scavenge

Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

并行Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

并发CMS

CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

G1

G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型

JVM的初步了解

GC类型及时机

Scavenge GC 一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,堆Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。

Full GC 对整个堆进行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。
有如下原因可能导致Full GC:
-Tenured被写满
-Perm域被写满
-System.gc()被显示调用
-上一次GC之后Heap的各域分配策略动态变化

类加载机制

类加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。 JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤。

JVM的初步了解

类加载器

类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。

类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。

类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。

引导类加载器(bootstrap class loader):

它用来加载 Java 的核心库,是用原生代码来实现的,C++实现,并不继承自 java.lang.ClassLoader。

扩展类加载器(extensions class loader):

它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

系统类加载器(system class loader):

它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

自定义加载器(Custom ClassLoader) :

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

JVM的初步了解