JVM内存结构

 JavaSE,Java平台标准版,为Java EE和Java ME提供了基础。

 JDK:Java开发工具包,JDK是JRE的超集,包含JRE中的所有内容,以及开发程序所需的编译器和调试程序等工具。

 JRE:Java SE运行时环境 ,提供库、Java虚拟机和其他组件来运行用Java编程语言编写的程序。主要类库包括:程序部署发布、用户界面工具类、继承库、其他基础库,语言和工具基础库

 JVM:java虚拟机,负责JavaSE平台的硬件和操作系统无关性、编译执行代码(字节码)和平台安全性。

JVM 内存结构 (1.7)

 

JVM 内存结构 (1.8)

程序计数器

一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,所以为线程私有。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈

虚拟机栈描述的是Java执行方法的内存模型。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

Java虚拟机栈可能出现两种类型的异常:

  1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  2. 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

本地方法栈

 本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

方法区(永久区)

这块区域对应的是Permanent Generation(持久代),一般的,方法区上执行的垃圾收集是很少的,因此方法区又被称为持久代的原因之一。

当方法区使用的内存超过它允许的大小时,就会抛出OutOfMemory:PermGen Space异常。

用于存储类信息,常量,静态变量static关键字修饰的等信息,当class文件被加载的时候就会被初始化,线程共享

方法区是所有线程共享的内存,在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,

并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池静态变量都转移到了Java堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

类元信息(Klass

  • 类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表(Constant Pool Table)
  • 常量池表(Constant Pool Table)存储了类在编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中

运行时常量池(Runtime Constant Pool)

  • 运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些
  • 运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的intern()方法

直接内存

直接内存位于本地内存,不属于JVM内存,但是也会在物理内存耗尽的时候报OOM,所以也讲一下。

在jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据。

相关参数 :

jdk1.7及以前:-XX:PermSize;-XX:MaxPermSize;

jdk1.8以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize

jdk1.8以后大小就只受本机总内存的限制

如:-XX:MaxMetaspaceSize=3M


Java虚拟机管理内存中最大的一块,线程共享区域。所有对象实例和数组都在堆上分配内存空间。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

从内存回收角度可分为:新生代(eden 、from survivor、to survivor)、老年代,

从内存分配角度:线程共享的java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。

例:

  

因为有static关键字,所以这个对象是存放在方法区,去掉static则存放在堆。

堆是JVM内存中最大的一块,由所有线程共享,是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于java虚拟机的发展,堆中也多了许多东西,现在主要有:

 

  • 对象实例
  • 类初始化生成的对象
  • 基本数据类型的数组也是对象实例
  • 字符串常量池
  • 字符串常量池原本存放于方法区,jdk7开始放置于堆中。
  • 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
  • 静态变量
  • 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
  • 线程分配缓冲区(Thread Local Allocation Buffer)
  • 线程私有,但是不影响java堆的共性
  • 增加线程分配缓冲区是为了提升对象分配时的效率

 

 

 栈

函数中定义的一些基本类型的变量对象的引用变量都在函数的栈内存中分配。 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。

 


 

分为新生代,老年代。

--新生代

新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中。

新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。新生代分为三个区。一个Eden区,两个Survivor区(一般而言)。

大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。

年轻代:创建的新对象会被放入年轻代的eden空间,而年轻代gc采用复制算法,复制算法会把内存分为两个区域(即两个survivor空间:from和to)。当进行一次minor gc时(既年轻代的gc),minor gc是串行的,eden空间如果没有被gc root引用的会被回收,而依然存活的会被移动到from空间中,如果from空间在minor gc时对象依旧可以存活,就会对该对象年龄+1,当年龄达到一定数值时会直接放入老年代,没有达到年龄的存活对象会被复制到to中。这时from和eden空间已经被清空,虚拟机会交换from和to的空间,空的from变成to,to的变成from,保证了to是空的,minor gc会不断重复这样的工作,直到to彻底被填满,这时会将对象移动到老年代。)

新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。

--老年代

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。

老年代:老年代空间的对象是经过minor gc反复锤炼出来的。老年代使用并行的gc回收期,标记-清除算法,并且产生的是full gc(major gc)。老年代gc虽然是并行的,但full gc会同时对年轻代进行gc,所以大量的full gc会严重耗费jvm的性能,甚至卡死应用。另外可以大对象会直接分配到老年代,避免了在minor gc对两个survivor空间的复制耗时。

什么情况下触发垃圾回收

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC Full GC

        Scavenge GC

        一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对新生代的Eden区进行,不会影响到老年代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

         Full GC(Major GC)

         对整个堆进行整理,包括Young、Tenured 和 Perm。Full GC 因为需要对整个堆进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。

有如下原因可能导致Full GC:
         . 新生代(Tenured)被写满

         . 持久代(Perm)被写满

         . System.gc()被显式调用

         . 上一次GC之后Heap的各域分配策略动态变化

          https://blog.csdn.net/qq_35625303/article/details/79374964

 

对象的内存布局

在HotSpot虚拟机中。对象在内存中存储的布局分为 对象头、实例数据、对齐填充。

对象头

hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(如 哈希码、GC分代年龄、锁状态标志、线程持有的锁,偏向线程ID、偏向时间戳等)。

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

markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。

为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息,

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

另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据

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

对齐填充

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

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

对象的访问定位

java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄直接指针两种。

• 使用句柄,java堆中会划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。

使用句柄来访问的好处就是reference中存储的是稳定的句柄地址,在对象被移动时指挥改变句柄中的实例数据指针,而reference本身不需要修改。

• 使用直接指针,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址。

使用直接指针的好处是,速度更快,它节省了一次指针定位的时间开销,对象的访问在java中非常频繁,因此此类开销积少成多后也是一项非常可观的执行成本。

对象分配规则

对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在
Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。


长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次
Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达
到阀值对象进入老年区。


动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的
一半,年龄大于或等于该年龄的对象可以直接进入老年代。


空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,
如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查
HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

 

运行时数据区中哪些区域是线程共享的?哪些是独享的?

在JVM运行时内存区域中,PC寄存器、虚拟机栈和本地方法栈是线程独享的。

而Java堆、方法区是线程共享的。但是值得注意的是,Java堆其实还未每一个线程单独分配了一块TLAB空间,这部分空间在分配时是线程独享的,在使用时是线程共享的。(TLAB介绍

常见问题

什么是Native方法?

由于java是一门高级语言,离硬件底层比较远,有时候无法操作底层的资源,于是,java添加了native关键字,被native关键字修饰的方法可以用其他语言重写,这样,我们就可以写一个本地方法,然后用C语言重写,这样来操作底层资源。当然,使用了native方法会导致系统的可移植性不高,这是需要注意的。

成员变量、局部变量、类变量分别存储在内存的什么地方?

类变量

  • 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁
  • 在java8之前把静态变量存放于方法区,在java8时存放在堆中

成员变量

  • 成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
  • 由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中

局部变量

  • 局部变量是定义在类的方法中的变量
  • 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中

由final修饰的常量存放在哪里?

final关键字并不影响在内存中的位置,具体位置请参考上一问题。

类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?

类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中

在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;

对于文本字符来说,它们会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池;字符串常量池存储的是字符串对象的引用,而不是字符串本身。

什么是字面量?什么是符号引用?

字面量

java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:

int a=1;//这个1便是字面量

String b="iloveu";//iloveu便是字面量

符号引用

由于在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,所以如果你在一个类中引用了另一个类,那么你完全无法知道他的内存地址,那怎么办,我们只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取他的内存地址。

例子:我在com.demo.Solution类中引用了com.test.Quest,那么我会把com.test.Quest作为符号引用存到类常量池,等类加载完后,拿着这个引用去方法区找这个类的内存地址。

 

待续。。。

 

https://www.sohu.com/a/460308600_497772

posted @ 2019-02-17 12:34  Nausicaa0505  阅读(158)  评论(0编辑  收藏  举报