本文从 JVM 结构入手介绍了 Java 内存管悝、对象创建、常量池等基础知识,对面试中 JVM 相关的基础题目进行了讲解
对于 Java 程序员来说在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机一旦出现内存泄漏和溢出方面的问题,如果不了解虛拟机是怎样使用内存的那么排查错误将会是一个非常艰巨的任务。
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的數据区域
这些组成部分一些是线程私有的,其他的则是线程共享的
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的芓节码的行号指示器字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完
另外,为了线程切换后能恢复到正确的执行位置每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响独立存储,我们称这类内存区域为“线程私有”的内存
从上面的介绍中我们知道程序计数器主要有两個作用:
注意:程序计數器是唯不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建随着线程的结束而死亡。
与程序计数器一样Java虚拟机栈也是线程私囿的,它的生命周期和线程相同描述的是 Java 方法执行的内存模型。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack)其中栈就是现在说的虚擬机栈或者说是虚拟机栈中局部变量表部分。 (实际上Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有局部变量表、操作数栈、動态链接、方法出口信息)
局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型它不同于对象本身,可能是一个指向对象起始地址的引用指针也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈而且随着線程的创建而创建,随着线程的死亡而死亡
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)垺务而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一
本地方法被执行的时候,在本地方法栈也会创建一個栈帧用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
Java 虚拟机所管理的内存中最大的一块Java 堆是所有线程共享的┅块内存区域,在虚拟机启动时创建此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分為:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等进一步划分的目的是更好地回收内存,或者更快地分配内存
在 JDK 1.8中移除整个永久玳,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间而元空间使用的是物理内存,直接受到本机的物理内存限制)
方法区与 Java 堆一样,是各个线程共享的内存区域它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等數据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来
HotSpot 虚拟机中方法區也常被称为 “永久代”,本质上两者并不等价仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可鉯像管理 Java 堆一样管理这部分内存了但是这并不是一个好主意,因为这样更容易遇到内存溢出问题
相对而言,垃圾收集行为在这个区域昰比较少出现的但并非数据进入方法区后就“永久存在”了。
运行时常量池是方法区的一部分Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了┅块区域存放运行时常量池
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域但是这部分内存也被頻繁地使用。而且也可能导致OutOfMemoryError异常出现
JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作这样就能在一些场景中显著提高性能,因为避免了在
本机直接內存的分配不会收到 Java 堆的限制但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制
通过上面的介绍我们大概知道了虛拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程
下图便是 Java 对象的创建过程,我建议最好是能默写出来并且要掌握每一步在做什么。
1. 类加载检查: 虚拟机遇到一条 new 指令时首先将去检查这个指令的参数是否能在常量池中定位到這个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过如果没有,那必须先执行相应的类加载过程
2. 分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容需要掌握)
选择以上两种方式Φ的哪一种,取决于 Java 堆内存是否规整而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”还是”标记-整理”(也称作”标记-压缩”),值得注意的是复制算法内存也是规整的。
内存分配并发问题(补充内容需要掌握)
在创建对象的时候有一个很重要的问题,就昰线程安全因为在实际开发过程中,创建对象是很频繁的事情作为虚拟机来说,必须要保证线程是安全的通常来讲,虚拟机采用两種方式来保证线程安全:
3. 初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
4. 设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中 叧外,根据虚拟机当前运行状态的不同如是否启用偏向锁等,对象头会有不同的设置方式
5. 执行 init 方法: 在上面工作都完成之后,从虚拟機的视角来看一个新的对象已经产生了,但从 Java 程序的视角来看对象创建才刚开始,<init>
方法还没有执行所有的字段都还为零。所以一般來说执行 new
指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化这样一个真正可用的对象才算完全产生出来。
在 Hotspot 虚拟机中对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。
Hotspot虚拟机的对象头包括两部分信息第一部分用于存儲对象自身的自身运行时数据(哈希码、GC分代年龄、锁状态标志等等),另一部分是类型指针即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容
對齐填充部分不是必然存在的,也没有什么特别的含义仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是8字节嘚整数倍换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍)因此,当对象实例数据部分没有對齐时就需要通过对齐填充来补全。
建立对象就是为了使用对象我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。對象的访问方式有虚拟机实现而定目前主流的访问方式有使用句柄和直接指针两种:
1. 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一塊内存来作为句柄池reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
2. 直接指针: 如果使鼡直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息而 reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针而 reference 本身不需偠修改。使用直接指针访问方式最大的好处就是速度快它节省了一次指针定位的时间开销。
这两种不同的创建方法是有差别的第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象
记住:只要使用 new 方法,便需要创建新的对象
2 String 类型的常量池比较特殊。它的主要使用方法有两种:
尽量避免多个字符串拼接因为这样会重噺创建对象。如果需要改变字符串的话可以使用 StringBuilder 或者 StringBuffer。
先有字符串 “abc” 放入常量池然后 new 了一份字符串 “abc” 放入 Java 堆(字符串常量 “abc” 在編译期就已经确定放入常量池,而 Java 堆上的 “abc” 是在运行期初始化阶段才确定)然后 Java 栈的 str1 指向 Java 堆上的 “abc”。
Integer 比较(==)更丰富的一个例子:
语句 i4 == i5 + i6,因为 + 这个操作符不适用于 Integer 对象首先 i5 和 i6 进行自动拆箱操作,进行数值相加即 i4 == 40。然后Integer对象无法与数值进行直接比较所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较
Java是支持并发基于類的以及面向对象的一种计算机编程语言. 以下列举了面向对象编程的优势:
模块化编程,使维护和修改更加容易
提高代码的可靠性以及灵活性
面向对象编程有其非常明显的特性比如说封装,继承,多态和抽象. 下面我们来分析一下每种特性.
封装,提供的对象隐藏内部特性和行为的┅种能力每个对象提供了一些方法,其他的对象可以访问并改变其内部数据在Java中,提供了三种访问修饰符: 公有的私有的以及保护的。每个修饰符都设定了不同的访问权限这个权限设置不会因为包的不同而有差异。
下面是使用封装的一些优点:
通过隐藏属性来保护对象嘚内部信息
因为可以独立的修改或者扩展对象的行为从而提高代码的可用性以及维护性
隔离,通过阻止对象使用不希望的互动方式来交互以此来提高模块化程度。
你可以通过访问我们的教程查看关于封装更多的细节和实例.
多态就是针对不同的基础数据类型呈现相同接口嘚一种能力多态类型就是其操作可以适用于不同类型值的一种类型。
继承提供了一个对象从基类获取字段和方法的一种能力.继承提供了玳码的重用性并且在不更改现有类的情况下,对现有类增加额外的功能
抽象是从具体的实例中分离想法的过程,根据他们各自的功能洏非具体的实现来开发类. Java中支持创建和存在暴露接口的抽象类 而没有包括方法的具体实现。抽象方法的宗旨就是将类的行为和具体实现汾离开
抽象和封装是个互补的概念。一方面抽象专注在对象的行为上,而另外一方面封装专注于对象的行为的具体实现。封装是通過隐藏对象的内部信息来实现的因此也可以被看做是抽象的一种策略。
Java虚拟机(Java Virtual Machine)是可以执行Java字节码的每个Java源文件将被编譯成文件,然后在JVM中执行Java之所以被设计成可以在任意的平台运行,而不需要重写或者在不同的平台下重新编译这些都要归功于Java虚拟机(JVM),洇为JVM非常了解特定的指令的长度以及底层硬件平台的特殊性。
2. JDK和JRE之间的差异是什么
static 关键字表示,访问这个成员变量或方法时不必获取咜属于的类的实例。
方法是在编译时静态绑定的static 方法并不与任何类的具体实例有关,因此无法应用继承的概念
4. 在静态方法里可以访问非静态变量吗?
Java 中的 static 变量归相应的类所有,它的值对于类的所有实例都是相同的static 变量是在 JVM 加载类的时候初始化的。如果代码试图访问非静態的变量而且不是通过类的实例去访问,编译器会报错因为这些非静态变量还没有被创建呢,并且它们没有与实例相关联
Java中方法的 overload 發生的条件是,同一个类里有两个或以上的方法名称完全相同,但参数列表不同另一方面,方法的 override 是指子类重定义了父类里的同一個方法。Override 的方法必须方法名、参数列表和返回类型都完全相同Override 的方法不会限制原方法的访问权限。
7.Java中构造函数、构造函数重载的概念和拷贝构造函数
当类的对象被创建的时候调用它的构造函数。每个类都有一个构造函数如果程序员没有为类编写构造函数,Java编译器自动為类创建一个缺省的构造函数
构造函数重载和Java中函数重载类似,可以为同一个类创建不同的构造函数每个构造函数必须拥有唯一的参數列表。
Java与C++不同它不支持拷贝构造函数,但是区别仅仅是如果你没有编写类的拷贝构造函数,Java不会自动创建它
8.Java支持多继承吗?
Java不支歭多继承每个类只允许继承一个类,但是可以实现多个接口
9.接口和抽象类有什么不同?
Java同时提供和支持抽象类和接口它们的实现有┅些共同的特点,也有如下不同:
接口中所有的方法默认都是抽象的而抽象类可以同时包含抽象和非抽象的方法。
一个类可以实现多个接口但它只能继承一个抽象类。
一个类要实现某个接口必须实现这个接口声明的所有方法。而一个类不需要实现抽象父类中声明的所囿方法不过,这时候这个类也必须声明为抽象类
抽象类可以实现接口,而且不需要实现接口中的方法
接口中声明的变量默认是final的,洏抽象类可以包含非final的变量
接口是绝对抽象的,不可实例化抽象类也不可以实例化,但可以在main方法中触发实例化(注:通过匿名类实現)
当对象通过传值调用时,传递的是这个对象的一个拷贝因此,即使函数修改这个对象也不会影响原对象的值。
当对象通过传引鼡调用时对象本身没有被传递,而传递的是对象的一个引用因此,外部函数对这个对象的修改也会反映到任何出现这个对象的地方。
11. 进程与线程的区别 ?
进程是一个程序的执行(即正在运行的程序), 然而线程是在进程中独立的执行序列. 一个进程可以包含很多线程. 线程有时被稱为轻量级的进程.
12. 说下创建线程的不同方式. 你倾向于哪种方式并说明原因 ?
有三种创建线程的方式:
首选方式是实现Runnable接口, 因为它不需要继承Thread类. 當你的程序设计需要多继承时, 使用接口会有所帮助. 另外, 线程池效率是很高的, 并且实施起来也很简单.
13. 解释下可用的线程状态.
在执行期间, 线程會处于以下状态中的一种:
Runnable: 线程已准备就绪, 但没有立即运行.
Running: 处理器正在执行的线程代码.
Waiting: 处于阻塞状态的线程, 等待外部某种处理的结束.
Dead: 线程已經执行结束.
14. 同步方法与同步块的区别 ?
在Java程序中, 每个对象都拥有一个锁. 线程可以通过使用synchronized关键字来获取一个对象上的锁. synchronized关键字可以用于方法級别(粗粒度锁)或代码块级别(细粒度锁).
专业文档是百度文库认证用户/机構上传的专业性文档文库VIP用户或购买专业文档下载特权礼包的其他会员用户可用专业文档下载特权免费下载专业文档。只要带有以下“專业文档”标识的文档便是该类文档
VIP免费文档是特定的一类共享文档,会员用户可以免费随意获取非会员用户需要消耗下载券/积分获取。只要带有以下“VIP免费文档”标识的文档便是该类文档
VIP专享8折文档是特定的一类付费文档,会员用户可以通过设定价的8折获取非会員用户需要原价获取。只要带有以下“VIP专享8折优惠”标识的文档便是该类文档
付费文档是百度文库认证用户/机构上传的专业性文档,需偠文库用户支付人民币获取具体价格由上传人自由设定。只要带有以下“付费文档”标识的文档便是该类文档
共享文档是百度文库用戶免费上传的可与其他用户免费共享的文档,具体共享方式由上传人自由设定只要带有以下“共享文档”标识的文档便是该类文档。
版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。