龙空技术网

字节跳动面试:JVM 一问三不知?来看看这个

程序员小橙 125

前言:

如今咱们对“java获取字节大小”大体比较看重,大家都想要分析一些“java获取字节大小”的相关知识。那么小编在网摘上网罗了一些对于“java获取字节大小””的相关内容,希望你们能喜欢,同学们快快来了解一下吧!

大家好!今天来学习Java虚拟机相关内容,作为面试必问的知识点,来深入了解一波!

思维导图:

1 关于JVM的介绍1.1 概论

JVM是 Java虚拟机的缩写形式。引入Java虚拟机后,Java在各个平台上运行时 不需要重新编译。Java语言编译程序只需生成在Java虚拟机上运行的目标代码,就可以在多种平台上轻松的运行,我们不需要对其进行任何改动。而平台上,只要装了对应的Java虚拟机,字节码文件(.class)就可以在该平台上运行。

java的跨平台性,是由虚拟机决定的,java文件的执行通过jvm虚拟机执行,不直接在操作系统上执行,如下图,JVM是直接与操作系统交互,用以执行java程序。

1.2 JVM运行流程

JVM有四个部分:

类加载器执行引擎本地接口运行时数据区2 JVM的内存区

对java程序员来说,在jvm的自动内存管理机制下,不需要像C或者C++程序员一样,为每一个对象去写 delete 或者 free 代码,因为很少出现内存泄漏、溢出的问题。但一旦出现内存泄漏或者内存溢出的问题,如果程序员对jvm内存结构不清楚,那么排查BUG将会成为一项比较令人头疼的工作。

运行时数据区:

2.1 程序计数器

在虚拟机的概念模型里, 字节码解释工作就是通过改变这个程序计数器的值来选取下一个要执行的字节码指令。像分支控制,循环控制,跳转,异常等操作和等功能都是通过该计数器完成。

如果线程执行的是Java方法时,程序计数器记录字节码指令的地址 ,执行 Native时,程序计数器为空,因为 Java 虚拟机调用的是操作系统的接口,接口的实现是 C语言和 C++,不是Java。

程序计数器是内存区域,但是它很小,也是唯一一个在Java虚拟机中不会出现 OutOfMemoryError 的内存区域,生命周期随线程的创建开始,随线程的结束而结束。

2.2 Java堆

Java堆是JVM所管理的内存中最大的一块区域, 它的唯一作用就是存放对象实例,负责分配所有的对象实例以及数组的内存。

Java堆也叫做GC堆 。Java堆可以分为:新、老年代。根据JVM的规范规定,Java堆可以处于物理上不连续的内存空间,逻辑上连续即可。如果没有完成内存分配,也没有可扩展的空间,则会抛出 OutOfMemoryError 异常。

2.3 方法区

方法区是各个线程共享的内存区域,它存储被虚拟机加载的各类数据。

Java虚拟机相对其他的来说,对方法区的限制很少,垃圾回收在这个区域是比较少的了,数据进入方法区以后并非可以实现永久存活。尤其是类型的卸载,条件相当苛刻。当方法区无法满足内存分配时,将抛出 OutOfMemoryError 异常。

2.4 Java虚拟机栈

虚拟机栈描述的是Java方法的执行内存模型 ,每个方法在执行的时候都会创建一个栈帧。

局部变量表存放八种基本数据类型 :boolean、byte、char、short、int、float、long、double, 对象引用 (reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java虚拟机会出现两种异常状况:

如果线程在栈中,申请的深度 > 虚拟机所允许的深度 ,将出现 StackOverFlowError 异常;如果虚拟机栈可以动态扩展,且扩展无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

2.5 本地方法栈

本地方法栈与虚拟机栈的作用非常像,区别是:虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务,在 HotSpot 虚拟机中直接将虚拟机栈和本地方法栈合二为一。

本地方法栈在执行的时候会创建一个栈帧 。也会像虚拟机栈,抛出 StackOverFlowError 异常和 OutOfMemoryError 异常。

举例

我们来看看上述的哪些信息会存放上方法区中;

静态变量和常量,在编译期间就放在方法区中;

我们先来看看new String时堆中的变化:

输出结果是F,栈种存放的地址不同,所以是不相等的。

在引用类型中,"=="指的是地址值的比较。

双引号直接写的字符串是在常量池之中,而new的对象则不在池之中。

运行期间添加进常量池的:

2. 6 运行时常量池

Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息。运行时常量池时方法区的一部分,受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

2.7 直接内存

直接内存一般不受分配内存的影响,但既然是内存,肯定会被本机总内存以及处理器寻址空间的限制。管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,从而导致动态扩展时出现 OutOfMemoryError 异常。

3 Java对象的创建过程

下面这张图就是Java对象创建的过程,总共来说分为五部分;

3.1 类加载过程

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,如果没有被加载过,必须先执行相应的类加载过程。

3.2 分配内存

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

指针碰撞:

场景:Java堆中内存是绝对规整的;原理:所有用过的内存都放在一边,空闲的内存放在另外一边,中间放一个指针作为分界点的指示器,分配内存时只需要把那个指针向空闲空间那边挪动一段与对象大小相等的距离就可以了;GC收集器:Serial、ParNew等带Compact过程的收集器。

空闲列表:

场景:Java堆中内存不是规整的;原理:虚拟机会维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录;GC收集器:CMS基于Mark-Sweep算法的收集器。

内存分配并发的问题

在创建对象的时候必须要保证线程安全,解决这个问题有两种方案:

CAS以及失败重试:对分配内存空间的操作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。CAS操作需要输入两个数值,一个旧值(操作前期望的值)和一个新值,在操作期间先比较旧值有没有发送变化,如果没有变化,才交换成新值,否则不进行交换。TLAB:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲。TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。3.3 初始化零值

内存分配完成后,虚拟机为了保证对象的实例字段不赋初始值就可以直接使用,需要将分配到的内存空间都初始化为零值。

3.4 设置对象头

初始化零值完成后, Java虚拟机要对对象进行必要的设置,要把很多信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,对象头要根据其状态选择不同的设置方式。

3.5 执行Init方法

一般执行 new 指令之后,会接着执行 <init> 方法,把对象按照程序员的想法来初始化,这样一个真正可用的对象才算诞生。

4 对象的访问定位

Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有 使用句柄 和 直接指针 两种。

4.1 使用句柄

如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池, reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。如图所示:

4.2 直接指针

使用直接指针访问时, 对象的布局就必须考虑到放置访问类型数据的相关信息这个问题,而在reference 中存储的是对象的地址。

使用句柄访问的优势是reference 中存储的是稳定的句柄地址,对象被移动时只改变句柄中的实例数据指针。 直接指针访问方式优势是节省时间。

5 OutOfMemoryError异常

在Java虚拟机规范的描述中,除了程序计算器之外, 虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。

5.1 堆内存异常

先要设置Java虚拟机的参数。由于IDEA默认设置的堆内存很大,所以我们需要单个配置; 点击Run >> Edit Configurations ,然后就开始配置,如下:

运行结果:

堆内存发生异常,上面的 **死循环中我们不断地new对象,导致堆内存已经耗尽。

5.2 栈内存异常

我们设置递归方法,不给跳出的条件:

以上就是使用不合理的递归造成的

第二种异常情况:

运行结果是: Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread ,但是运行这段代码可能会导致操作系统卡顿。

以上就是本篇文章的全部内容,欢迎关注评论。

标签: #java获取字节大小