一、JVM概述

1、说一下JVM的主要组成部分及其作用?

(1)组成
image.png
① Class loader(类加载器):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到方法区
② Execution engine(执行引擎):执行classes中的指令
③ Native Interface(本地接口):与本地方法库交互,是其它编程语言交互的接口。
④ Runtime data area(运行时数据区):JVM的内存
(2)作用
首先通过类加载器(ClassLoader)会把Java代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

2、什么是JVM?

Java虚拟机。Java程序运行的时候,编译器将Java文件编译成平台无关的Java字节码文件(.class),接下来对应平台JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。
同时JVM也是一个跨语言的平台,和语言无关,只和class的文件格式关联,任何语言,只要能翻译成符合规范的字节码文件,都能被JVM运行。


二、类加载子系统(ClassLoader)

1、说一下Java怎么加载类的?

image.png
(1)加载(Loading)
①通过一个类的全限定名获取定义此类的二进制字节流
②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
(2)链接(Linking)
① 验证(Verification)
确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
② 准备(Prepare)
为类变量分配内存并且设置该类变量的默认初始值
③ 解析(Resolution)
将常量池内的符号引用转换为直接引用的过程
(3)初始化(Initialization)
执行类构造器方法()的过程
image.png

2、什么是双亲委派模型?

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

3、说一下类加载器

(1)概述
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在JVM中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM内存,然后再转化为 class 对象。
(2)分类
①启动类加载器(Bootstrap ClassLoader)
虚拟机自身的一部分,用来加载JAVA_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库
②其他类加载器

  • 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库;
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上的指定类库,可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

    4、为什么要有双亲委派机制?

    (1)避免类的重复加载
    (2)保护程序安全,防止核心API被随意篡改

三、运行时数据区

1、简单聊聊 Java 虚拟机栈

(1)线程私有,生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
(2)几乎所有的Java方法调用都是通过栈来实现的,方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
(3)栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
①局部变量表:主要存放了编译期可知的各种数据类型(如int、double等)和对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
②操作数栈:用于存放方法执行过程中产生的中间计算结果和计算过程中产生的临时变量。
③动态链接:服务一个方法需要调用其他方法的场景,将符号引用转换为调用方法的直接引用。在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在Class文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。
(4)Java的两种返回方式:①return语句正常返回;②抛出异常。两种方式都会导致栈帧被弹出
(5)栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
(6)程序运行中栈可能会出现两种错误:
StackOverFlowError:若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError:如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

2、说一下JVM运行时数据区(内存区域)?

JVM - 图4
(1)程序计数器(Program Counter Register)
当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成
(2)Java 虚拟机栈(Java Virtual Machine Stacks)
线程私有的,它的生命周期与线程相同。
方法执行时,JVM会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息(服务Java方法的),每个方法执行的过程就对应了一个入栈和出栈的过程。
(3)本地方法栈(Native Method Stack)
与虚拟机栈的作用是一样的,本地方法栈为虚拟机调用 Native 方法服务
(4)Java 堆(Java Heap)
Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存
(5)方法区(Methed Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据

3、说一下堆与栈的区别?

(1)物理地址
堆的物理地址分配对对象是不连续的,性能较慢;栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的,所以性能较快。
(2)内存分配
堆是不连续的,所以分配的内存是在运行期确认的,大小不固定,一般堆大小远远大于栈;栈是连续的,所以分配的内存大小在编译期就确认,大小是固定的。
(3)存放的内容
堆存放的是对象的实例和数组(更关注数据的存储);栈存放的是局部变量,操作数栈,返回结果(更关注程序方法的执行)
(4)程序的可见度
堆对于整个应用程序都是共享、可见的;栈是线程私有的,只对线程可见。

4、队列和栈是什么?有什么区别?

队列和栈都是被用来预存储数据的。
(1)操作名称不同
队列是入队出队;栈是入栈出栈
(2)可操作的方式不同
队列是在队尾入队,队头出队,两边均可操作;栈的出栈和入栈都在栈顶进行。
(3)操作方法不同
队列是先进先出;栈是后进先出

5、说一下JDK1.6、1.7、1.8内存区域的变化?

JDK1.6使用永久代实现方法区:
JVM - 图5
JDK1.7时发生了一些变化,将字符串常量池、静态变量,存放在堆上:
JVM - 图6
JDK1.8时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间:
JVM - 图7

6、为什么使用元空间替代永久代作为方法区的实现?

客观上使用永久代来实现方法区的决定的设计导致了Java应用更容易遇到内存溢出的问题,而且有极少数方法 (例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。

7、Java堆的内存分区了解吗?

将Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域。
新生代存放存活时间短的对象,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。新生代又可以分为三个区域,eden、from、to,比例是8:1:1。

8、对象一定分配在堆中吗?有没有了解逃逸分析技术?

不一定。在编译期间,JIT会对代码做很多优化,其中有一部分优化的目的就是减少内存堆分配压力,其中一种重要的技术叫做逃逸分析。
(1)什么是逃逸分析
指分析指针动态范围的方法。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸。

当一个对象被new出来之后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸。

(2)逃逸分析的好处
①栈上分配
如果确定一个对象不会逃逸到线程之外,那么就可以考虑将这个对象在栈上分配,对象占用的内存随着栈帧出栈而销毁,这样一来,垃圾收集的压力就降低很多。
②同步消除
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。
③标量替换
假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么可以不创建对象,直接用创建若干个成员变量代替,可以让对象的成员变量在栈上分配和读写(标量替换:把一个Java对象拆散,将其用到的成员变量恢复为原始类型来访问的过程)。


四、垃圾回收

1、怎么判断对象是否可以被回收?

(1)引用计数器
为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收(缺点是不能解决循环引用的问题)
(2)可达性分析
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的

2、说一下JVM有哪些垃圾回收算法?

(1)标记-清除算法
标记无用对象,然后进行清除回收。
①阶段
标记阶段:标记出可以回收的对象。
清除阶段:回收被标记的对象。
②优点:实现简单,不需要对象进行移动
③缺点:
执行效率不高。如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。
内存空间的碎片化问题。标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(2)标记-整理算法
——老年代
在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。
①优点:解决了标记-清理算法存在的内存碎片问题
②缺点:需要进行局部对象移动,一定程度上降低了效率
(3)标记-复制算法
——新生代
把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
①优点:按顺序分配内存,实现简单、运行高效,不用考虑内存碎片
②缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制
(4)分代收集算法
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记-整理算法。

3、说一下JVM有哪些垃圾回收器?

image.png
(1)Serial(复制算法)
新生代单线程收集器,标记和清理都是单线程;
进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束;
优点是简单高效。
(2)Serial Old(标记-整理算法)
Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
(3)ParNew(复制算法)
新生代收并行集器,Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现
(4)Parallel(复制算法)
新生代并行收集器,追求高吞吐量,高效利用 CPU。高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景
(5)Parallel Old (标记-整理算法)
Parallel 老生代版本,老年代并行收集器,吞吐量优先
(6)CMS(标记-清除算法)
老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
(7)G1(标记-整理算法)
Java堆并行收集器,JDK1.7引入,不会产生内存碎片,G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

4、能详细说一下CMS收集器的垃圾收集过程吗?

image.png
(1)初始标记(CMS initial mark):单线程运行,需要Stop The World,标记GC Roots能直达的对象。
(2)并发标记((CMS concurrent mark):无停顿,和用户线程同时运行,从GC Roots直达对象开始遍历整个对象图。
(3)重新标记(CMS remark):多线程运行,需要Stop The World,标记并发标记阶段产生对象。
(4)并发清除(CMS concurrent sweep):无停顿,和用户线程同时运行,清理掉标记阶段标记的死亡的对象。

5、新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
区别:新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

6、简述分代垃圾回收器是怎么工作的?

(1)概述
分代垃圾回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
(2)新生代
①概念
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
②执行流程

  • 把 Eden 和 From Survivor 存活的对象放入 To Survivor 区;
  • 清空 Eden 和 From Survivor 分区;
  • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
(3)老生代
老生代当空间占用到达某个值之后就会触发全局垃圾回收,一般使用标记整理的执行算法。

7、简述Java垃圾回收机制

(1)在Java中,程序员是不需要显式地去释放一个对象的内存的,而是由虚拟机自行执行。
(2)在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行。
(3)扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

8、GC是什么?为什么要GC

(1)是什么
GC(Gabage Collection),自动监测对象是否超过作用域从而进行自动回收内存
(2)为什么
Java 语言没有提供释放已分配内存的显示操作方法

9、怎么判断对象是否可以被回收?

(1)引用计数器法
为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收(缺点是不能解决循环引用的问题)。
(2)可达性分析算法
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

10、详细介绍一下 CMS 垃圾回收器?

(1)CMS(Concurrent Mark-Sweep),以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器
(2)适用于要求服务器响应速度的应用上
(3)在启动JVM的参数加上-XX:+UseConcMarkSweepGC来指定使用 CMS 垃圾回收器
(4)使用标记-清除的算法实现
(5)在GC时会产生大量内存碎片
(6)当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure
(7)临时CMS会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低

11、Java中可作为GC Roots的对象有哪几种?

虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象

12、Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC都是什么意思?

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

13、Minor GC/Young GC什么时候触发?

新创建的对象优先在新生代Eden区进行分配,如果Eden区没有足够的空间时,就会触发Young GC来清理新生代。

14、什么时候会触发Full GC?

(1)Young GC之前检查老年代:在要进行 Young GC 的时候,发现 老年代可用的连续内存空间 < 新生代历次Young GC后升入老年代的对象总和的平均大小,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。
(2)Young GC之后老年代空间不足:执行Young GC之后有一批对象需要放入老年代,老年代没有足够的内存空间存放这些对象,此时必须立即触发一次 Full GC
(3)老年代空间不足:老年代内存使用率过高,达到一定比例,也会触发Full GC。
(4)空间分配担保失败( Promotion Failure):新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
(5)方法区内存空间不足:如果方法区由永久代实现,永久代空间不足 Full GC。
(6)System.gc()等命令触发:System.gc()、jmap -dump 等命令会触发 Full gc。

15、对象什么时候会进入老年代?

(1)长期存活的对象将进入老年代
在对象的对象头信息中存储着对象的迭代年龄,迭代年龄会在每次YoungGC之后对象的移区操作中增加,每一次移区年龄加一。当这个年龄达到15(默认)之后,这个对象将会被移入老年代。
(2)大对象直接进入老年代
有一些占用大量连续内存空间的对象在被加载就会直接进入老年代,这样的大对象一般是一些数组、长字符串等。
(3)动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
(4)空间分配担保
假如在Young GC之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。

16、G1垃圾收集器了解吗?

G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。
运行过程:
image.png

  • 初始标记(initial mark):标记了从GC Root开始直接关联可达的对象。STW(Stop the World)执行。
  • 并发标记(concurrent marking):和用户线程并发执行,从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象、
  • 最终标记(Remark):STW,标记再并发标记过程中产生的垃圾。
  • 筛选回收(Live Data Counting And Evacuation):制定回收计划,选择多个Region 构成回收集,把回收集中Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。需要STW。

    17、有了CMS,为什么还要引入G1?

    CMS同样有三个明显的缺点:

  • 标记-清除算法会导致内存碎片比较多

  • CMS的并发能力比较依赖于CPU资源,并发回收时垃圾收集线程可能会抢占用户线程的资源,导致用户程序性能下降
  • 并发清除阶段,用户线程依然在运行,会产生所谓的理“浮动垃圾”(Floating Garbage),本次垃圾收集无法处理浮动垃圾,必须到下一次垃圾收集才能处理。如果浮动垃圾太多,会触发新的垃圾回收,导致性能降低

G1主要解决了内存碎片过多的问题。


五、类文件结构


六、JVM调优

1、说一下JVM调优的工具?

jdk 自带了很多监控工具,都位于 jdk 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两个视图监控工具。
(1)jconsole
用于对JVM中的内存、线程和类等进行监控
(2)jvisualvm
jdk 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

2、常用的JVM调优的参数都有哪些?

堆配置:

  • -Xms:初始堆大小
  • -Xms:最大堆大小
  • -XX:NewSize=n:设置年轻代大小
  • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3表示Eden:3 Survivor:2,一个Survivor区占整个年轻代的1/5
  • -XX:MaxPermSize=n:设置持久代大小

收集器设置:

  • -XX:+UseSerialGC:设置串行收集器
  • -XX:+UseParallelGC:设置并行收集器
  • -XX:+UseParalledlOldGC:设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC:设置并发收集器

并行收集器设置:

  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数
  • -XX:MaxGCPauseMillis=n:设置并行收集最大的暂停时间(如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
  • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
  • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
  • -XX:ParallelGCThreads=n:设置并发收集器年轻代手机方式为并行收集时,使用的CPU数。并行收集线程数

打印GC回收的过程日志信息:

  • -XX:+PrintGC
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename

    3、有哪些常用的命令可以作为性能监控和故障处理工具?

    操作系统工具

  • top:显示系统整体资源使用情况

  • vmstat:监控内存和CPU
  • iostat:监控IO使用
  • netstat:监控网络使用

JDK性能监控工具

  • jps:虚拟机进程查看
  • jstat:虚拟机运行时信息查看
  • jinfo:虚拟机配置查看
  • jmap:内存映像(导出)
  • jhat:堆转储快照分析
  • jstack:Java堆栈跟踪
  • jcmd:实现上面除了jstat外所有命令的功能

    4、了解哪些可视化的性能监控和故障处理工具?

    (1)JDK自带
    JConsole,VisualVM,Java Mission Control
    (2)第三方
    MAT:Java 堆内存分析工具。
    GChisto:GC 日志分析工具。
    GCViewer:GC 日志分析工具。
    JProfiler:商用的性能分析利器。
    arthas:阿里开源诊断工具。
    5、

七、其它知识点

1、Java中都有哪些引用类型?

(1)强引用
指在程序代码之中普遍存在的引用赋值,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

  1. Object obj =new Object();

(2)软引用
用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。

  1. Object obj = new Object();
  2. ReferenceQueue queue = new ReferenceQueue();
  3. SoftReference reference = new SoftReference(obj, queue);
  4. //强引用对象滞空,保留软引用
  5. obj = null;

(3)弱引用
用来描述那些有用非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  1. Object obj = new Object();
  2. ReferenceQueue queue = new ReferenceQueue();
  3. WeakReference reference = new WeakReference(obj, queue);
  4. //强引用对象滞空,保留弱引用
  5. obj = null;

(4)虚引用(幽灵引用/幻影引用)
最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

  1. Object obj = new Object();
  2. ReferenceQueue queue = new ReferenceQueue();
  3. PhantomReference reference = new PhantomReference(obj, queue);
  4. //强引用对象滞空,保留虚引用
  5. obj = null;

2、Java程序运行机制

首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;运行字节码的工作是由解释器(java命令)来完成的。

3、深拷贝和浅拷贝

浅拷贝(shallowCopy):只是增加了一个指针指向已存在的内存地址;
深拷贝(deepCopy):增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存(使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误)

4、对象创建的方式

使用new关键字:调用了构造函数
使用Class的newInstance方法:调用了构造函数
使用Constructor类的newInstance方法:调用了构造函数
使用clone方法:没有调用构造函数
使用反序列化:没有调用构造函数

5、讲一下对象的创建过程

new 一个对象在堆中的过程主要分为五个步骤:
(1)类加载检查
当 Java 虚拟机遇到一条字节码 new 指令时,它会首先检查根据 class 文件中的常量池表(Constant Pool Table)能否找到这个类对应的符号引用,然后去方法区中的运行时常量池中查找该符号引用所指向的类是否已被 JVM 加载、解析和初始化过:如果没有,那就先执行相应的类加载过程;如果有,那么进入下一步,为新生对象分配内存。
(2)分配内存
在堆中给划分一块内存空间分配给这个新生对象用。
具体的分配方式根据堆内存是否规整有两种方式:

  • 堆内存规整的话采用的分配方式就是指针碰撞:所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把这个指针向空闲空间方向挪动一段与对象大小相等的距离
  • 堆内存不规整的话采用的分配方式就是空闲列表:所谓内存不规整就是已被使用的内存和空闲的内存相互交错在一起。JVM 就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的连续空间划分给这个对象,并更新列表上的记录。

(3)初始化零值
对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。对齐填充仅仅起占位作用,没啥特殊意义,初始化零值这个操作就是初始化实例数据这个部分
(4)设置对象头
设置对象头中的一些信息
(5)执行 init 方法
最后就是执行构造函数,构造函数即 Class 文件中的 () 方法,一般来说,new 指令之后会接着执行 () 方法,按照构造函数的意图对这个对象进行初始化,这样一个真正可用的对象才算完全地被构造出来了。

6、能说一下对象的内存布局吗?

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
JVM - 图11
(1)对象头
①对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为Mark Word,它是个动态的结构,随着对象状态变化。
②类型指针:指向对象的类元数据类型(即对象代表哪个类)。
③如果对象是一个Java数组,那还应该有一块用于记录数组长度的数据。
(2)实例数据
用来存储对象真正的有效信息,也就是我们在程序代码里所定义的各种类型的字段内容,无论是从父类继承的,还是自己定义的。
(3)对齐填充
不是必须的,没有特别含义,仅仅起着占位符的作用。

7、对象怎么访问定位?

Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的。
主流的访问方式:
(1)句柄
Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
JVM - 图12
(2)直接指针
Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。
JVM - 图13

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 使用直接指针来访问最大的好处就是速度更快,节省了一次指针定位的时间开销。 HotSpot虚拟机主要使用直接指针来进行对象访问。

8、内存溢出和内存泄漏是什么意思?

内存泄露:申请的内存空间没有被正确释放,导致内存被白白占用。
内存溢出:申请的内存超过了可用内存,内存不够了。
两者关系:内存泄露可能会导致内存溢出。

9、能手写内存溢出的例子吗?

在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能,重点关注堆和栈。
(1)Java堆溢出
Java堆用于储存对象实例,只要不断创建不可被回收的对象,比如静态对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

  1. /**
  2. * VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
  3. */
  4. public class HeapOOM {
  5. static class OOMObject {
  6. }
  7. public static void main(String[] args) {
  8. List<OOMObject> list = new ArrayList<OOMObject>();
  9. while (true) {
  10. list.add(new OOMObject());
  11. }
  12. }
  13. }

(2)虚拟机栈内存溢出
可以把栈的内存设大一点,然后不断地去创建线程,因为操作系统给每个进程分配的内存是有限的,所以到最后,也会发生OutOfMemoryError异常。

  1. /**
  2. * vm参数:-Xss2M
  3. */
  4. public class JavaVMStackOOM {
  5. private void dontStop() {
  6. while (true) {
  7. }
  8. }
  9. public void stackLeakByThread() {
  10. while (true) {
  11. Thread thread = new Thread(new Runnable() {
  12. public void run() {
  13. dontStop();
  14. }
  15. });
  16. thread.start();
  17. }
  18. }
  19. public static void main(String[] args) throws Throwable {
  20. JavaVMStackOOM oom = new JavaVMStackOOM();
  21. oom.stackLeakByThread();
  22. }
  23. }

10、内存泄漏可能由哪些原因导致呢?

(1)静态集合类
静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。
(2)单例模式
单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
(3)数据连接、IO、Socket等连接
创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。
(4)变量不合理的作用域
一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。

  1. public class Simple {
  2. Object object;
  3. public void method1(){
  4. object = new Object();
  5. //...其他代码
  6. //由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放
  7. object = null;
  8. }
  9. }

(5)hash值发生变化
使用HashMap、HashSet等容器中时候,由于对象修改之后的Hash值和存储进容器时的Hash值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。
(6)ThreadLocal使用不当
ThreadLocal的弱引用导致内存泄漏,使用完ThreadLocal一定要记得使用remove方法来进行清除。

11、如何判断对象仍然存活?

(1)引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
(2)可达性分析算法
将一系列 GC Roots 作为初始的存活对象合集(Gc Root Set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
12、