JVM 内存结构(java 内存结构)

JVM 内存结构(java 内存结构)

借用JavaGuide哥的两张图(看过不少描述图,这两张是我最喜欢的):

JDK 1.8 之前:
在这里插入图片描述

JDK 1.8
在这里插入图片描述

1. JVM 内存结构描述

1.1 程序计数器

程序计数器中只存储当前线程执行程序的行号,一个类指针的数据结构。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

我们熟悉的分支操作、循环操作、跳转、异常处理和线程恢复等基础模型都需要依赖这个计数器来完成。

JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器。

1.2 Java 虚拟机栈

Java虚拟机栈描述的是Java方法执行时的内存模型,每个方法在执行时都会创建一个栈帧,而每个栈帧中包含:局部变量表、操作数栈、动态连接、方法出口信息等;java虚拟机栈栈顶的栈帧就是当前执行方法的栈帧,而如果当前方法调用其他方法时又会创建一个新的栈帧并放到栈顶。(可能有点绕,在理解的前提下是你已经熟悉了栈的概念)

栈帧:

  • 局部变量表:即在方法内部定义的变量:boolean、byte、char、short、int、float、long、double,对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

  • 操作数栈: 结构与局部变量类似,存放的是局部变量的复制内容,计算的中间值或临时值等,比如将局部变量的a,b两个值压入操作数栈中,计算他们的结果,然后将结果又压入操作数栈中。

  • 动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

  • 方法出口信息(方法返回地址):在方法调用结束后,必须返回到该方法最初被调用时的位置,程序才能继续运行,所以在栈帧中要保存一些信息,用来帮助恢复它的上层主调方法的执行状态。方法返回地址就可以是主调方法在调用该方法的指令的下一条指令的地址。

1.3 本地方法栈

与Java 虚拟机栈 非常类似,它是java调用Native 方法时开辟的内存。Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。

1.4 堆

堆是虚拟机中主要的区域,所有的对象实例以及数组都在堆上分配,常说的垃圾回收(GC),也主要是指这里。
注:由于JIT技术的成熟,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存,所以并非绝对的都是在堆上分配

堆又可以细分为:

  • 新生代
  • 老年代

注:G1之后的垃圾收集器虽然也有新生代和老年代的概念,但是实际在堆上划分的内存已经不是按照新生代,老年代的比例划分了,而是多个大小相同的区域。

1.5 方法区(元空间)

方法区/元空间,主要存放的是已加载的类信息、属性信息、方法信息、常量池、静态变量、即时编译器编译后的代码缓存。

已加载的类信息:

  • 类的完整有效名称:包名和类名
  • 类的直接父类完整有效名称;
  • 类的修饰符(public abstract final)
  • 类的直接接口列表;

属性信息(类中的成员变量):修饰符(public,private,final,static等),类型(int,double等),属性名称

方法信息(类的方法信息):方法的名称,方法的返回类型,方法的参数,方法的修饰符,方法的局部变量表和操作数栈,异常信息表;

常量池:

  • Class文件常量池:包含字面量和符号引用,字面量如类中定义的字符串,final修饰的变量等;符号引用:方法名、方法描述符、类名、字段名,字段描述符,比如在类中定义了String a 变量,它会将String这个类型转换为符号引用,这样在运行时能通过这个引用找到对应的类进行解析等,再比如调用其他类的方法等,就会存储该类的方法引用等;

  • 运行时常量池:当类加载到内存中后,jvm 就会将class常量池中的内容存放到运行时常量池中,运行时常量池也是每个类都有一个。并且在类加载的解析阶段会把运行时常量池的符号引用替换成直接引用,这个过程需要查找字符串常量池

  • 字符串常量池:

String a= "hellow";
String b= new String("hellow");

第一种方式声明的字面量hellow是在编译期就已经确定的,它会直接进入class文件常量池中;当运行期间在全局字符串常量池中会保存它的一个引用.实际上最终还是要在堆上创建一个”hellow”对象。

第二种方式方式使用了new String(),也就是调用了String类的构造函数,我们知道new指令是创建一个类的实例对象并完成加载初始化的,因此这个字符串对象是在运行期才能确定的,创建的字符串对象是在堆内存上。

class文件里常量池里大部分数据会被加载到“运行时常量池”,包括String的字面量;但同时“Hello”字符串的一个引用会被存到“字符串常量池”中

字符串常量池是JVM所维护的一个字符串实例的引用表

静态变量:类变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块

方法区(元空间)在JDK 1.8后使用的是直接内存,即系统的实际内存。

什么情况会触发FULL GC?
  1. System.gc() 该方法是建议虚拟机触发full gc 虽然不一定能执行,但是大多数情况会执行full gc,如果不是必要情况,不建议在系统中调用该方法

  2. 老年代空间不足。老年代空间不足又分为几种情况:

    • Survivor(s0或s1)区域中的对象年龄达到晋升老年代时,如果发现晋升的对象大小大于老年代剩余大小,则会触发full gc。

    • 老年代连续空间不足,Survivor晋升到老年代时,虽然老年代剩余空间大于晋升对象大小,但是没有足够的连续空间用来分配空间,这个时候仍然会粗放full gc,不过这种情况跟使用的回收算法是标记-清除有关

    • 大对象或长期存活的对象(如数组,字符串等)直接进入老年代时,发现老年代剩余空间不足。

  3. jdk 1.7 之前的永久代满了,那么现在的jdk1.8之后的元空间会触发full gc吗?

对象可以在栈中分配吗?

我们都知道,所有对象都是在堆上分配的,但是当面试官问出这样的问题时,我们总会有点疑惑,什么?对象也能在栈上分配了?怎么做到的呢?这样做的好处是什么?

JVM中存在一个 逃逸分析 的概念,逃逸分析的主要作用就是分析对象作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为就叫做 方法逃逸。甚至该对象还可能被外部线程访问到,例如赋值被类变量或可以在其他线程中访问的实例变量,称为 线程逃逸。

那么如果逃逸分析能够证明对象没有逃逸到方法外或线程外,那么完全可以将对象在栈上分配,在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间,这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象。

然而在Hotspot虚拟机中,并没有进行实际的栈上分配,而是使用了标量替换这一技术。

标量和聚合量:

标量:不能被进一步分解的变量称为标量,仅能存储一个值的变量,如:基础类型(int float double 等8个基本类型),对象的引用(reference)等

聚合量:可以被进一步分解成标量,对象就是聚合量。

标量替换:将对象的成员变量分解为分散的标量,这就叫做标量替换

所以如果对象未发生逃逸,编译器会在方法内将未逃逸的聚合量分解成多个标量,以此来减少堆上分配。

这就是对象在栈上分配内存 的由来。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云哲-吉吉2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值