神贴献上:https://www.nowcoder.com/discuss/421353?channel=-1&source_id=1
本文所有的问题和部分答案均来自这篇神贴,如有神助!
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。
虚拟机中的共划分为三个代:https://www.cnblogs.com/yonyong/p/9334279.html
参考https://blog.csdn.net/weixin_34075268/article/details/93711080
https://www.cnblogs.com/rong0912/p/12047674.html
注:
jdk1.7,jvm从方法区移除的不是运行时常量池,而是字符串常量池,jdk1.8,jvm把字符串常量池移到了堆内存里。
jdk1.8,并没有取消方法区,而是方法区的实现由永久代变成了元空间。
- 方法区(线程共享):属于共享内存区域,主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
- Java堆(线程共享):对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
- Java虚拟机栈(线程私有):线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储
局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
- 本地方法栈(线程私有):区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
- 程序计数器(线程私有):内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

- 运行时常量池:属于方法区一部分.Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
- 直接内存:非虚拟机运行时数据区的部分
1. JVM的主要组成部分及其作用
- 类加载器:把class文件加载到内存中,class文件需要符合格式要求才会被虚拟机认可、加载和执行
- 执行引擎:解释命令,提交到操作系统执行
- 本地接口:融合不同的编程语言为java所用
- 运行时数据区:所写的程序都要加载到这个区域运行
2. jvm 运行时数据区组成?方法区(jdk1.8之后转移到直接内存),堆,虚拟机栈,本地方法栈,程序计数器
3. 堆栈的区别?
- 堆:Java内存管理中最大的一块,存储实例对象和数组,线程共享;
- 栈:分为虚拟机栈和本地方法栈,这里主要是指虚拟机栈。为java方法服务,每个方法在执行时都会创建栈帧,用于存储局部变量表,操作数栈,动态链接和方法出口等信息,线程私有。
4. 运行时数据区哪些是线程共享,哪些是线程私有
- 线程共享:方法区(元空间),堆
- 线程私有:虚拟机栈,本地方法栈,程序计数器
5. Java 中成员变量、局部变量、静态变量、常量分别存储在那些内存区域中?
- 成员变量:堆
- 局部变量:
- 局部变量是基本类型(8种基本类型):栈
- 局部变量是对象:对象实例存储在堆中,对象引用存储在栈中
- 静态变量(static修饰):方法区(元数据区)
- 常量(final修饰):方法区(元数据区)
6. 说一下类加载的执行过程?
类加载的过程主要分为三个部分:
详细见:https://www.cnblogs.com/jxxblogs/p/12208112.html
https://www.jianshu.com/p/c9cdd963943a
- 加载:它是Java将字节码数据从不同的数据源读取到JVM中,并映射为JVM认可的数据结构(Class对象),这里的数据源可能是各种各样的形态,如jar文件、class文件,甚至是网络数据源等;如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
- 链接:这是核心的步骤,简单说是把原始的类定义信息平滑地转化入JVM运行的过程中。这里可进一步细分为三个步骤:
- 验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误
- 准备:主要是为类变量(注意,不是实例变量)分配内存,并设置类变量初始值。注意这个初始值通常情况是数据类型的零值,假设public static int value = 123; 在准备阶段之后value的值是0,而不是123.赋值为123的动作要到类的初始化阶段才会被执行;如果类字段的字段属性表中存在ConstantValue属性,在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,例如public static final int value = 123; value的在准备阶段就会被赋值为123.
- 解析:将常量池内的符号引用替换为直接引用的过程。在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
- 初始化:主要是对类变量初始化,是执行类构造器<clinit>()的过程。
- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。即Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()已经执行完毕。因此第一个被执行的<clinit>()方法的类型一定是java.util.Object。
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
- 执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用时,才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。、
- 多个线程同时去初始化一个类,只有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法。
7. Java 中都有哪些加载器?
详细见:https://www.pianshen.com/article/13191313234/
当JVM启动的时候,java缺省开始使用如下三种类加载器:
- 启动类加载器:引导类加载器是用本地代码实现的类加载器,它负责将<JAVA_HOME>/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以,不允许直接通过引用进行操作。
- 扩展类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将<JAVA_HOME>/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
- 系统类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径 (java -classpath 或 - Djava.class.path 变量所指的目录,即当前类所在路径及其引用的第三方类库的路径。开发者可以直接使用系统类加载器。
扩展类加载器和系统类加载器均是继承自java.lang.ClassLoader抽象类。
8. 什么是双亲委派模型?
简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载Java类型。

9. 反射中,Class.forName () 和 ClassLoader.loadClass () 区别
java反射机制:https://www.cnblogs.com/nongzihong/p/10521133.html
Class的加载分为三个阶段:加载,连接,初始化。
- Class.forName(className)实际上是调用Class.forName(className, true, this.getClass().getClassLoader()).第二个参数,是指Class被加载后是不是必须初始化。
- ClassLoader.loadClass(className)实际上调用的是ClassLoader.loadClass(name, false),第二个参数指出Class是否被链接。
区别就出来了,Class.forName(className)装载的class已经被初始化,而ClassLoader.loadClass(className)装载的class还没有被link。
一般情况下,这两个方法效果一样,都能装载Class。但如果程序依赖于Class是否被初始化,就必须用Class.forName(name)了。
例如,在JDBC编程中,常看到这样的用法,Class.forName("com.mysql.jdbc.Driver"),如果换成了 getClass().getClassLoader().loadClass("com.mysql.jdbc.Driver"),就不行。
为什么呢?打开com.mysql.jdbc.Driver的源代码看看,
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
原来,Driver在static块中会注册自己到java.sql.DriverManager。而static块就是在Class的初始化中被执行。所以这个地方就只能用Class.forName(className)。
10. 说一下对象创建的过程?
- 检查类是否已经被加载
- 为对象分配内存空间
- 为对象字段设置零值
- 设置对象头
- 执行构造函数
11. 对象有哪几部分构成?虚拟机如何访问对象?
对象的内存布局:
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
【解释】HotSpot虚拟机是java虚拟机中主要使用的一种,我们平时所说的java虚拟机是泛指,而Hotspot虚拟机就是其中使用最广泛的一种。
1.Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2. 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
3. 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍
对象的访问方式:
Java 程序通过栈上的 reference 数据来操作堆上的具体对象。目前主流的访问方式有①使用句柄和②直接指针两种:
- 句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
- 直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
12. java 中都有哪些引用类型?
在Java中一共有四种引用类型,分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。
1.强引用是指在代码中普遍存在的,类似 Object obj = new Object();这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉引用的对象。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为 null,这样一来,JVM 就可以适时的回收对象了
2.软引用 是用来描述一些还有用但并非是必要的对象。对于软引用着的对象,在系统将要发生内存溢出异常之前,将会把这类对象列进回收范围进行第二次的回收。如果这次回收仍然没有足够的内存,就会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
3.弱引用 也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次的垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
4.虚引用,它是最弱的一种引用关系。无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集时收到一个系统通知。虚引用必须和引用队列(ReferenceQueue)联合使用。
在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
13.怎么判断对象是否可以被回收?
对象标记算法:
- 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是它很难解决对象之间相互循环引用的问题。
- 可达性分析算法
- 概念:
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

-
- 可以作为GC Roots 的对象:
虚拟机栈中的引用的对象
全局的静态的对象,也就是使用了 static 关键字
常量引用,就是使用了 static final 关键字
本地方法栈中引用的对象
如何判断一个对象是否可以回收:
宣告一个对象死亡,至少要经历两次标记
1.第一次标记
如果对象进行可达性分析算法之后没发现与 GC Roots 相连的引用链,那它将会第一次标记并且进行一次筛选(看有没有覆盖finalize方法)。
- 当对象没有覆盖 finalize () 方法、或者 finalize () 方法已经被 JVM 执行过(每个对象的finalize方法只能执行一次,不能无限次复活,否则内存只增不减),则判定为可回收对象。
- 如果对象有必要执行 finalize () 方法,则被放入 F-Queue 队列中。稍后在 JVM 自动建立、低优先级的 Finalizer 线程(可能多个线程)中触发这个方法.
2.第二次标记
GC 对 F-Queue 队列中的对象进行二次标记。
- 如果对象在 finalize () 方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出 “即将回收” 集合。
- 如果此时对象还没成功逃脱,那么只能被回收了。
更多理解见:https://www.cnblogs.com/wylwyl/p/10500592.html, https://blog.csdn.net/hl_java/article/details/79136049
14. 内存泄露和内存溢出分别是什么?什么原因造成?如何避免?
内存泄漏:
1.定义
当某些对象不再被应用程序所使用,但是由于仍然被引用而导致垃圾收集器不能释放 (Remove, 移除) 他们.这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
2.场景
-
长生命周期的对象持有短生命周期对象的引用
-
修改对象的地址(常见于hashset的哈希值)从而无法找到该对象。
-
机器的连接数和关闭时间设置,长时间开启非常耗费资源的连接
内存溢出:
1.定义
指程序运行过程中无法申请到足够的内存而导致的一种错误。
2.场景
-
堆内存溢出(对象过多)
-
方法区内存溢出(加载的类过多)
-
线程栈溢出(递归太深或者方法层级过多)
总结:
内存泄露是内存溢出的主要诱因,内存泄漏的堆积最终会导致内存溢出,不是唯一因素。
如何避免:
-
尽早释放无用对象的引用
-
使用字符串处理,避免使用 String,应大量使用 StringBuffer,每一个 String 对象都得独立占用内存一块区域
-
尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
-
避免在循环中创建对象
-
开启大型文件或数据库。不要一次拿了太多的数据
15. 给对象分配内存如何保证线程安全?
内存分配的两种方式:
-
指针碰撞
-
空闲列表
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是 “标记 - 清除”,还是 “标记 - 整理”(也称作 “标记 - 压缩”),值得注意的是,复制算法内存也是规整的,关于垃圾回收器和垃圾回收算法,我会在下一章节展开。
1.指针碰撞
场景:堆内存规整,没有内存碎片。
原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没有用过的内存区域移动对象内存大小位置即可
2.空闲列表
场景:堆内存不规整,有内存碎片
原理:虚拟机会维护一个列表,该列表会记录那些内存是可用的,在分配的时候找一个足够大的内存区域划分给对象实例,并更新列表
内存并发分配,采用下面两种方式保证线程安全:
- CAS + 失败重试:CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB(Thread Local Allocation Buffer):为每一个线程预先在 Eden 区分配一块儿内存,这块独立的空间由该线程独享。JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
16. 说一下 jvm 有哪些垃圾回收算法?
有三种垃圾回收算法:标记-清除,复制,标记-整理
1. 标记-清除:
这是一个非常基本的GC算法,它是现代GC算法的思想基础,分为标记和清除两个阶段:先把所有活动的对象标记(标记的方式就是上一章节讲的可达性分析法)出来,然后把没有被标记的对象统一清除掉。
存在的问题:
-
效率问题,两个过程的效率都不高
-
空间问题,清除之后会产生大量不连续的内存。

2. 复制:
将原有的内存空间分成两块,每次只使用其中一块。在GC时,将正在使用的内存块中的存活对象复制到未使用的那一块中,然后清除正在使用的内存块中的所有对象,并交换两块内存的角色,完成一次垃圾回收。
存在的问题:它比标记-清除算法要高效,但不适用于存活对象较多的内存,因为复制的时候会有较多的时间消耗。致命缺点就是会有一半的内存浪费。

HotSpot虚拟机的改良算法:
1. 弱代理论
大多数分配了内存的对象并不会存货太长时间,在处于年轻时代就会死掉。很少有对象会从老年代变成年轻代。改良算法根据弱代理论。
2. 内存布局和算法
新生代内存分配一块较大的Eden空间和两块较小的Survivor空间(默认Eden和Survivor的大小比例是8:1)。每次使用Eden和其中一块Survivor空间(当Eden内存满了就会把对象分配到Survivor区)
回收时(在Eden区满时会触发Minor GC(young GC))将Eden和Survivor空间中存活的对象一次性复制到另一块Survivor空间上。最后清理掉Eden和使用过的Survivor空间。Survivor区总有一个是空的。
如果另一块Survivor空间没有足够内存来存放上一次新生代收集下来的存活对象,那么这些对象将通过分配担保机制直接进入老年代(老年代满时会发生后Major GC(Full GC),回收年老代和年轻代中不再被使用的对象资源)。
额外的知识:【
JVM区域总体分两类,heap区和非heap区。
heap区又分为:
Eden Space(伊甸园)、
Survivor Space(幸存者区)、
Old Gen(老年代)。
非heap区又分:
Code Cache(代码缓存区);
Perm Gen(永久代);
Jvm Stack(java虚拟机栈);
Local Method Statck(本地方法栈);
————————————————
原文链接:https://blog.csdn.net/shiyong1949/java/article/details/52585256
】
3. 标记-整理:
适用于存活对象较多的场合,它的标记阶段和标记-清除算法中的一样。整理阶段是将所有存活的对象压缩到内存的一端,之后清理边界外所有的空间。它的效率也不高。

标记-清除和标记-整理算法的比较:
本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:
如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且要全称暂停用户应用程序才能进行(Stop The World)。
如果不移动和整理,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。
所以移动则内存回收时会更复杂,不移动则内存分配时会更复杂。所以标记-清除的延迟更低,吞吐量(赋值器(使用垃圾收集的用户程序)与收集器的效率总和)也低,因为内存分配和访问的频率要比垃圾收集频率高得多;而标记-整理的吞吐量高,延迟大。
关注吞吐量的Parallel Old收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。还有一种“和稀泥式”解决方案可以不再内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清理算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法手机一次,以获得规整的内存空间。CMS收集器面临空间碎片过多时就是采用这种处理方法。
分代收集算法:
新生代和老生代对象数量和存活时间不同,所以通常是分代使用不同的垃圾回收器。
- 少量数据存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去。只有少量存活,那就算则复制算法,只需付出少量存活对象的复制成本就可以完成GC。
- 大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。
垃圾回收流程见:https://blog.csdn.net/en_joker/article/details/79737533
17. 说一下 jvm 有哪些垃圾回收器?
HotSpot共有7个垃圾收集器:

如果两个收集器之间存在连线,则说明它们可以搭配使用。
-
新生代收集器:Serial、ParNew、Parallel Scavenge (这三个使用的都是复制算法)
-
老年代收集器:Serial Old(标记-整理算法)、Parallel Old(标记-整理算法)、CMS(标记-清除算法)
-
整堆收集器:G1(Garbage-First)
1. Serial 收集器:串行,Stop The World
2. ParNew 收集器:Serial收集器的多线程版本,其余行为(控制参数、stop the world、收集算法、回收策略等等)和 Serial 收集器完全一样
3. Parallel Scavenge 收集器:多线程,它看上去几乎和 ParNew 都一样,不同的是关注点是吞吐量,并有自适应调解策略(不需要人工指定新生代大小、Eden与Survivor区的比例、晋升老年代对象大小等细节参数,虚拟机会动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。),也是区别于ParNew的一个重要特征。
4. Serial Old 收集器:Serial 收集器的老年代版本,单线程收集器。采用标记 - 整理算法。
5. Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本。使用多线程和 “标记 - 整理” 算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
6. CMS(Concurrent Mark Sweep) 收集器:以获取最短回收停顿时间为目标的收集器。是 HotSpot 虚拟机第一款真正意义上的并发收集器,使用的是标记-清除算法。
7. G1 (Garbage-First) 收集器:面向服务器。低停顿,可建立可预测的停顿时间模型。工作范围是整个Java堆。
18. 详细介绍一下 CMS垃圾回收器?
CMS回收器:标记-清除算法,注重用户体验,以获得最短回收停顿时间为目标。
它的主要适合场景是对响应时间的重要性需求大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。
运行过程:CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停
- 初始标记:标记GC Roots能直接到的对象。速度很快,但是仍存在Stop The World(进行垃圾收集工作的时候必须暂停其他所有的工作线程)问题。
- 并发标记:进行GC Roots Tracing的过程,找出存活对象且用户线程可并发执行。
- 重新标记:标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。仍然存在Stop The World(暂停)问题。
- 并发清除:对标记的对象进行清除回收。
注意:初始标记是标记直接与GC Roots连接的对象,并发标记是把所有与GC Roots连接的对象都标记。
优点:并发收集、低停顿。
缺点:
- 对CPU资源敏感;
- 无法处理浮动垃圾(并发标记和并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾);
- 它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
【概念补充】:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
19. G1 为什么能建立可预测的停顿时间模型?
G1回收器:面向服务器,其他回收器是分代回收,G1是作用整个堆,不再有物理上的分代。将Java堆划分为大小相等的Region区域。低停顿并可建立可预测的停顿时间模型。
1. 特点:
- 并行与并发:使用多个CPU来缩短Stop The World停顿时间。
- 分代收集:不需要其他收集器配合,独立管理整个GC堆,但保留了分代的概念(每个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。Humongous Region用于存储大对象,G1的大多数行为都把这个区域作为老年代的一部分来看待)。
- 空间整合:不会产生空间碎片,收集后能提供规整的可用内存。
- 可预测的停顿:除了追求低停顿外,还能建立可预测的停顿时间模型。
2. 存在的问题:
分配在 Region 中的对象可以与 Java 堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个 Java 堆才能保证准确性。
3. 运行步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
前三个步骤跟CMS一致,筛选回收是对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。(必须暂停用户线程,由多条收集器线程并行完成)
4. G1为什么能建立可预测的停顿时间模型?
因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护了一个优先列表,每次根据用户设定的允许收集停顿时间,优先回收价值最大的Region。这样就保证了在有限时间内可以获取尽可能高的收集效率。
20. Minor GC(Young GC) 和 Major GC(Old GC) 有什么不同呢?
- 部分收集(Partial GC)
- Minor GC(Young GC):新生代GC,Minor非常频繁,回收速度一般也比较快,在Eden区满时触发。
- Major GC(Old GC):老年代GC,目前只有CMS收集器会有单独收集老年代的行为。出现了Major GC经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scanvenge收集器的收集策略里就有直接进行Major GC的策略选择过程),Major GC的速度一般会比Minor GC慢10倍以上,在老年代满时触发。(不同的资料有不同所指,可能是老年代收集也可能是整堆收集。)
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
23. 什么对象会进入老年代?
- 大对象直接进入老年代:比如很长的字符串,或很大的数组等,参数-XX:PretenureSizeThreshold=3145728设置,超过这个参数设置的值就直接进入老年代。
- 长期存活的对象进入老年代:在对中分配内存的对象,其对象头中包含了GC分代年龄标记信息。对象在Eden区出生,初始值为1,每熬过一次Minor GC,值就加1,当年龄超过阈值,就会进入老年代。可以通过参数-XX:MaxTenuringThreshold设置年龄阀值(默认是 15 岁)。
- 当Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,不需要达到阈值年龄。
24. 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?
原文链接:https://blog.csdn.net/qq_32534441/article/details/95456973
一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法
①进行一些必要的清理工作(对垃圾回收器不能处理的特殊情况进行处理)
②也有可能使该对象重新被引用,我习惯叫这种作用为复活。注意!!每个对象的finalize()方法只能被执行一次,第二次就会直接跳过finalize()方法,这就是为了防止出现对象无限复活,内存空间只增不减。
一般忽略第二种情况,概念就变成了:一旦垃圾收集器准备好释放对象占用的存储空间(进入第一个回收周期),首先会去调用finalize()方法进行一些必要的清理工作,只有到下一次再进行垃圾回收动作(下一个回收周期)的时候,才会真正释放这个对象所占用的内存空间。
不会立即释放对象占用的内存。 如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,而垃圾收集器是运行在后台的线程,只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,这时候仍然不会立即释放该对象内存,因为有些对象是可恢复的(在 finalize方法中恢复引用 )。只有确定了对象无法恢复引用的时候才会清除对象内存。
25. 常量池都包括哪些内容?常量池的位置?
1. 字符串常量池
- JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
- JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。
字符串常量池里放的是什么?
- 在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
- 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。
需要说明的是:字符串常量池中的字符串只存在一份!
如:
-
String s1 = "hello,world!";
-
String s2 = "hello,world!";
即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。
注意:只有静态字符串才会进入字符串池(String ab = "ab"),含有变量的字符串(String a = "a"; String b = a + "b")如b,不会进入字符串池,采用new创建的字符串对象也不进入字符串池。
intern()方法返回字符串对象的规范化表示形式。
例如:
String ab = new String("ab");
ab.intern();
这个方法会首先检查字符串池中时候有"ab"这个字符串,如果存在就返回这个字符串的引用,否则就讲这个字符串添加到字符串池中,然后返回这个字符串的引用。
2. class常量池(静态常量池)
2.1 class常量池简介:
- 我们写的每一个java类被编译后,就会形成一个class文件;class文件除了包含类的版本、字段、方法、接口的描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 每个class文件都有一个class常量池
2.2 什么是字面量和符号引用
- 字面量包括:1. 文本字符串 2. 八种基本类型的值 3. 被声明为final的常量等;
- 符号引用包括:1. 类和方法的全限定名 2. 字段的名称和描述符 3. 方法的名称和描述符
3. 运行时常量池
- 运行时常量池是方法区的一部分,在JDK8后HotSpot将方法区改为了在本地内存实现的元空间。所以运行时常量池存在于内存中,也就是class常量池被加载到内存后的版本。一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
- 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言不要求常量一定只有编译器才能产生,也就是说,并非预置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,例String类的intern()方法。-->作用与字符串常量池,在运行期间,字符串常量池也会加载到运行时常量池。
JVM内存配置参数的意思:https://blog.csdn.net/wang379275614/article/details/78471604
-Xmx:最大堆大小
-Xms:初始堆大小
-Xmn:年轻代大小
-XXSurvivorRatio:年轻代中Eden区与Survivor区的大小比值
例如 -Xmx10240m -Xms10240m -Xmn5120m -XXSurvivorRatio=3
即Eden大小 : 一个Survivor大小 = 3, 即一个survivor占1/5,总survivor区大小占2/5.
浙公网安备 33010602011771号