Java 内存结构(运行时数据区)

程序计数器:线程私有。一块较小的内存空间,也是运行速度最快的存储区域。如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空。 (为什么要记录当前线程的执行地址? 因为 CPU 需要不停的切换各个线程,切换回来以后,就得知道接着从哪开始继续执行。)

Java 虚拟机栈:线程私有。它的生命周期与线程相同。虚拟机栈对应的是方法的内存区域,每个方法执行时都会创建一个栈帧,用来存储该方法的局部变量表,操作数栈,动态链接,方法返回地址等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表,存储三种类型的数据:

  1. 基本数据类型
  2. 引用类型:指向一个对象在内存中的地址
  3. returnAddress 类型:指向指令的地址(已经很少见了,指向异常处理的指令,现在已经由异常表代替

操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

动态链接:在运行时常量池中存储了诸如类名,方法名。通过名称找到相应的类和方法在内存中的地址,这个过程就是动态链接。

方法返回地址:方法返回之后调用者需要继续往下执行就需要知道要执行的地址,该地址就是方法返回地址,它被记录在了栈帧中。

本地方法栈:线程私有。本地方法栈与虚拟机栈的作用是非常相似的,区别在于虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 native 方法服务,native 方法为本地方法,不是用 Java 语言写的有可能是 c 或者 c++ 写的

:对大多数应用来说,堆是 Java 虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。堆的作用是存放对象实例,几乎所有的对象实例都在这里分配内存。

方法区:与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造方法、接口定义)、常量、静态变量、即时编译器编译后的代码(字节码)等数据。方法区是 JVM 规范中定义的一个概念,具体放在哪里,不同的实现可以放在不同的地方。

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

Java 中有哪几种常量池?

class 文件常量池、运行时常量池、字符串常量池

class 文件常量池:属于  class  文件的其中一项,用于存放编译期间生成的各种字面量(Literal)和符号引用(Symbolic References)字面量相当于 Java 里常量的概念,比如字符串,声明为 final 的常量值等。符号引用包括了:类和接口名,字段名,方法名。

运行时常量池:class  文件常量池是在类被编译成  class  文件时生成的。而当类被加载到内存中后,JVM 就会将  class  文件常量池中的内容存放到运行时常量池中。

字符串常量池:HotSpot VM 里的字符串常量池(StringTable)是个哈希表,全局只有一份,被所有的类共享。StringTable 具体存储的是 String 对象的引用,而不是 String 对象实例自身。String 对象实例在 JDK 6 及之前是在永久代里,从 JDK 7 开始放在堆里。

运行时常量池和字符串常量池的关联?

运行时常量池和字符串常量池在字符串解析时会有关联。

类的运行时常量池中有 CONSTANT_String_info(见题 3 表格)类型的常量,CONSTANT_String_info 类型的常量的解析(resolve)过程如下:

首先到字符串常量池(StringTable)中查找是否已经有了该字符串的引用,如果有,则直接返回字符串常量池的引用;如果没有,则在堆中创建 String 对象,并在字符串常量池驻留其引用,然后返回该引用。

String#intern  方法

在 JDK 7  及之后的版本中,该方法的作用如下:如果字符串常量池中已经有这个字符串,则直接返回常量池中的引用;如果没有,则将这个字符串的引用保存一份到字符串常量池,然后返回这个引用。

public static void main(String args[]) {
 
    // 创建2个对象,str持有的是new创建的对象引用
    // 1)驻留(intern)在字符串常量池中的对象
    // 2)new创建的对象
    String str = new String("joonwhee");
    // 字符串常量池中已经有了,返回字符串常量池中的引用
    String str2 = "joonwhee";
    // false,str为new创建的对象引用,str2为字符创常量池中的引用
    System.out.println(str == str2);
    // str修改为字符串常量池的引用,所以下面为true
    str = str.intern();
    // true
    System.out.println(str == str2);
}

永久代(PermGen)

永久代在  Java 8  被移除,一个重要原因是永久代经常出现 OOM。

永久代主要存储了三种数据:

  1. 类元数据,也就是方法区中包含的数据,除了编译生成的字节码被放在本地内存
  2. 字符串常量池中驻留引用的字符串对象(interned Strings),字符串常量池只驻留引用,而实际对象是在永久代中。
  3. 类静态变量

移除永久代后,interned Strings  和类静态变量被移动了堆中,类元数据被移动到了后来的元空间。

永久代和方法区的关系?

  • 方法区是 Java 虚拟机规范中定义的一种逻辑概念,而永久代是对方法区的实现。但是永久代并不等同于方法区,方法区也不等同于永久代。

  • 永久代中的 interned Strings 并不属于方法区,按规范:堆是存储 Java 对象的地方 ,这部分应该属于堆,因此永久代并不是只用于实现方法区。

  • 方法区中 JIT 编译生成的代码并不是存放在永久代,而是在本地内存中,因此可以说方法区也并不只是由永久代来实现。

元空间(metaspace)

  • 元空间在 Java 8 移除永久代后被引入,用来代替永久代,本质和永久代类似,都是对方法区的实现。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

  • 元空间主要用于存储 Class metadata(类元数据),根据其命名其实也看得出来

  • 可以通过 -XX: MaxMetaspaceSize 参数来限制元空间的大小,如果没有设置该参数,则元空间默认限制为机器内存

为什么引入元空间?

  • 在 Java 8 之前,Java 虚拟机使用永久代来存放类元信息,通过-XX: PermSize、-XX: MaxPermSize 来控制这块内存的大小,随着动态类加载的情况越来越多,这块内存变得不太可控,到底设置多大合适是每个开发者要考虑的问题。

  • 如果设置小了,容易出现内存溢出;如果设置大了,又有点浪费

  • 而元空间可以较好的解决内存设置多大的问题:当我们没有指定 -XX: MaxMetaspaceSize 时,元空间可以动态的调整使用的内存大小,以容纳不断增加的类

元空间能彻底解决内存溢出(Out Of Memory)问题吗?

元空间无法彻底解决内存溢出的问题,只能说是有所缓解。当内存使用完毕后,元空间一样会出现内存溢出的情况,最典型的场景就是出现了内存泄漏时。

如何判断对象是否可被回收

常见的判定方法有两种:引用计数法和可达性分析算法,HotSpot 中采用的是可达性分析算法。

引用计数法

  • 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;如果计数器为 0 ,说明对象不可能再被使用的。

  • 在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 正因为循环引用的存在,因此主流的 Java 虚拟机不使用引用计数算法。

可达性分析算法 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收。 在 Java 中 GC Roots 一般包含以下内容: GC Root 有哪些|seamless

介绍下四种引用(强引用、软引用、弱引用、虚引用)

  • 强引用:我们平时 new 了一个对象就是强引用,即使在内存不足的情况下,JVM 宁愿抛出 OutOfMemory 错误也不会回收这种对象。
  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次 GC 将会回收掉该对象(不管当前内存空间足够与否)。
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

垃圾收集有哪些算法?

垃圾回收算法

标记 - 清除

将存活的对象进行标记,然后清理掉未被标记的对象

特点:

  • 标记和清除过程效率都不高
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存

标记 - 整理

标记存活的对象,未被标记的对象会被清理;在清除的过程中,让存活的对象向前移动,避免产生空间碎片

特点:

  • 效率比标记 - 清除算法慢
  • 不会产生内存碎片

复制

将内存划分为大小相等的两块(from,to),每次只使用其中一块(from)。垃圾回收时将存活的对象复制到另一块上(to),同时对空间进行整理。然后把 from 区进行清理,最后调换 to 区和 from 区的位置(原来的 from 区变成 to 区,原来的 to 区变成 from 区)

特点:

  • 需要 2 倍内存;
  • 不会产生内存碎片。

现在的商业虚拟机都采用复制算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。 HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

  • 新生代(YoungGC): 复制算法
  • 老年代(OldGC): 标记 - 清除 或者 标记 - 整理 算法
指向原始笔记的链接

HotSpot 为什么要分为新生代和老年代?

  • 新生代(年轻代):新对象和没达到一定年龄的对象都在新生代
  • 老年代:被长时间使用的对象,老年代的内存空间应该要比年轻代更大

HotSpot 根据对象存活周期的不同将内存划分为几块,一般把堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

其中新生代又分为 1 个 Eden 区和 2 个 Survivor 区,通常称为 From Survivor 和 To Survivor 区。

新生代中 Eden 区和 Survivor 区的默认比例?

在 HotSpot 虚拟机中,Eden 区和 Survivor 区的默认比例为 8:1:1,即-XX: SurvivorRatio=8,其中 Survivor 分为 From Survivor 和 To Survivor,因此 Eden 此时占新生代空间的 80%

HotSpot GC 的分类?

  1. Partial GC:并不收集整个 GC 堆的模式,具体如下:

    • Young GC/Minor GC:只收集新生代的 GC。

    • Old GC:只收集老年代的 GC。只有 CMS 的 concurrent collection 是这个模式。

    • Mixed GC:收集整个新生代以及部分老年代的 GC,只有 G1 有这个模式。

  2. Full GC/Major GC:收集整个 GC 堆的模式,包括新生代、老年代、永久代(如果存在的话)等所有部分的模式。

HotSpot GC 的触发条件?

Young GC:当新生代中的 Eden 区没有足够空间进行分配时会触发 Young GC。

Full GC

  • 当准备要触发一次 Young GC 时,如果发现 Young GC 的平均晋升大小比目前老年代剩余的空间大,则不会触发 Young GC 而是转为触发 Full GC。(通常情况)

  • 如果有永久代的话,在永久代需要分配空间但已经没有足够空间时,也要触发一次 Full GC。

  • System.gc()默认也是触发 Full GC。

  • heap dump 带 GC 默认也是触发 Full GC。

  • CMS GC 时出现 Concurrent Mode Failure 会导致一次 Full GC 的产生。

Full GC 后老年代的空间反而变小?

假如做 Full GC 的时候,老年代里的对象几乎没有死掉的,而新生代又要晋升活对象上来,那么 Full GC 结束后老年代的使用量自然就上升了。

什么情况下新生代对象会晋升到老年代?

  • 对象每在 Survivor 区中“熬过”一次 Young GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁,可以通过-XX: MaxTenuringThreshold 设置),就将会被晋升到老年代中。

  • Young GC 后,如果对象太大无法进入 Survivor 区,则会通过分配担保机制进入老年代。

  • 如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

  • 如果新生代的垃圾收集器为 Serial 和 ParNew,并且设置了-XX: PretenureSizeThreshold 参数,当对象大于这个参数值时,会被认为是大对象,直接进入老年代。

介绍下垃圾收集机制(在什么时候,对什么,做了什么)

在什么时候

HotSpot GC 的触发条件?|seamless

对什么?

对那些 JVM 认为已经“死掉”的对象。即从 GC Root 开始搜索,搜索不到的,并且经过一次筛选标记没有复活的对象。

做了什么?

对这些 JVM 认为已经“死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。

GC Root 有哪些

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类的静态属性引用的对象
  • 方法区中的常量引用的对象
  • 所有被同步锁持有的对象
  • JNI(Java Native Interface)引用的对象

发生 Young GC 的时候需要扫描老年代的对象吗

在分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,如果回收新生代时也不得不同时扫描老年代的话,那么 Young GC 的效率可能下降不少。

在大多垃圾收集器中(G1 有不同的地方),通过 CardTable 来维护老年代对年轻代的引用,CardTable 可以说是 Remembered Set(RS)的一种特殊实现,是 Card 的集合。Card 是一块 2 的幂字节大小的内存区域,例如 HotSpot 用 512 字节,里面可能包含多个对象。CardTable 要记录的是从它覆盖的范围出发指向别的范围的指针。以分代式 GC 的 CardTable 为例,要记录老年代指向年轻代的跨代指针,被标记的 Card 是老年代范围内的。当进行年轻代的垃圾收集时,只需要扫描年轻代和老年代的 CardTable 即可保证不对全堆扫描也不会有遗漏。CardTable 通常为字节数组,由 Card 的索引(即数组下标)来标识每个分区的空间地址。

垃圾收集器有哪些?

垃圾收集器|seamless

介绍 CMS 垃圾收集器的特点

什么是 CMS

CMS(Concurrent Mark Sweep)收集器是一种以最短回收停顿时间为目标的收集器,它可以避免「老年代 GC」出现「长时间」的卡顿(在垃圾回收的时,用户线程都会完全停止)。

CMS 使用的是“标记—清除”算法,它的收集过程可以分为 5 个步骤,包括:初始标记、并发标记、并发预清理、重新标记、并发清除(初始标记以及重新标记这两个阶段会 Stop The World)

CMS 的工作流程

初始标记:STW(stop the world),遍历  GC Roots,标记  GC Root 直达的对象

并发标记:从「初始标记」阶段被标记为存活的对象作为起点,向下遍历,找出所有存活的对象。

由于该阶段是用户线程和 GC 线程并发执行,对象之间的引用关系在不断发生变化,这些对象是要重新标记的,否则就会出现错误。为了提升重新标记的效率,JVM 会使用写屏障(write barrier)将引用关系发生变化的对象所在的区域对应的 card 标记为 dirty,后续只需要扫描这些 dirty card 区域即可,避免扫描整个老年代。

并发预处理:这个阶段是为了减少「重新标记」所消耗的时间,因为「重新标记」会 STW

「并发预处理」会再扫描一遍标记为 dirty 的卡页,处理「并发标记」阶段发生引用变化的对象。

重新标记:STW,主要做两件事:

  • 遍历 GCRoots,重新扫描标记

  • 遍历被标记为 dirty 的 card,重新扫描标记

并发清理:清理未使用的对象并回收它们占用的空间。清理完了以后会重置 CMS 算法相关的内部数据,为下一次 GC 做准备。

CMS 的缺点

CMS 是一款优秀的收集器,并发收集、低停顿,但是它有以下 3 个明显的缺点:

  1. 使用的标记-清除算法,可能存在大量空间碎片。
  2. CMS 收集器对 CPU 资源非常敏感。在并发阶段,会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会降低。
  3. CMS 收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生。

介绍下 G1 垃圾收集器的特点

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。G1 是一款面向服务端应用的垃圾收集器。与其他 GC 收集器相比,G1 具备如下特点:并行与并发、分代收集、空间整合、可预测的停顿。

在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。G1 收集器将整个 Java 堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。

G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。

Mixed GC 是 G1 垃圾收集器特有的收集方式,Mixed GC 大致可划分为全局并发标记(global concurrent marking)和拷贝存活对象(evacuation)两个大部分:

「全局并发标记」是基于 SATB 形式的并发标记,包括 4 个阶段:初始标记、并发标记、最终标记、清理。「拷贝存活对象」阶段是全暂停的。它负责把一部分 region 里的活对象拷贝到空 region 里去,然后回收原本的 region 的空间。

类加载的过程

类加载的过程包括:加载、验证、准备、解析、初始化(验证、准备、解析统称为连接)

加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的 java.lang.Class 对象。

验证:确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值 “通常情况” 下是数据类型的零值。

解析:将常量池内的符号引用替换为直接引用。

初始化:到了初始化阶段,才真正开始执行类中定义的 Java 初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。

Java 虚拟机中有哪些类加载器

分类

启动类加载器(Bootstrap ClassLoader): 它用来加载 <JAVA_HOME>\jre\lib 目录中的,或者被 -Xbootclasspath 参数指定的路径中的,并且是虚拟机识别的类库,加载到虚拟机内存中。

扩展类加载器(Extension ClassLoader): 这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 <JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器(Application ClassLoader): 由 sun.misc.Launcher$AppClassLoader 实现。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类加载器: 用户根据自定义需求,自由地定制加载的逻辑,继承 AppClassLoader,重写 findClass(),遵守双亲委派模型。

指向原始笔记的链接

什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

使用双亲委派模型的好处?

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。