1.哪些情况下的对象会被垃圾回收机制处理掉?
如何判断对象已死?
1.引用计数算法
给对象添加一个引用计数器。但是难以解决循环引用问题。
2.可达性分析算法
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。
GC ROOTS对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
3. 一个对象的真正死亡至少要经历两次标记过程:
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer 线程去执行它。这里所谓的 “执行” 是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。
finalize() 方法只会被系统自动调用一次。
4. 方法区回收?
主要回收:废弃的常量和无用的类
- 判断废弃常量:一般是判断没有该常量的引用。
- 无用的类
- 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
2.什么是强引用、软引用、弱引用以及虚引用?
强引用(FinalReference):
- 类似于 Object obj = new Object(); 创建,只要强引用在就不回收。
软引用(SoftReference):
- 在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
- 应用场景:
- 软引用可用来实现内存敏感的高速缓存
- 引用队列(ReferenceQueue),用于
弱引用(WeakReferenc):
- 对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
- 应用场景:
- 内存泄漏监测框架LeakCanary
- WeakHashMap 当key只有弱引用时,GC发现后会自动清理键和值,作为简单的缓存表解决方案。
- ThreadLocal ThreadLocal.ThreadLocalMap.Entry 继承了弱引用,key为当前线程实例,和WeakHashMap基本相同。
虚引用(PhantomReference):
- 无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
- 应用:为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。
3.JVM的回收算法是怎样的
标记 — 清除算法:
复制算法:
- 把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。
- 解决标记 — 清除算法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。
- 但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。
标记-整理算法:
- 不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。
分代回收算法:
- 根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
- 新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
- 老年代:老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 — 清除 或者 标记 — 整理 算法回收。
4.垃圾回收机制和调用 System.gc()的区别?
GC的触发条件:
- 当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用。
- 但是当Java堆内存不足时,GC会被调用。
System.gc():
- gc()函数的作用只是提醒虚拟机,希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
5.JVM 内存模型介绍下,哪些地方可能 OutOfMeomory?
程序计数器
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为空 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
Java 虚拟机栈
-
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。 -
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double )、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError :线程请求的栈深度大于虚拟机所允许的深度。 OutOfMemoryError :如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
Java 堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB )。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError :如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
方法区
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
6.为什么会有StackOverflow,stack的大小是多少
7.Java虚拟机和Android虚拟机有什么区别
文件格式不同:
-
Java虚拟机 :
- 基于栈。基于栈的机器必须使用指令来载入和操作栈上的数据
- 运行的是Java字节码。(Java类会被编译成一个或多个字节码**
.class 文件**,打包到**.jar 文件**中)。Java虚拟机从相应的.class文件和.jar文件中获取相应的字节码。 -
Dalvik虚拟机(Android) :
- 基于寄存器
- 运行的是自定义.dex字节码格式。(Java类被编译成
.class 文件后,通过一个**dx** 工具将所有的.class 文件转换成一个**.dex 文件**,然后dalvik 虚拟机会从中读取指令和数据) dex 文件格式可以减少整体文件尺寸,提高I/O 操作的类查询速度,odex 文件是为了在运行过程中进一步提高性能,对dex 文件的进一步优化。
7.Android两种虚拟机有什么区别
Dalvik虚拟机
- Dalvik 虚拟机采用的是
JIT(Just-In-Time) 编译模式,意思为即时编译,我们知道apk被安装到手机中时,对应目录会有 dex 或odex 和 apk文件,apk 文件存储的是资源文件,而dex 或odex (经过优化后的dex 文件内部存储class文件)内部存储class文件,每次运行app时虚拟机会将dex文件解释翻译成机器码,这样才算是本地可执行代码,之后被系统运行。 - Dalvik虚拟机可以看做是一个Java VM,它负责解释dex文件为机器码,如果我们不做处理的话,每次执行代码,都需要Dalvik将dex代码翻译为微处理器指令,然后交给系统处理,这样效率不高。
ART虚拟机:
- ART 是一种执行效率更高且更省电的运行机制,执行的是本地机器码,这些本地机器码是从dex字节码转换而来。ART采用的是AOT(Ahead-Of-Time)编译,应用在第一次安装的时候,字节码就会预先编译成机器码存储在本地。在App运行时,ART模式就较Dalvik模式少了解释字节码的过程,所以App的运行效率会有所提高,占用内存也会相应减少。
区别:
- Dalvik每次都要编译再运行,Art只会安装时启动编译
- Art占用空间比Dalvik大(原生代码占用的存储空间更大),就是用“空间换时间”
- Art减少编译,减少了CPU使用频率,使用明显改善电池续航
- Art应用启动更快、运行更快、体验更流畅、触感反馈更及时
8.类加载过程
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
- 遇到
new 、getstatic 、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。 - 使用
java.lang.reflect 包的方法对类进行反射调用的时候。 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
加载过程:
- 加载:指的是把class字节码文件从各个来源通过类加载器装载入内存中。
- 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
- 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
- 验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
- 文件格式的验证
- 元数据的验证
- 字节码的验证
- 符号引用的验证
- 准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
- 解析:将常量池内的符号引用替换为直接引用的过程。
- 初始化:主要是对类变量初始化,是执行类构造器的过程。
- 只对static修饰的变量或语句进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
9.class文件格式
直接查看博客吧:https://blog.csdn.net/zhangjg_blog/article/details/21486985
10.双亲委派机制,为什么要用双亲委派机制,如何打破双亲委派机制?
类加载器:
- 对于任意一个类,都必须由加载它的类加载器和这个类本身一起来共同确立其在Java虚拟机中的唯一性。
类加载种类:
启动类加载器,启动类加载器在HotSpot虚拟机中使用C++语言实现,它是虚拟机的一部分其它类加载器都由Java语言实现,并且全部继承自java.lang.ClassLoader,它们是独立于虚拟机外部的。
类加载器分为四类:
- 启动类加载器:它的作用是将JAVA_HOME/lib目录下的类加载到内存中。需要注意的是由于启动类加载器涉及到虚拟机本地的实现细节,开发人员将无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- 扩展类加载器:它是由Sun的ExtClassLoader实现的,它的作用是将JAVA_HOME/lib/ext目录下或由系统变量 java.ext.dir指定位置中的类加载到内存中,它可以由开发人员直接使用。
- 应用程序类加载器:它是由Sun的AppClassLoader实现的,它的作用是将classpath路径下指定的类加载到内存中。它也可以由开发人员使用。
- 自定义类加载器:自定义的类加载器继承自ClassLoader,并覆盖findClass方法,它的作用是将特殊用途的类加载到内存中。
双亲委派模型:
-
所谓的类加载器的双亲委派模型指的是类加载器之间的层次关系。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合关系来实现,而不是通过继承。如上图所示。 -
某一个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,则成功返回;如果父类加载器无法完成加载任务,将抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载,依次类推。
双亲委派模型的好处:
- 即防止内存中出现多份同样的字节码。
- 如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,而且如果不使用这种双亲委派模型将会给虚拟机的安全带来隐患。所以,要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。
如何打破双亲委派机制:
- 重写loadClass方法,将需要特殊对待的类自己先处理,非处理范围的类调用super方法即可
- 使用线程上下文类加载器
什么是常量
- 用final修饰的成员变量表示常量,值一旦给定就无法改变!
- final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
Class文件中的常量池
-
在Class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。 -
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量: -
类和接口的全限定名 -
字段名称和描述符 -
方法名称和描述符
方法区中的运行时常量池
运行时常量池是方法区的一部分。 CLass文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是**String类的intern()**方法。
常量池的好处
- 常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
- 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
- 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
- 节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。
双等号==的含义
- 基本数据类型之间应用双等号,比较的是他们的数值。
- 复合数据类型(类)之间应用双等号,比较的是他们在内存中的存放地址。
参考学习博客:
一文看懂 JVM 内存布局及 GC 原理
Java常量池理解与总结
深入理解Java Class文件格式(一)
每天一道面试题」如何理解双亲委派模型及为什么要使用这种机制
Java虚拟机(JVM)你只要看这一篇就够了!
|