java 虚拟机的存储_JVM虚拟机内存

1 Java内存区域与内存溢出异常

1.1运行时数据区域

根据java虚拟机规范,Java的虚拟机啊锁需要的内存将会包括以下几个运行时数据区域。

da3c511d84b9cc059df9ec14e35b5d7e.png

1.1.1 程序计数器

程序计数器是一块较小的内存空间,线程私有。 字节码解释器工作就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令, 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址, 如果正在执行的是Native方法,这个计数器的值则为空(Undifined)。

这个区域是没有规定任何OutOfMemoryError情况的区域。

1.1.2 Java虚拟机栈

线程私有的,生命周期与线程相同。 它描述的是Java方法执行的线程内存模型: 每个方法都被执行的时候,Java虚拟机会同步创建一个栈帧(Stack Frame)用于储存局部变量表,操作数栈,动态连接,方法出口等信息。 每个方法被调用到执行完毕的过程,就对应着一个帧栈在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种Java虚拟机的基本类型(int, long, short, float, boolean, double, byte, char),对象引用(reference类型, 它并不等同于对象本身), 可能是一个指向对喜庆起始地址的引用指针, 也可能是指向一个代表对象句柄或者其他与此对象的位置) 和returnAddress类型(指向了一条字节码指令的地址)。

StackOverFlowError异常: 如果线程请求的栈深度大于虚拟机所允许的深度。

OutOfMemoryError异常: 当栈扩展到无法申请到足够的内存会抛出OutOfMemoryError异常。

1.1.3 本地方法栈

与虚拟机栈很相似但有所区别的是,本地方法栈则是为虚拟机使用到本地的Native方法。 也会有 StackOverflowError 和 OutOfMemoryError 异常。

1.1.4 堆

堆是虚拟机所管理的内存中最大的一块Java堆, 是被所有线程共享的一块内存, 在虚拟机启动时创建。线程共享,主要是存放对象实例和数组。 从分配内存的角度看, 所有线程共享的Java堆中可以分出多个线程私有的分配缓冲区Thread Local Allocation Buffer, TLAB)。 堆可以处于物理上不连续的内存空间中,但在逻辑上应该被视为连续的。

当堆没有完成实例分配,无法扩展时, Java虚拟机将会抛出OutOfMemoryError异常。

1.1.5 方法区

线程共享的内存区域,它用于储存被虚拟机加载的类型信息,常量, 静态变量,即时编译器编译后的代码缓存等数据。

总结以下:

f191e6365df1434020964210b63f51cc.png

1.1.6 运行时常量池

运行时常量池,是方法区的一部分。用于储存编译期存放编译期生成的各种字面量和符号引用,这部分将在类加载后存放到方法区的运行时常量池。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

1.1.7 直接内存

非虚拟机运行时数据区的部分

1.2 HotSpot虚拟机对象探秘

主要介绍数据是如何创建、如何布局以及如何访问的。

1.2.1对象的创建

当虚拟机遇到一条字节码new指令时, 首先将去检查这个指令的参数是否能在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析 和初始化过。 如果没有就执行相应的累加载过程。

然后虚拟机将为对象分配内存,在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。

每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。

内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。

1.2.2 对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

Header

Header包括两部分,第一部分储存对象自身运行时的数据, 比如hashCode, GC分代年龄, 锁状态标志, 线程持有的锁, 偏向线程ID, 偏向事间戳等。 32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针, 就是对象指向它的类型元数据的指针, 虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

** 实例数据(Instance Data):**

程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

对齐填充(Padding)

是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

1.2.3 对象的访问定位

使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

通过句柄访问

Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

4fb5f005e4978f1cb3cbee8407ad5684.png

通过直接指针访问

reference 中直接存储对象地址

a80f382573620022fb0fdf800db118c4.png

2 垃圾收集器与内存分配策略

2.1 概述

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

2.2 对象已死?

在堆理存放着几乎所有对象实例,在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

2.2.1 引用计数算法

给对象添加一个引用计数器。但是难以解决循环引用问题。

2.2.2可达性分析算法。

通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

2.2.3 引用

强引用

类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。

软引用

SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

弱引用

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

虚引用

PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

2.2.4 生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。

finalize() 方法只会被系统自动调用一次。

2.2.5 回收方法区

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

永久代垃圾回收主要两部分内容:废弃的常量和无用的类。

判断废弃常量:一般是判断没有该常量的引用。

判断无用的类:要以下三个条件都满足

该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例

加载该类的 ClassLoader 已经被回收

该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法。

2.3 垃圾回收算法

2.3.1 标记 —— 清除算法

直接标记清除就可。

效率不足,内存碎片多。

2.3.2 复制算法

把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

2.3.3 标记-整理算法

不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。

2.3.4 分代回收

根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。

新生代

每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。

老年代

老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。

2.4 HotSpot 的算法实现

2.5 垃圾回收器

2.6 内存分配与回收策略。

新生对象通常会分配在新声代中, 少数情况下,也可能会直接分配在老年代。

新生代 GC (Minor GC)

发生在新生代的垃圾回收动作,频繁,速度快。

老年代 GC (Major GC / Full GC)

发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

标签:对象,虚拟机,GC,内存,JVM,线程,引用

来源: https://blog.csdn.net/weixin_47285644/article/details/114045606

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值