JVM内存模型(一篇足以)

1 jvm逻辑内存模型

jvm内存模型详解记录

jdk1.8 内存模型

程序计数器

是什么: 程序计数器是一个记录着当前线程所执行的字节码的行号指示器。

为什么用:  首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。

特点

 1.线程隔离性,每个线程工作时都有属于自己的独立计数器。

 2.执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址(参考上一小节的描述)。

3.执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。

4.程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。

5.程序计数器,是唯一 一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

jvm内存模型详解记录

Java 虚拟机栈

虚拟机栈生命周期与线程相同。启动一个线程,程序调用函数,栈帧被压入栈中,函数调用结束,相应的是栈帧的出栈。

每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
通常所说的栈,一般是指在虚拟机栈中的局部变量部分。
局部变量所需内存在编译期间完成分配,
如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。
如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError。

栈帧 :由局部变量表,操作数栈,帧数据区组成。

局部变量表:存放的是函数的入参,以及局部变量。

操作数栈:存放调用过程中的计算结果的临时存放区域。

帧数据区:存放的是异常处理表和函数的返回,访问常量池的指针。

举个例子,线程执行进入方法A,则会创建栈帧入栈,A方法调用了B方法,B栈帧入栈,B方法中调用C方法,C创建了栈帧压入栈中,接下来是D入栈

反过来,D方法执行完,栈帧出栈,接着是C,B,A。

jvm内存模型详解记录

这里写图片描述

 

  • 局部变量表
    存放方法参数和局部变量
    相对于类属性变量的准备阶段和初始化阶段来说,局部变量没有准备阶段,必须显式初始化
    如果是非静态方法,则在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是参数和局部变量
    字节码指令中的STORE指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内
  • 操作栈
    操作栈是一个初始状态为空的桶式结构栈
    在方法执行过程中,会有各种指令往栈中写入和提取信息
    JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈
    字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的stack属性中

下面用一段简单的代码说明操作栈与局部变量表的交互

  • 详细的字节码操作顺序如下:

    第1处说明:局部变量表就像个中药柜,里面有很多抽屉,依次编号为0, 1, 2,3,.,. n
    字节码指令istore_ 1就是打开1号抽屉,把栈顶中的数13存进去
    栈是一个很深的竖桶,任何时候只能对桶口元素进行操作,所以数据只能在栈顶进行存取

某些指令可以直接在抽屉里进行,比如inc指令,直接对抽屉里的数值进行+1操作
程序员面试过程中,常见的i++和++i的区别,可以从字节码上对比出来

  • iload_ 1从局部变量表的第1号抽屉里取出一个数,压入栈顶,下一步直接在抽屉里实现+1的操作,而这个操作对栈顶元素的值没有影响
    所以istore_ 2只是把栈顶元素赋值给a
  • 表格右列,先在第1号抽屉里执行+1操作,然后通过iload_ 1 把第1号抽屉里的数压入栈顶,所以istore_ 2存入的是+1之后的值

这里延伸一个信息,i++并非原子操作。即使通过volatile关键字进行修饰,多个线程同时写的话,也会产生数据互相覆盖的问题.

局部变量表:
存放编译期可知的各种基本数据类型、对象引用类型和returnAddress类型(指向一条字节码指令的地址:函数返回地址)。
long、double占用两个局部变量控件Slot。
局部变量表所需的内存空间在编译期确定,当进入一个方法时,方法在栈帧中所需要分配的局部变量控件是完全确定的,不可动态改变大小。
异常:线程请求的栈帧深度大于虚拟机所允许的深度—StackOverFlowError,如果虚拟机栈可以动态扩展(大部分虚拟机允许动态扩展,也可以设置固定大小的虚拟机栈),但是无法申请到足够的内存—OutOfMemorError。

操作数栈:
后进先出LIFO,最大深度由编译期确定。栈帧刚建立使,操作数栈为空,执行方法操作时,操作数栈用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。
操作数栈可以存放一个jvm中定义的任意数据类型的值。
在任意时刻,操作数栈都一个固定的栈深度,基本类型除了long、double占用两个深度,其它占用一个深度
动态连接:
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

方法返回地址:
当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。

本地方法栈

本地方法是由其它语言编写的,编译成和处理器相关的机器代码

本地方法保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的

拓展:JAVA方法是由JAVA编写的,编译成字节码,存储在class文件中

java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

与java环境外交互

有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。

与操作系统交互

JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

DLL的加载是通过调用System.loadLibrary(String filename)或System.load(String filename)方法实现的。

Java 堆

存放对象实例,所有的对象的内存都在这里分配。

老年代:2/3的堆空间
年轻代:1/3的堆空间
eden区:8/10 的年轻代
survivor0: 1/10 的年轻代
survivor1:1/10的年轻代

元数据区域
元数据区域取代了1.7版本及以前的永久代。元数据和永久代本质上都时方法区的实现。方法区皴法虚拟机加载的类型西,静态变量,常量数据。
参数设置:-XX:MetaspaceSize=18m
-XX:MaxMetaspaceSize=60m

这里写图片描述

Java 堆中还可以细分为:新生代和老年代;

再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配

的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local

Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,

存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配

内存。

Heap是OOM故障最主要的发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用
通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间
堆的内存空间既可以固定大小,也可运行时动态地调整,通过如下参数设定初始值和最大值,比如

复制代码

1

-Xms256M. -Xmx1024M

其中-X表示它是JVM运行参数

  • ms是memorystart的简称 最小堆容量
  • mx是memory max的简称 最大堆容量

但是在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms和Xmx设置成一样大小,避免在GC后调整堆大小时带来的额外压力

堆分成两大块:新生代和老年代
对象产生之初在新生代,步入暮年时进入老年代,但是老年代也接纳在新生代无法容纳的超大对象

新生代= 1个Eden区+ 2个Survivor区
绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young GC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区,这个区真是名副其实的存在
Survivor 区分为S0和S1两块内存空间,送到哪块空间呢?每次Young GC的时候,将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态
如果YGC要移送的对象大于Survivor区容量上限,则直接移交给老年代
假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去,那就错了。每个对象都有一个计数器,每次YGC都会加1。

复制代码

1

-XX:MaxTenuringThreshold

参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升至老年代。如果该参数配置为1,那么从新生代的Eden区直接移至老年代。默认值是15,可以在Survivor 区交换14次之后,晋升至老年代
对象分配与简要GC流程图

若Survivor区无法放下,或者超大对象的阈值超过上限,则尝试在老年代中进行分配;
如果老年代也无法放下,则会触发Full Garbage Collection(Full GC);
如果依然无法放下,则抛OOM.

堆出现OOM的概率是所有内存耗尽异常中最高的
出错时的堆内信息对解决问题非常有帮助,所以给JVM设置运行参数-

复制代码

1

XX:+HeapDumpOnOutOfMemoryError

让JVM遇到OOM异常时能输出堆内信息

在不同的JVM实现及不同的回收机制中,堆内存的划分方式是不一样的

存放所有的类实例及数组对象
除了实例数据,还保存了对象的其他信息,如Mark Word(存储对象哈希码,GC标志,GC年龄,同步锁等信息),Klass Pointy(指向存储类型元数据的指针)及一些字节对齐补白的填充数据(若实例数据刚好满足8字节对齐,则可不存在补白)

特点

Java虚拟机所需要管理的内存中最大的一块.

堆内存物理上不一定要连续,只需要逻辑上连续即可,就像磁盘空间一样.
堆是垃圾回收的主要区域,所以也被称为GC堆.

堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的(通过-Xmx和-Xms控制),因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError.

线程共享
整个Java虚拟机只有一个堆,所有的线程都访问同一个堆.
它是被所有线程共享的一块内存区域,在虚拟机启动时创建.
而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个

方法区

和java堆一样,方法区是一块所有线程共享的内存区域。

保存系统的类信息,比如,类的字段,方法,常量池等。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误

jdk1.6和jdk1.7方法区可以理解为永久区。

jdk1.8已经将方法区取消,替代的是元数据区。

jdk1.8的元数据区可以使用参数-XX:MaxMetaspaceSzie设定大小,这是一块堆外的直接内存,与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽可用系统内存

其他拓展

运行时常量池

运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。

运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)

直接内存

直接内存(Direct Memory)就是Java堆外内存

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

面试的时候记录这个图就行

jvm内存模型详解记录

 

2.  对象的内存布局

在HotSpot虚拟机中。对象在内存中存储的布局分为

1.对象头
2.实例数据
3.对齐填充

2.1 .1 对象头【markword】

在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】。

 

markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。
为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息,

 

假设当前为32bit,在对象未被锁定情况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。

不同状态下存放数据

这其中锁标识位需要特别关注下。锁标志位与是否为偏向锁对应到唯一的锁状态

锁的状态分为四种无锁状态偏向锁轻量级锁重量级锁

不同状态时对象头的区间含义,如图所示。

对象头.jpg

HotSpot底层通过markOop实现Mark Word,具体实现位于markOop.hpp文件。

 

markOop中提供了大量方法用于查看当前对象头的状态,以及更新对象头的数据,为synchronized锁的实现提供了基础。[比如说我们知道synchronized锁的是对象而不是代码,而锁的状态保存在对象头中,进而实现锁住对象]。

关于对象头和锁之间的转换,网上大神总结

偏向锁轻量级锁重量级锁.png

2.1.2 实例数据

 

存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。
分配策略:相同宽度的字段总是放在一起,比如double和long

2.1.3对齐填充

这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求。

 

由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。

2.2 对象的访问定位

java程序需要通过引用(ref)数据来操作堆上面的对象,那么如何通过引用定位、访问到对象的具体位置。

 

对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
1.句柄访问对象
2.直接指针访问对象。(Sun HotSpot使用这种方式)

参考Java对象访问定位

2.2.1 句柄访问

简单来说就是java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。

优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。

访问方式2.jpg

2.2.2 直接指针

与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样。

优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】

访问方式1.jpg

3.内存溢出

 

两种内存溢出异常[注意内存溢出是error级别的]
1.StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度
2.OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]

java -verbose:class -version 可以查看刚开始加载的类,可以发现这两个类并不是异常出现的时候才去加载,而是jvm启动的时候就已经加载。这么做的原因是在vm启动过程中我们把类加载起来,并创建几个没有堆栈的对象缓存起来,只需要设置下不同的提示信息即可,当需要抛出特定类型的OutOfMemoryError异常的时候,就直接拿出缓存里的这几个对象就可以了。

比如说OutOfMemoryError对象,jvm预留出4个对象【固定常量】,这就为什么最多出现4次有堆栈的OutOfMemoryError异常及大部分情况下都将看到没有堆栈的OutOfMemoryError对象的原因。


 

5.运行时常量池(Runtime Constant Pool)

5.1.1 定义

运行时常量池是方法区的一部分.
方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码.其中常量存储在运行时常量池中.

我们知道,.java文件被编译之后生成的.class文件中除了包含:类的版本、字段、方法、接口等信息外,还有一项就是常量池
常量池中存放编译时期产生的各种字面量和符号引用,.class文件中的常量池中的所有的内容在类被加载后存放到方法区的运行时常量池中。
PS:int age = 21;//age是一个变量,可以被赋值;21就是一个字面值常量,不能被赋值;
int final pai = 3.14;//pai就是一个符号常量,一旦被赋值之后就不能被修改。

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池( Constant pool table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

在近三个JDK版本(6、7、8)中, 运行时常量池的所处区域一直在不断的变化,
在JDK6时它是方法区的一部分
7又把他放到了堆内存中
8之后出现了元空间,它又回到了方法区。
其实,这也说明了官方对“永久代”的优化从7就已经开始了

5.1.2 特性

class文件中的常量池具有动态性.
Java并不要求常量只能在编译时候产生,Java允许在运行期间将新的常量放入方法区的运行时常量池中.
String类中的intern()方法就是采用了运行时常量池的动态性.当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串,则返回池中的字符串.否则,将此 String 对象添加到池中,并返回此 String 对象的引用.

5.1.3 可能抛出的异常

运行时常量池是方法区的一部分,所以会受到方法区内存的限制,因此当常量池无法再申请到内存时就会抛出OutOfMemoryError异常.

我们一般在一个类中通过public static final来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。

当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。

当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。

6 直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但在JVM的实际运行过程中会频繁地使用这块区域.而且也会抛OOM

在JDK 1.4中加入了NIO(New Input/Output)类,引入了一种基于管道和缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆里的DirectByteBuffer对象作为这块内存的引用来操作堆外内存中的数据.
这样能在一些场景中显著提升性能,因为避免了在Java堆和Native堆中来回复制数据.

综上看来
程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样。
而堆、方法区是线程共享的,在Java虚拟机中只有一个堆、一个方法栈。并在JVM启动的时候就创建,JVM停止才销毁。

7 Metaspace (元空间)

在JDK8,元空间的前身Perm区已经被淘汰,在JDK7及之前的版本中,只有Hotspot才有Perm区(永久代),它在启动时固定大小,很难进行调优,并且Full GC时会移动类元信息

在某些场景下,如果动态加载类过多,容易产生Perm区的OOM.
比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多的类,经常出现致命错误:

复制代码

1

Exception in thread ‘dubbo client x.x connector' java.lang.OutOfMemoryError: PermGenspac

为解决该问题,需要设定运行参数

复制代码

1

-XX:MaxPermSize= l280m

如果部署到新机器上,往往会因为JVM参数没有修改导致故障再现。不熟悉此应用的人排查问题时往往苦不堪言,除此之外,永久代在GC过程中还存在诸多问题

所以,JDK8使用元空间替换永久代.区别于永久代,元空间在本地内存中分配.
也就是说,只要本地内存足够,它不会出现像永久代中java.lang.OutOfMemoryError: PermGen space

同样的,对永久代的设置参数PermSize和MaxPermSize也会失效
在JDK8及以上版本中,设定MaxPermSize参数,JVM在启动时并不会报错,但是会提示:

复制代码

1

Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize=2560m; support was removed in 8.0

默认情况下,“元空间”的大小可以动态调整,或者使用新参数MaxMetaspaceSize来限制本地内存分配给类元数据的大小.

在JDK8里,Perm 区所有内容中

  • 字符串常量移至堆内存
  • 其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间

    比如上图中的Object类元信息、静态属性System.out、整型常量000000等
    图中显示在常量池中的String,其实际对象是被保存在堆内存中的。

元空间特色

  • 充分利用了Java语言规范:类及相关的元数据的生命周期与类加载器的一致
  • 每个类加载器都有它的内存区域-元空间
  • 只进行线性分配
  • 不会单独回收某个类(除了重定义类 RedefineClasses 或类加载失败)
  • 没有GC扫描或压缩
  • 元空间里的对象不会被转移
  • 如果GC发现某个类加载器不再存活,会对整个元空间进行集体回收

GC

  • Full GC时,指向元数据指针都不用再扫描,减少了Full GC的时间
  • 很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了
  • 元空间只有少量的指针指向Java堆
    这包括:类的元数据中指向java.lang.Class实例的指针;数组类的元数据中,指向java.lang.Class集合的指针。
  • 没有元数据压缩的开销
  • 减少了GC Root的扫描(不再扫描虚拟机里面的已加载类的目录和其它的内部哈希表
  • G1回收器中,并发标记阶段完成后就可以进行类的卸载

元空间内存分配模型

  • 绝大多数的类元数据的空间都在本地内存中分配
  • 用来描述类元数据的对象也被移除
  • 为元数据分配了多个映射的虚拟内存空间
  • 为每个类加载器分配一个内存块列表
    • 块的大小取决于类加载器的类型
    • Java反射的字节码存取器(sun.reflect.DelegatingClassLoader )占用内存更小
  • 空闲块内存返还给块内存列表
  • 当元空间为空,虚拟内存空间会被回收
  • 减少了内存碎片

最后,从线程共享的角度来看

  • 堆和元空间是所有线程共享的
  • 虚拟机栈、本地方法栈、程序计数器是线程内部私有的

从这个角度看一下Java内存结构
Java 的线程与内存

 

8 从GC角度看Java堆

堆和方法区都是线程共享的区域,主要用来存放对象的相关信息。我们知道,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间才能知道会创建哪些对象,因此, 这部分的内存和回收都是动态的,垃圾收集器所关注的就是这部分内存(本节后续所说的“内存”分配与回收也仅指这部分内存)。而在JDK1.7和1.8对这部分内存的分配也有所不同,下面我们来详细看一下

Java8中堆内存分配如下图:

9 JVM关闭

  • 正常关闭:当最后一个非守护线程结束或调用了System.exit或通过其他特定于平台的方式,比如ctrl+c。
  • 强制关闭:调用Runtime.halt方法,或在操作系统中直接kill(发送single信号)掉JVM进程。
  • 异常关闭:运行中遇到RuntimeException 异常等

在某些情况下,我们需要在JVM关闭时做一些扫尾的工作,比如删除临时文件、停止日志服务。为此JVM提供了关闭钩子(shutdown hocks)来做这些事件。

Runtime类封装java应用运行时的环境,每个java应用程序都有一个Runtime类实例,使用程序能与其运行环境相连。

关闭钩子本质上是一个线程(也称为hock线程),可以通过Runtime的addshutdownhock (Thread hock)向主jvm注册一个关闭钩子。hock线程在jvm正常关闭时执行,强制关闭不执行。

对于在jvm中注册的多个关闭钩子,他们会并发执行,jvm并不能保证他们的执行顺序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值