Java面试08——JVM知识点汇总

1.JVM内存结构

在这里插入图片描述
上图是Java8之前,Java8使用元空间替代方法区
在这里插入图片描述

程序计数器

概述:较小的内存空间,为当前线程执行的字节码的行号指示器
作用:通过改变计数器的值来指定下一条需要执行的字节码指令,来恢复中断前程序运行的位置
特点:

  • 线程私有化,每个线程都有独立的程序计数器。
  • 唯一一个在Java虚拟机中无内存溢出的区域。

Java虚拟机栈

概述:虚拟机栈描述的是Java方法执行的内存模型。每个方法从调用直到执行的过程,对应着一个栈帧在虚拟机栈的入栈和出栈的过程
作用:每个方法执行都创建一个“栈帧”来存储局部变量表、操作数栈、动态链接、方法出口等信息。其中局部变量表存放了编译器可知的各种基本类型(boolean byte char short int float long double)、对象引用(reference类型,他不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节吗指令的地址)。

特点:

  • 线程私有化
  • 生命周期与线程执行结束相同
    在这里插入图片描述
    可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
    java -Xss512M HackTheJava

该区域可能抛出以下异常:
当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常

本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
在这里插入图片描述
方法区的大小设置:

-XX:PermSize=N //⽅法区 (永久代) 初始⼤⼩
-XX:MaxPermSize=N //⽅法区 (永久代) 最⼤⼤⼩,超过这个值将会抛出 OutOfMemoryError 异 常:java.lang.OutOfMemoryError: PermGen

相对⽽⾔,垃圾收集⾏为在这个区域是⽐᫾少出现的,但并⾮数据进⼊⽅法区后就“永久存在”
了。

元空间

JDK 1.8 的时候,⽅法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取⽽代
之是元空间,元空间使⽤的是直接内存。
元空间的大小设置:

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最⼩⼤⼩)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最⼤⼤⼩

与永久代很⼤的不同就是,如果不指定⼤⼩的话,随着更多类的创建,虚拟机会耗尽所有可⽤的
系统内存。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

  1. 整个永久代有⼀个 JVM 本身设置固定⼤⼩上限,⽆法进⾏调整,⽽元空间使⽤的是直接内
    存,受本机可⽤内存的限制,虽然元空间仍旧可能溢出,但是⽐原来出现的⼏率会更⼩。
    当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace
    你可以使⽤ -XX MaxMetaspaceSize 标志设置最⼤元空间⼤⼩,默认值为 unlimited,这意味着
    它只受系统内存的限制。 -XX MetaspaceSize 调整标志定义元空间的初始⼤⼩如果未指定此标
    志,则 Metaspace 将根据运⾏时的应⽤程序需求动态地重新调整⼤⼩。
  2. 元空间⾥⾯存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了,
    ⽽由系统的实际可⽤空间来控制,这样能加载的类就更多了。
  3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有⼀个叫永久代的东⻄, 合并
    之后就没有必要额外的设置这么⼀个永久代的地⽅了。

Java堆是Java虚拟机所管理的内存中最大的一块。他被所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的时存放对象实例,几乎所有的对象实例都在这里分配内存
因而,堆是垃圾收集的主要区域(“GC 堆”)。

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava

创建时间:JVM启动时创建该区域
占用空间:Java虚拟机管理内存最大的一块区域

作用:用于存放对象实例及数组(所有new的对象)

特点:

  • 垃圾收集器作用该区域,回收不使用的对象的内存空间
  • 各个线程共享的内存区域
  • 该区域的大小可通过参数设置

方法区

作用:是各个线程共享的内存区域。它用于存储类信息、常量、静态变量、即时编译后的代码等数据。

对这块区域进行垃圾回收的主要目标是***对常量池的回收和对类的卸载***,但是一般比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中

补充:⽅法区和永久代的关系
《Java 虚拟机规范》只是规定了有⽅法区这个概念和它的作⽤,并没有规定如何去实现它。那么,在不同的 JVM 上⽅法区的实现肯定是不同的了。 ⽅法区和永久代的关系很像Java 中接⼝和类的关系,类实现了接⼝,⽽永久代就是 HotSpot 虚拟机对虚拟机规范中⽅法区的⼀种实现⽅式。 也就是说,永久代是 HotSpot 的概念(元空间道理一样),⽅法区是 Java 虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久代这⼀说法

运行时常量池

(2020.1.5 更新)
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息时常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这些内容将在类加载后进入方法区的运行时常量池中存放。
既然运⾏时常量池是⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛出 OutOfMemoryError 错误。

补充:

1. JDK1.7之前运⾏时常量池逻辑包含字符串常量池存放在⽅法区, 此时hotspot虚拟机对⽅法区的实现为永久代
2. JDK1.7 字符串常量池被从⽅法区拿到了堆中, 这⾥没有提到运⾏时常量池,也就是说字符串常量池被单独拿到堆,运⾏时常量池剩下的东⻄还在⽅法区, 也就是hotspot中的永久代 。
3. JDK1.8 hotspot移除了永久代⽤元空间(Metaspace)取⽽代之, 这时候字符串常量池还在堆, 运⾏时常量池还在⽅法区, 只不过⽅法区的实现从永久代变成了元空间
(Metaspace)

2.Java对象模型

Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。

每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。
在这里插入图片描述
注意:
1.JVM内存结构,和Java虚拟机的运行时区域有关。
2.Java对象模型,和Java对象在虚拟机中的表现形式有关。

对象的创建及内存分配

(2020.1.6 更新)
在面向对象的编程语言中,创建对象通常是简单的使用new关键字。但在JVM中,遇到一条new指令时,首先将去检查这个指令的参数是否在常量池中定位到类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
分配内存的方式:

  1. 指针碰撞(Bump the pointer):针对Java堆中内存是绝对规整的,用过和没用过的内存各放一边,中间一个指针作为分界点的指示器,这样分配内存就变成把那个指针指向空闲空间移动的问题。
  2. 空闲列表(Free List):针对Java堆中内存是不规整的,已使用的和未使用的内存相互交错,JVM维护一个列表,记录上哪些内存是可用的,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

实际应用中,具体使用的是哪种内存分配方式有Java堆中的内存是否规整决定。在使用Serial、ParNew等Compact过程的垃圾收集器时,系统使用的方法是指针碰撞,而是用CMS这种基于Mark-Sweep算法的收集器时采用的是空闲列表。

除此之外,还需要考虑的问题是对象创建在虚拟机中是否是频繁的行为,这在并发下不是线程安全的。通常的解决方案有两种:

  • 对分配内存空间的动作进行同步处理——实际上是JVM采用CAS配上失败重试的方法保证更新操作的原子性。
  • 把分配内存的动作按照线程划分到不同的空间之中进行,也就是每个线程在Java堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储的布局可以分为3块内存区域:对象头(Header)、实例数据(Instance Data)和对象填充(Padding)
在这里插入图片描述
实例数据部分是对象真正存储的有效信息,也是在代码中多定义的各种类型的字段内容。

对象填充不是必然存在的,也没有特殊意义,仅仅起着占位符的作用。

3.垃圾回收(GC) 非常重要!

在Java的运行时数据区中,程序计数器、虚拟机栈、本地方法栈三个区域都是线程私有的,随线程而生,随线程而灭,在方法结束或线程结束时,内存自然就跟着回收了,不需要过多考虑回收的问题。
而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的是方法区进行。

GC主要回答了以下三个问题:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

就这三个问题,接下来做具体叙述。

对象存活判定算法

在堆里存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,首要的就是确定这些对象中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。

1.引用计数算法
引用计数算法是在JVM中被摒弃的一种对象存活判定算法,不过它也有一些知名的应用场景(如Python、FlashPlayer),因此在这里也简单介绍一下。

用引用计数器判断对象是否存活的过程是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。

引用计数算法的实现简单,判定效率也很高,大部分情况下是一个不错的算法。它没有被JVM采用的原因是它很难解决对象之间循环引用的问题

下面这个例子,在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

public class Test {
    public Object instance = null;
    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
	    a=null;
		b=null;
		System.gc();	
    }
}

对象a 和对象b都有字段instance,赋值令a.instance = b;b = a;除此之外,这两个对象再无引用。如果JVM采用引用计数算法来管理内存,这两个对象不可能再被访问,但是他们互相引用着对方,导致它们引用计数不为0,所以引用计数器无法通知GC收集器回收它们。

而事实上执行这段代码,a和b是可以被回收的,下面一节将介绍JVM实际使用的存活判定算法。

2.可达性分析算法
将 GC Roots 作为起始点进行搜索,可达的对象都是存活的ÿ

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值