Java内存区域
1.说一下 JVM 的主要组成部分及其作用?
![](https://img-blog.csdnimg.cn/direct/374a1952cbd34626ae9710d42b8d9a5d.png)
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、
Execution engine(执行引擎);两个组件为Runtime data area(运行时数据
区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名(如:
java.lang.Object)来装载class文件到Runtime data area中的method
area。
Execution engine(执行引擎):执行classes中的指令。
Native Interface(本地接口):与native libraries交互,是其它编程语
言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内
存。
作用
:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)
再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方
法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作
系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将
字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他
语言的本地库接口(Native Interface)来实现整个程序的功能。
2.下面是Java程序运行机制详细说明
Java程序运行机制步骤
首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名
为.class;
运行字节码的工作是由解释器(java命令)来完成的。
![](https://img-blog.csdnimg.cn/direct/2206254a4ebb4d15b2d0672a548087dd.png)
在这里插入图片描述
从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这
些.class文件加载到JVM中。
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入
到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个
java.lang.Class对象,用来封装类在方法区内的数据结构。
3.说一下 JVM 运行时数据区
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个
不同的数据区域
。这些区域都有各自的用途,以及创建和销毁的时间,有些区域
随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销
毁。Java 虚拟机所管理的内存被划分为如下几个区域:
![](https://img-blog.csdnimg.cn/direct/3b473e0c2252403885f4baad885f43ae.png)
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范,
Java 虚拟机规范规定的区域分为以下 5 个部分:
程序计数器(Program Counter Register):当前线程所执行的字节码的行号
指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的
字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个
计数器来完成;
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作
数栈、动态链接、方法出口等信息;
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚
拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享
的,几乎所有的对象实例都在这里分配内存;
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变
量、即时编译后的代码等数据。
3.深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加
的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的
错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来
的对象也会相应的改变。
深复制:在计算机中开辟一块
新的内存地址
用于存放复制的对象。
4.处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的
位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还
没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个
问题有两种方案:
对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的
原子性);
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆
中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,
TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并
分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使
用TLAB。
![](https://img-blog.csdnimg.cn/direct/e7851ac68112484eba6094ef8aac6b03.png)
5.Java会存在内存泄漏吗?请简单描述
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,
Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收
掉,自动从内存中清除。
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因
很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,
尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导
致不能被回收,这就是java中内存泄露的发生场景。
6.简述Java垃圾回收机制
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行
执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会
执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没
有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
7.垃圾回收的优点和原理。并考虑2种回收机制
java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时
不再考虑内存管理的问题。
由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的 对象才有“作用域”。
垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。
垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存
堆中已经死亡的或很长时间没有用过的对象进行清除和回收。
程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。
垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。
8.说一下 JVM 有哪些垃圾回收算法?
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清
除垃圾碎片。
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的
对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不
高,只有原来的一半。
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清
除掉端边界以外的内存。
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年
代,新生代基本采用复制算法,老年代采用标记整理算法。
标记-清除算法
标记无用对象,然后进行清除回收。
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收
集分为两个阶段:
标记阶段:标记出可以回收的对象。
清除阶段:回收被标记的对象所占用的空间。
标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法
的基础上进行改进的。
优点
:实现简单,不需要对象进行移动。
缺点
:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的
频率。
标记-清除算法的执行的过程如下图所示
![](https://img-blog.csdnimg.cn/direct/0ee189d007134e879a4fb3f2b69e5629.png)
复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划
为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区
域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象
进行回收。
优点
:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点
:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程如下图所示
![](https://img-blog.csdnimg.cn/direct/6ba1c0421fe14e9bb82a177e5d37485f.png)
标记-整理算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年
代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除
算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎
片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理
算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使
他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未
用的内存都各自一边。
优点
:解决了标记-清理算法存在的内存碎片问题。
缺点
:仍需要进行局部对象移动,一定程度上降低了效率。
标记-整理算法的执行过程如下图所示
![](https://img-blog.csdnimg.cn/direct/d93b23cceead463ea5542876efe887e9.png)
分代收集算法
当前商业虚拟机都采用
分代收集
的垃圾收集算法。分代收集算法,顾名思义是根
据对象的
存活周期
将内存划分为几块。一般包括
年轻代
、
老年代
和
永久代
,如
图所示:
![](https://img-blog.csdnimg.cn/direct/2556e42357c4408f9c607f92a76b1b0c.png)
9.说一下 JVM 有哪些垃圾回收器?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体
实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器
包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial
Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器
之间的连线表示它们可以搭配使用。
img
Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点
是简单高效;
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程
版本,在多核CPU环境下有着比Serial更好的表现;
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效
利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高
效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不
高的场景;
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年
代版本;
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,
Parallel Scavenge收集器的老年代版本;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集
器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最
短GC回收停顿时间。
G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是
JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会
产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的
范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代
或老年代。
![](https://img-blog.csdnimg.cn/direct/76117557808a4ddf86363fa88c839471.png)
10.简述java内存分配与回收策率以及Minor GC和Major
GC
所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面我
们介绍了内存回收,这里我们再来聊聊内存分配。
对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场
景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,
如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直
接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种
垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵
循以下几种「普世」规则:
对象优先在 Eden 区分配
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行
分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空
间,则将启用分配担保机制在老年代中分配内存。
这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中
发现 Major GC/Full GC。
Minor GC
是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所
有 Minor GC 非常频繁,一般回收速度也非常快;
Major GC/Full GC
是指发生在老年代的 GC,出现了 Major GC 通常会伴
随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导
致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对
象。
前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象
直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复
制。因此对于大对象都会直接在老年代进行分配。
长期存活对象将进入老年代
虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应
该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对
象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被
移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬
过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升
到老年代。
11.简述java类加载机制?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初
始化,最终形成可以被虚拟机直接使用的java类型。
12.描述一下JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也
是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时
候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊
的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用
类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证
程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候
才加载。这当然就是为了节省内存开销。
13.什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被
java程序直接引用。
2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。
Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找
并加载 Java 类。
3. 系统类加载器(system class loader):它根据 Java 应用的类路径
(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来
完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取
它。
4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实
现。
14.说一下类装载的执行过程?
类装载分为以下 5 个步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为
一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
15.什么是双亲委派模型?
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的
类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一
个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到
JVM 内存,然后再转化为 class 对象。
![](https://img-blog.csdnimg.cn/direct/95932bca4d114a81a66ccc59fb83a5e3.png)
类加载器分类:
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载
Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚
拟机识别的类库;
其他类加载器:
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext.
dirs系统变量指定的路径中的所有类库;
应用程序类加载器(Application ClassLoader)。负责加载用户类路径
(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我
们没有自定义类加载器默认就是用这个加载器。
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载
这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如
此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无
法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加
载类。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父
类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加
载。