运行时数据区域(内存模型)

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器(也有人称PC 寄存器)。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java 虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,任何时候,一个处理器都只会执行一条线程中的指令。因此为了线程切换后能够恢复发到正确的位置,我们就需要程序计数器来记录程序执行的位置,当前线程重新拥有CPU 时可以继续执行剩下的代码。

每条线程都需要有一个独立的的程序计数器,各条线程间计数器互不影响,独立存储。我们称这类内存区域为”线程私有”。在JVM规范中规定,如果线程执行的是一个Java方法,则程序计数器中保存的是当前需要执行的指令的字节码地址;如果线程执行的是native方法,则程序计数器中的值是空(undefined)。前面我们提到了:程序计数器是一个很小的内存地址。此内存区域是唯一一个在Java 虚拟机中没有规定任何内存泄露(OutOfMemoryError)情况的区域,程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变。就像书签一样,不管卡在那,它自己不会变。

Java 虚拟机栈

Java 虚拟机栈(Java Vitual Machine Stack)跟C中栈有点类似。与程序计数器一样,Java 虚拟机栈也是线程私有的。

每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

程序每执行一个方法,就会分配一个栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。将到这里,大家就应该会明白为什么在使用递归方法的时候容易导致栈内存溢出的现象了。

局部变量表

局部变量表,顾名思义,就是用来存储在编译器可知的基本数据类型的变量(8种基本数据类型);对象引用(reference 类型, 对于引用类型的变量,存的是指向对象起始地址的引用指针)和returnAddress 类型(方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址)。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

操作数栈

操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

两种异常状况

StackOverflowError 异常 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常。 OutOfMemoryError 异常 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈和虚拟机栈发挥的作用十分相似。同样是线程私有,它们之间的区别不过是虚拟机栈为Java 方法服务,而本地方法栈为虚拟机使用到的Native 方法服务。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError 异常和OutOfMemoryError 异常。

Java 堆

Java 堆(Java Heap)是Java 虚拟机所管理的最大的一块内存。Java 堆是被所有线程共享。Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都是在这里分配内存。

所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化将会导致一些微妙的变化发生,所有的对象在堆上分配也变得不那么“绝对”了。

Java 堆是垃圾收集器的主要区域,因此很多时候也叫“GC”堆。

方法区

方法区(Method Area)与堆一样,是被各个线程共享的内存区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

运行时常量池(Runtime Constant Pool)是方法区的一部分,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将字符串常量池从永久代移除了。

直接内存

直接内存(Direct Memory)不是Java 虚拟机规范中定义的内存区域。

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

本机直接内存受本机总内存限制。

jvm垃圾回收机制

回收策略

复制回收算法

通过依次扫描区域所有的可达对象,然后将其复制到另外一片区域保存起来,再将其现在正在使用的区域内存全部清空,此方法的优点在于方便快捷,只需要便利出所有的可达对象即可,而且不会出现碎片化内存。但是缺点也很明显,复制对象需要计算成本,此外需要准备一个额外相同Eden区域大小的内存空间,也是一笔巨大的开销。

标记清除

这种方法首先遍历整个区域中的对象,然后标记所有的可达对象,再将所有内存中未被标记的对象全部清除。主要缺点在于会产生大量的碎片内存。

标记整理法

集上面两种算法的优点于一身,首先遍历整个空间对可达对象进行标记,然后再讲所有可达对象整理到一起去,最后清除掉不可达的对象,达到GC回收清理内存的目的。

检测对象是否有用方法

引用计数法

new出来一个对象之后,之后每次对该对象做了引用,那么就将该对象+1,在GC时,只需要判断该对象对应的count是否为0就可以轻松判断出是否还存在引用关系。

但是这种方法很不严谨,循环引用会使得其产生内存泄漏,永远无法释放这些资源。

根搜索算法

Jvm会起一个后台守护进程来维护一个树结构,如果发生引用就在树上维护一条边,那么同样的,如果这个引用被释放了,那么这个类也就和这个树失去了链接,从根节点GC Root对其进行搜索也搜索不到,便可以判断其对象已经无人再引用,可以释放。

内存回收

  1. 新产生的对象优先分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入年老代);
  2. 当Eden区满了或放不下了,这时候其中存活的对象会复制到from区。如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入年老代。之后Eden区的内存全部回收掉。
  3. 之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和from区存活下来的对象复制到to区(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和from区的所有内存。
  4. 会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了。
  5. 当年老代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC

垃圾回收有两种类型:Minor GC 和 Full GC

Minor GC

对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。

Full GC

也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。

jvm垃圾回收算法

标记清除

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。

在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

适用场合

存活对象较多的情况下比较高效,适用于年老代(即旧生代)

缺点

容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收。扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)

复制算法

从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉。现在的商业虚拟机都采用这种收集算法来回收新生代。

适用场合

存活对象较少的情况下比较高效,扫描了整个空间一次(标记存活对象并复制移动),适用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少

缺点

需要一块儿空的内存空间,需要复制移动对象

标记整理

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。

这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。

首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。

在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

总结

年轻代:复制算法

  1. 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  2. 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
  3. 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
  4. 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

年老代:标记-清除或标记-整理

  1. 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
  3. 每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好。

Minor GC和Full GC触发条件

Minor GC触发条件

  1. eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
  2. Full GC之前调用,仅适用于Parallel Scavenge(-XX:+UseParallelGC),虚拟机运行在Server模式下的默认收集器组。

Full GC触发条件

  1. 调用System.gc时,系统建议执行Full GC,但是不必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

GC中Stop the world

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。

STW总会发生 不管是新生代还是老年代 就算是CMS也有STW的时候。

双亲委派模型

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,加载器才会尝试自己去加载。

为什么采用双亲委派?

  1. Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  2. 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改

JDBC和双亲委派模型关系

JDBC破坏了双亲委派,因为类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到需要的文件,这时候就需要委托子类加载器去加载class文件。

JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类。我们知道,BootStrap类加载器默认只负责加载 $JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型。

java数组清空能释放jvm内存嘛_JVM面试题汇总相关推荐

  1. 深入理解Java虚拟机学习笔记-1.JVM内存模型

    JVM内存模型 1.内存模型结构图 名称 特征 作用 配置参数 异常 程序计数器 占用内存小,线程私有, 生命周期与线程相同 大致为字节码行号指示器 无 无 虚拟机栈 线程私有,生命周期与线程相同,使 ...

  2. java堆栈句柄,深入了解JVM—内存区域

    1.Java虚拟机运行时数据区 在前面的几篇博文中,我们只是简单的把内存区域分为了堆和栈,但其实,这种分法是十分粗糙的,jvm在实际运行的时候,内存区域的划分绝对不是那么简简单单的就两块,我们一起看下 ...

  3. java jvm内存分配_JVM系列一:JVM内存组成及分配

    java内存组成介绍:堆(Heap)和非堆(Non-heap)内存 按照官方的说法:"Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Java 虚 ...

  4. java 内存溢出的分类_【深入理解Java虚拟机】读后感:JVM内存划分与内存溢出小结...

    扫码关注公众号:Java 技术驿站 发送:vip 将链接复制到本浏览器,永久解锁本站全部文章 [公众号:Java 技术驿站] [加作者微信交流技术,拉技术群] # JVM内存划分与内存溢出小结 # 1 ...

  5. linux tomcat java heap space_Linux下tomcat JVM内存设置

    常见的内存溢出有以下两种: java.lang.OutOfMemoryError: PermGen space java.lang.OutOfMemoryError: Java heap space ...

  6. java direct memory_第一讲  JVM内存四大类型:Heap,Stack,Contant,DirectMemory等

    第一讲JVM内存四大类型:Heap,Stack,Contant,DirectMemory等 Stack属于栈的区域,属于每条线程私有的. 方法区和本地方法栈有很大的不同,方法区是用Java级别角度做的 ...

  7. 万万没想到,JVM内存结构的面试题可以问的这么难?

    在我的博客中,之前有很多文章介绍过JVM内存结构,相信很多看多我文章的朋友对这部分知识都有一定的了解了. 那么,请大家尝试着回答一下以下问题: 1.JVM管理的内存结构是怎样的?  2.不同的虚拟机在 ...

  8. jvm内存模型_JVM内存模型的相关概念

    1.前言 Android的虚拟机是根据移动设备的特点基于Java虚拟机(JVM)改进而来,虽然没有保留规范,但作为Java语言的使用者,了解一下JVM的规范还是有必要的. 2.JVM内存模型 JVM在 ...

  9. jvm内存结构_JVM系列之内存结构

    JVM的内存结构大概分为: 堆(Heap):线程共享.所有的对象实例以及数组都要在堆上分配.回收器主要管理的对象. 堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden ...

  10. 原创 | 万万没想到,JVM内存结构的面试题可以问的这么难?

    △Hollis, 一个对Coding有着独特追求的人△ 这是Hollis的第 223 篇原创分享 作者 l Hollis 来源 l Hollis(ID:hollischuang) 在我的博客中,之前有 ...

最新文章

  1. Google怎么用linux
  2. jetty服务器上运行html页面,web项目嵌入Jetty运行的两种方式(Jetty插件和自制Jetty服务器)...
  3. SpringMvc的执行过程
  4. 二分分类2.1 二分分类
  5. amazon php 空间,如何将PHP图像资源放入Amazon Web Services?
  6. 【岗位详情】腾讯广告大数据开发工程师(北京)
  7. sql server分页_SQL Server中的分页
  8. ODBC和JDBC是做什么的?为初学者理解概念问题
  9. 页脚保持在未满屏页面的底部
  10. 从蒙到入门——JavaEE完整体系架构
  11. 安卓ViewFlipper跑马灯效果
  12. arcgis怎么压缩tif文件_PDF文件怎么压缩才能变小?这样压缩,真的很简单!
  13. linux sed 替换 斜杠,sed命令替换字符包含斜杠\,引号的处理方法
  14. 前端 psd切片生成html.css,1个将PSD网页模板切片输出为DIV+CSS架构网页教程
  15. Wlan学习备忘(上)
  16. 融创孙喆一:父辈的光环与阴影下,我反对扮演所谓的二代
  17. 【古墓射手】隐私政策
  18. 读书笔记-高调做事低调做人 把握好度
  19. Windows下安装PyQt(python3.8+PyQt5)
  20. Buuctf-WEB-Havefun(WP)

热门文章

  1. JavaScript实现map
  2. php引用()详解及注意事项
  3. [题解]第十一届北航程序设计竞赛预赛——I.神奇宝贝大师
  4. Android设置标题栏图标
  5. Ubuntu中安装网易云音乐(可以直接打开的最简单的方法)
  6. java kafka 开发,Kafka JAVA API开发-基础案例
  7. mac推箱子c语言,c语言写的推箱子源码,非常适合新手学习
  8. python数据保存为excel_Python读excel生成数据存入txt文件
  9. android Activity生命周期总结
  10. log nginx 显示时间_【日常小知识系列01】Nginx日志简述