目录
走进java虚拟机
什么是虚拟机?
JVM 由哪些部分组成?
怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?
Java 内存区域与内存溢出异常
JVM 运行内存的分类?
Java 内存堆和栈区别?
JAVA 对象创建的过程?
对象的内存布局是怎样的?
对象是如何定位访问的?
有哪些 OutOfMemoryError 异常?
Java 中会存在内存泄漏吗?
垃圾收集器与内存分配策略
什么是垃圾回收机制?
如何判断一个对象是否已经死去?
Java 对象有哪些引用类型?
JVM 垃圾回收算法?
什么是安全点?
JVM 垃圾收集器有哪些?
对象分配规则是什么?
什么是新生代 GC 和老年代 GC?
虚拟机性能监控与故障处理工具
JDK 的命令行工具有哪些可以监控虚拟机?
JDK 的可视化工具有哪些可以监控虚拟机?
怎么获取 Java 程序使用的内存?
调优案例分析与实战
常见 GC 的优化配置?
如何排查线程 Full GC 频繁的问题?
有看过 GC 日志么?
TODO JVM 线程案例
TODO 类文件结构
TODO 虚拟机类加载机制
类加载器是有了解吗?
类加载发生的时机是什么时候?
类加载器是如何加载 Class 文件的?
什么是双亲委派模型(Parent Delegation Model)?
什么是破坏双亲委托模型?
TODO 虚拟机字节码执行引擎
TODO 早期(编译期)优化
TODO JIT
TODO 晚期(运行期)优化
666. 彩蛋
基于网络整理,和自己编辑;随录一下我能想到的jvm缓存的事儿?如有疑问欢迎胖友们一起提问探讨。
Java 虚拟机,是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件( .class
)。
Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
但是,跨平台的是 Java 程序(包括字节码文件),,而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM 。
考察对 JVM 体系结构的掌握
《深入理解java虚拟机:JVM高级特性与最佳实践》(第3版)的 【2.2 运行时数据区域】;按照《java虚拟机规范》将运行时数据区域划分为5个部分:线程私有的虚拟机栈、本地方法栈、程序计数器3个部分, 线程共享的 堆、方法区2个部分;
JVM 的结构基本上由 4 部分组成:
注意打钩的 4 个地方。
内存区,将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等。
关于这一块,我们在 「Java 内存区域与内存溢出异常」 也会细看。
执行引擎,执行引擎的任务是负责执行 class 文件中包含的字节码指令,相当于实际机器上的 CPU 。
Sun 有一个 Java System 属性来确定JVM的位数:32 或 64。
sun.arch.data.model=32 // 32 bit JVM
sun.arch.data.model=64 // 64 bit JVM
我可以使用以下语句来确定 JVM 是 32 位还是 64 位:
|
? 32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB ,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。
64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64 ,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB 。甚至有的 JVM,如 Azul ,堆内存到 1000G 都是可能的。
? 64 位 JVM 中,int 的长度是多数?
Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位或者 4 个字节。意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。
《深入拆解 Java 虚拟机》 的 「2.3.1 对象的创建」 。
JVM 运行内存的分类如下图所示:
注意,这个图是基于 JDK6 版本的运行内存的分类。
8:1:1
的比例来分配。? 直接内存是不是虚拟机运行时数据区的一部分?
参见 《JVM 直接内存》 文章。
直接内存(Direct Memory),并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中农定义的内存区域。在 JDK1.4 中新加入了 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
? 直接内存(堆外内存)与堆内存比较?
? 实际上,后续的版本,主要对【方法区】做了一定的调整
JDK8 的改变
那么方法区还在么?F的解答:方法区在 Metaspace 中了,方法区都是一个概念的东西。? 狼哥通过撸源码获得该信息。
因为,《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。
同时,大多数用的 JVM 都是 Sun 公司的 HotSpot 。在 HotSpot 上把 GC 分代收集扩展至方法区,或者说使用永久带来实现方法区。
参考文章
? JDK8 之后 Perm Space 有哪些变动? MetaSpace ⼤⼩默认是⽆限的么? 还是你们会通过什么⽅式来指定⼤⼩?
-XX:MetaspaceSize
: 分配给类元数据空间(以字节计)的初始大小(Oracle 逻辑存储上的初始高水位,the initial high-water-mark)。此值为估计值,MetaspaceSize 的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。-XX:MaxMetaspaceSize
:分配给类元数据空间的最大值,超过此值就会触发Full GC 。此值默认没有限制,但应取决于系统内存的大小,JVM 会动态地改变此值。? 为什么要废弃永久代?
1)现实使用中易出问题。
由于永久代内存经常不够用或发生内存泄露,爆出异常 java.lang.OutOfMemoryError: PermGen
。
2)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
3)Oracle 可能会将HotSpot 与 JRockit 合二为一。
参照 JEP122 :http://openjdk.java.net/jeps/122 ,原文截取:
Motivation
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
即:移除永久代是为融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,不需要配置永久代。
java.lang.StackOverFlowError
错误;如果是堆内存没有可用的空间存储生成的对象,JVM 会抛出 java.lang.OutOfMemoryError
错误。-Xss
选项设置栈内存的大小,-Xms
选项可以设置堆的开始时的大小。当然,如果你记不住这个些,只要记住如下即可:
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
《深入拆解 Java 虚拟机》 的 「2.3.1 对象的创建」 。
注意,加粗的文字部分。
1)检测类是否被加载
当虚拟机遇到 new
指令时,首先先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就执行类加载过程。
2)为对象分配内存
类加载完成以后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了。只需要在堆上分配所需要的内存即可。
具体的分配内存有两种情况:第一种情况是内存空间绝对规整,第二种情况是内存空间是不连续的。
多线程并发时会出现正在给对象 A 分配内存,还没来得及修改指针,对象 B 又用这个指针分配内存,这样就出现问题了。解决这种问题有两种方案:
-XX:+/-UseTLAB
参数决定。3)为分配的内存空间初始化零值
对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。
4)对对象进行其他设置
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的 hashcode ,GC 分代年龄等信息。
5)执行 init 方法
执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于 Java 程序来说还需要执行 init 方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了 init 方法之后,这个对象才真正能使用。
到此为止一个对象就产生了,这就是 new 关键字创建对象的过程。过程如下:
另外,这个问题,面试官可能引申成 “
A a = new A()
经历过什么过程”的问题。
《深入拆解 Java 虚拟机》 的 「2.3.2 对象的内存布局」 。
对象的内存布局包括三个部分:
《深入拆解 Java 虚拟机》 的 「2.3.3 对象的访问定位」 。
对象的访问定位有两种:
? 对比两种方式?
这两种对象访问方式各有优势。
我们目前主要虚拟机 Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
《深入拆解 Java 虚拟机》 的 「2.4 实战:OutOfMemoryError 异常」 。
在 Java 虚拟机中规范的描述中,除了程序计数器外,虚拟机内存的其它几个运行时区域都有发生的 OutOfMemoryError(简称为“OOM”) 异常的可能。
方法区和运行时常量池溢出
从 JDK8 开始,就变成元数据区的内存溢出。
本机直接内存溢出
1)Java 堆溢出
重现方式,参见 《Java 堆溢出》 文章。
另外,Java 堆溢出的原因,有可能是内存泄露,可以使用 MAT 进行分析。
2)虚拟机栈和本地方法栈溢出
由于在 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于 HotSpot 来说,虽然 -Xoss
参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由 -Xss
参数设定。
关于虚拟机栈和本地方法栈,在 Java 虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。
StackOverflowError 不属于 OOM 异常哈。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
重现方式,参见 《OutOfMemoryError 异常 —— 虚拟机栈和本地方法栈溢出》 文章。
3)运行时常量池溢出
因为 JDK7 将常量池和静态变量放到 Java 堆里,所以无法触发运行时常量池溢出。如果想要触发,可以使用 JDK6 的版本。
重现方式,参见 《JVM 内存溢出 - 方法区及运行时常量池溢出》 文章。
4)方法区的内存溢出
因为 JDK8 将方法区溢出,所以无法触发方法区的内存溢出溢出。如果想要触发,可以使用 JDK7 的版本。
重现方式,参见 《Java 方法区溢出》 文章。
5)元数据区的内存溢出
实际上,方法区的内存溢出在 JDK8 中,变成了元数据区的内存溢出。所以,重现方式,还是参见 《Java 方法区溢出》 文章,只是说,需要增加 -XX:MaxMetaspaceSize=10m
VM 配置项。
6)本机直接内存溢出
重现方式,参见 《JVM 内存溢出 —— 直接内存溢出》 文章。
另外,非常推荐一篇文章,胖友耐心阅读,提供了更多有趣的案例,《Java 内存溢出(OOM)异常完全指南》 。
? 当出现了内存溢出,你怎么排错?
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因)。然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收也会发生内存泄露。例如说:
《深入拆解 Java 虚拟机》 的 「3.1 概述」 。
new
或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由 Java 虚拟机通过垃圾回收机制完成的。GC 为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控。System#gc()
或 Runtime#getRuntime()#gc()
,但 JVM 也可以屏蔽掉显示的垃圾回收调用。? 为什么不建议在程序中显式的声明 System.gc()
?
因为显式声明是做堆内存全扫描,也就是 Full GC ,是需要停止所有的活动的(Stop The World Collection),对应用很大可能存在影响。
另外,调用 System.gc()
方法后,不会立即执行 Full GC ,而是虚拟机自己决定的。
? 如果一个对象的引用被设置为 null
, GC 会立即释放该对象的内存么?
不会, 这个对象将会在下一次 GC 循环中被回收。
? #finalize()
方法什么时候被调用?它的目的是什么?
《深入拆解 Java 虚拟机》 的 「3.2.4 生存还是死亡」 。
#finallize()
方法,是在释放该对象内存前由 GC (垃圾回收器)调用。
《深入拆解 Java 虚拟机》 的 「3.2 对象已死吗」 。
有两种方式:
1)引用计数
每个对象有一个引用计数属性,新增一个引用时计数加 1 ,引用释放时计数减 1 ,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。目前在用的有 Python、ActionScript3 等语言。
2)可达性分析(Reachability Analysis)
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。目前在用的有 Java、C# 等语言。
? 如果 A 和 B 对象循环引用,是否可以被 GC?
可以,因为 Java 采用可达性分析的判断方式。
? 在 Java 语言里,可作为 GC Roots 的对象包括以下几种?
? 方法区是否能被回收?
方法区可以被回收,但是价值很低,主要回收废弃的常量和无用的类。
如何判断无用的类,需要完全满足如下三个条件:
java.lang.Class
对象没有在任何地方被引用,无法在任何地方利用反射访问该类。《深入拆解 Java 虚拟机》 的 「3.2.3 再谈引用类型」 。
Java 一共有四种引用类型:
1)强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
艿艿:不然,代码都没法写了 ?
2)软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
使用示例,见 《精尽 MyBatis 源码分析 —— 缓存模块》 的 「2.10 SoftCache」 小节。
3)弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
使用示例,见
4)虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
如果胖友想看看各种引用在 GC 下的效果,可以看看 《Java 中的四种引用类型》 提供的代码示例。
? WeakReference 与 SoftReference的区别?
虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率。
? 为什么要有不同的引用类型?
不像 C 语言,我们可以控制内存的申请和释放,在 Java 中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对 GC 回收时机不可控的妥协。有以下几个使用场景可以充分的说明:
《深入拆解 Java 虚拟机》 的 「3.3 垃圾回收算法」 。
有四种算法:
1)标记-清除算法
标记-清除(Mark-Sweep)算法,是现代垃圾回收算法的思想基础。
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就可以了)。然后,在清除阶段,清除所有未被标记的对象。
2)标记-整理算法
标记整理算法,类似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
3)复制算法
复制算法,可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。
4)分代收集算法
当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。
1:1
的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。8:1:1
,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。《深入拆解 Java 虚拟机》 的 「3.4.2 安全点」 。
SafePoint 安全点,顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定(the thread’s representation of it’s Java machine state is well described),比如记录OopMap 的状态,从而确定 GC Root 的信息,使 JVM 可以安全的进行一些操作,比如开始 GC 。
SafePoint 指的特定位置主要有:
详细的内容,可以看看 《深入学习 JVM-JVM 安全点和安全区域》 。
如何使线程中断
主动式
主动式 JVM 设置一个全局变量,线程去按照某种策略检查这个变量一旦发现是 SafePoint 就主动挂起。
HostSpot 虚拟机采用的是主动式使线程中断。
被动式
被动式就是发个信号,例如关机、Control+C ,带来的问题就是不可控,发信号的时候不知道线程处于什么状态。
安全区域
如果程序长时间不执行,比如线程调用的 sleep 方法,这时候程序无法响应 JVM 中断请求这时候线程无法到达安全点,显然 JVM 也不可能等待程序唤醒,这时候就需要安全区域了。
安全区域是指一段代码片中,引用关系不会发生变化,在这个区域任何地方 GC 都是安全的,安全区域可以看做是安全点的一个扩展。
- 线程执行到安全区域的代码时,首先标识自己进入了安全区域,这样 GC 时就不用管进入安全区域的线程了.
- 线程要离开安全区域时就检查 JVM 是否完成了 GC Roots 枚举(或者整个 GC 过程),如果完成就继续执行,如果没有完成就等待直到收到可以安全离开的信号。
《深入拆解 Java 虚拟机》 的 「3.5 垃圾收集器」 。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
新生代收集器
ParNew 收集器
ParNew 收集器,是 Serial 收集器的多线程版。
Parallel Scavenge 收集器
小结表格如下:
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
关于每种垃圾收集器的说明,请看 如下文章:
? G1 和 CMS 的区别?
G1 和 CMS 的 Full GC 都是单线程 mark sweep compact 算法,直到 JDK10 才优化为并行的。
感兴趣的胖友,可以看看 《GC 优化的一些总结》 的分析。
? CMS 算法的过程,CMS 回收过程中 JVM 是否需要暂停?
会有短暂的停顿。详细的,可以看看 《[jvm][面试] 并发收集器 CMS(Concurrent Mark-Sweep)》 。
? 如何使用指定的垃圾收集器
配置 | 描述 |
---|---|
-XX:+UserSerialGC | 串行垃圾收集器 |
-XX:+UserParrallelGC | 并行垃圾收集器 |
-XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
-XX:ParallelCMSThreads | 并发标记扫描垃圾回收器 =为使用的线程数量 |
-XX:+UseG1GC | G1垃圾回收器 |
《深入拆解 Java 虚拟机》 的 「3.6 对象分配与回收策略」 。
对象优先分配在 Eden 区。
如果 Eden 区无法分配,那么尝试把活着的对象放到 Survivor0 中去(Minor GC)
- 如果 Survivor0 可以放入,那么放入之后清除 Eden 区。
- 如果 Survivor0 不可以放入,那么尝试把 Eden 和 Survivor0 的存活对象放到 Survivor1 中。
- 如果 Survivor1 可以放入,那么放入 Survivor1 之后清除 Eden 和 Survivor0 ,之后再把 Survivor1 中的对象复制到 Survivor0 中,保持 Survivor1 一直为空。
- 如果 Survivor1 不可以放入,那么直接把它们放入到老年代中,并清除 Eden 和 Survivor0 ,这个过程也称为分配担保。
ps:清除 Eden、Survivor 区,就是 Minor GC 。
总结来说,分配的顺序是:新生代(Eden => Survivor0 => Survivor1)=> 老年代
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。
这样做的目的是,避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。
虚拟机为每个对象定义了一个年龄计数器,如果对象经过了 1 次 Minor GC 那么对象会进入 Survivor 区,之后每经过一次 Minor GC 那么对象的年龄加 1 ,知道达到阀值对象进入老年区。
动态判断对象的年龄。
为了更好的适用不同程序的内存情况,虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代。
如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。
每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次 Full GC ,如果小于检查 HandlePromotionFailure 设置,如果
true
则只进行 Monitor GC ,如果false
则进行 Full GC 。
? 为什么新生代内存需要有两个 Survivor 区?
详细的原因,可以看 《为什么新生代内存需要有两个 Survivor 区》 文章。
GC 经常发生的区域是堆区,堆区还可以细分为
默认新生代(Young)与老年代(Old)的比例的值为
1:2
(该值可以通过参数–XX:NewRatio
来指定)。默认的
Eden:from:to=8:1:1
(可以通过参数–XX:SurvivorRatio
来设定)。
新生代GC(MinorGC/YoungGC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 MinorGC 非常频繁,一般回收速度也比较快。
老年代GC(MajorGC/FullGC):指发生在老年代的 GC,出现了 MajorGC,经常会伴随至少一次的 MinorGC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 MajorGC 的策略选择过程)。MajorGC 的速度一般会比 MinorGC 慢 10 倍以上。
? 什么情况下会出现 Young GC?
对象优先在新生代 Eden 区中分配,如果 Eden 区没有足够的空间时,就会触发一次 Young GC 。
? 什么情况下回出现 Full GC?
Full GC 的触发条件有多个,FULL GC 的时候会 STOP THE WORD 。
System#gc()
方法时。《深入拆解 Java 虚拟机》 的 「4.2 JDK 的命令行工具」 。
jps :虚拟机进程状况工具
JVM Process Status Tool ,显示指定系统内所有的HotSpot虚拟机进程。
jstat :虚拟机统计信息监控工具
JVM statistics Monitoring ,是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jinfo :Java 配置信息工具
JVM Configuration info ,这个命令作用是实时查看和调整虚拟机运行参数。
jmap :Java 内存映射工具
JVM Memory Map ,命令用于生成 heap dump 文件。
jhat :虚拟机堆转储快照分析工具
JVM Heap Analysis Tool ,命令是与 jmap 搭配使用,用来分析 jmap 生成的 dump 文件。jhat 内置了一个微型 的HTTP/HTML 服务器,生成 dump 的分析结果后,可以在浏览器中查看。
jstack :Java 堆栈跟踪工具
Java Stack Trace ,用于生成 Java 虚拟机当前时刻的线程快照。
HSDIS :JIT 生成代码反编译
《深入拆解 Java 虚拟机》 的 「4.3 JDK 的可视化工具」 。
Java 自带
JConsole :Java 监视与管理控制台
Java Monitoring and Management Console 是从 Java5 开始,在 JDK 中自带的 Java 监控和管理控制台,用于对 JVM 中内存,线程和类等的监控。
VisualVM :多合一故障处理工具
JDK 自带全能工具,可以分析内存快照、线程快照、监控内存变化、GC变化等。
特别是 BTrace 插件,动态跟踪分析工具。
第三方
MAT :内存分析工具
Memory Analyzer Tool ,一个基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。
GChisto :一款专业分析 GC 日志的工具。
另外,一些开源项目,例如 SkyWalking、Cat ,也提供了 JVM 监控的功能,更加适合生产环境,对 JVM 的监控。
可以通过 java.lang.Runtime
类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。
Runtime#freeMemory()
方法,返回剩余空间的字节数。Runtime#totalMemory()
方法,总内存的字节数。Runtime#maxMemory()
方法,返回最大内存的字节数。在 《深入拆解 Java 虚拟机》 的 「第5章 调优案例分析与实战」 中,已经提供了一些案例,建议胖友可以看看。
配置 | 描述 |
---|---|
-Xms | 初始化堆内存大小 |
-Xmx | 堆内存最大值 |
-Xmn | 新生代大小 |
-XX:PermSize | 初始化永久代大小 |
-XX:MaxPermSize | 永久代最大容量 |
-XX:SurvivorRatio | 设置年轻代中 Eden 区与 Survivor 区的比值 |
-XX:Xmn | 设置年轻代大小 |
另外,也可以看看 《JVM 调优》 文章。
? JVM 的永久代中会发生垃圾回收么?
如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果我们仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。
Java8 :从永久代到元数据区 (注:Java8 中已经移除了永久代,新加了一个叫做元数据区的 native 内存区)。
艿艿:这个问题,一般面试不会问,加进来,主要让胖友知道,有这么个知识点。
参见文章如下:
TODO 问晓峰
TODO 问阿牛
TODO 问狼哥
TODO 问闪电侠
TODO 问超哥
类加载器,是面试的重点,所以要掌握好。当然,相对来说难度也不算上~
《深入拆解 Java 虚拟机》 的 「7.4 类加载器」 。
类加载器(ClassLoader),用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java
文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class
文件)。
类加载器,负责读取 Java 字节代码,并转换成 java.lang.Class
类的一个实例。
Class#newInstance(...)
方法,就可以创建出该类的一个对象。《深入拆解 Java 虚拟机》 的 「7.2 类加载的时机」 。
虚拟机严格规定,有且仅有 5 种情况必须对类进行加载:
注意,有些文章会称为对类进行“初始化”。
new
、getstatic
、putstatic
、invokestatic
这四条字节码指令时,如果类还没进行初始化,则需要先触发其初始化。java.lang.reflect
包的方法对类进行反射调用的时候,如果类还没进行初始化,则需要先触发其初始化。#main(String[] args)
方法,虚拟机则会先初始化该主类。java.lang.invoke.MethodHandle
实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。《深入拆解 Java 虚拟机》 的 「7.2 类加载的时机」 。
下图所示是 ClassLoader 加载一个 .class
文件到 JVM 时需要经过的步骤:
.class
文件并把这个文件包含的字节码加载到内存中。第三阶段,Initialization(类中静态属性和初始化赋值),以及Using(静态块的执行)等。
注意,不包括卸载(Unloading)部分。
? 1)加载
加载是“类加载”过程的第一阶段,胖友不要混淆这两个名字。
在加载阶段,虚拟机需要完成以下三件事情:
java.lang.Class
对象,作为对方法区中这些数据的访问入口。相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个 java.lang.Class
类的对象,这样便可以通过该对象访问方法区中的这些数据。
? 2)加载
2.1 验证:确保被加载的类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。java.lang.Object
之外。验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2.2 准备:为类的静态变量分配内存,并将其初始化为默认值
准备阶段,是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static
),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
思考下,对于类本身,静态变量就是其属性。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如 0
、0L
、null
、false
等),而不是被在 Java 代码中被显式地赋予的值。
假设一个类变量的定义为: public static int value = 3
。那么静态变量 value
在准备阶段过后的初始值为 0
,而不是 3
。因为这时候尚未开始执行任何 Java 方法,而把 value
赋值为 3
的 public static
指令是在程序编译后,存放于类构造器
方法之中的,所以把 value
赋值为 3
的动作将在初始化阶段才会执行。
这里还需要注意如下几点:
- 对基本数据类型来说,对于类变量(
static
)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。- 对于同时被
static
和final
修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final
修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。- 对于引用数据类型 reference 来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的空值,即
null
。- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的“空”值。
3、如果类字段的字段属性表中存在 ConstantValue 属性,即同时被 final
和 static
修饰,那么在准备阶段变量 value
就会被初始化为 ConstValue 属性所指定的值。
假设上面的类变量 value
被定义为: public static final int value = 3
。编译时, javac
将会为 value
生成 ConstantValue 属性。在准备阶段虚拟机就会根据 ConstantValue 的设置将 value
赋值为 3。我们可以理解为 static final
常量在编译期就将其结果放入了调用它的类的常量池中。
2.3 解析:把类中的符号引用转换为直接引用
这个步骤,艿艿看的也有点懵逼。
R 大在 《JVM 符号引用转换直接引用的过程?》 和 《JVM 里的符号引用如何存储?》 做过解答,看的还是懵逼。
解析阶段,是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
? 3)初始化
初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:
JVM 初始化步骤:
《深入拆解 Java 虚拟机》 的 「7.4.2 双亲委派模型」 。
类加载器 ClassLoader 是具有层次结构的,也就是父子关系,如下图所示:
Bootstrap ClassLoader :根类加载器,负责加载 Java 的核心类,它不是 java.lang.ClassLoader
的子类,而是由 JVM 自身实现。
此处,说的是 Hotspot 的情况下。
Extension ClassLoader :扩展类加载器,扩展类加载器的加载路径是 JDK 目录下 jre/lib/ext
。扩展加载器的 #getParent()
方法返回 null
,实际上扩展类加载器的父类加载器是根加载器,只是根加载器并不是 Java 实现的。
-classpath
选项、java.class.path
系统属性或 CLASSPATH
环境变量所指定的 jar 包和类路径。程序可以通过 #getSystemClassLoader()
来获取系统类加载器。系统加载器的加载路径是程序运行的当前路径。该模型要求除了顶层的 Bootstrap 启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。简略代码如下:
|
每个类加载器都有自己的命名空间(由该加载器及所有父类加载器所加载的类组成。
? Java 虚拟机是如何判定两个 Java 类是相同的?
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
比如一个 Java 类 com.example.Sample
,编译之后生成了字节代码文件 Sample.class
。两个不同的类加载器 ClassLoaderA 和 ClassLoaderB 分别读取了这个 Sample.class
文件,并定义出两个 java.lang.Class
类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException 。
? 双亲委派模型的工作过程?
1、当前 ClassLoader 首先从自己已经加载的类中,查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
2、当前 ClassLoader 的缓存中没有找到被加载的类的时候
让我们来简单撸下源码。代码如下:
艿艿:要不要跟面试官吹下,自己看过源码得知~
|
? 为什么优先使用父 ClassLoader 加载类?
2、隔离功能:主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String ,同时也避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛 java.lang.ClassCaseException
。
这也就是说,即使我们自己定义了一个
java.util.String
类,也不会被重复加载。
《深入拆解 Java 虚拟机》 的 「7.4.3 破坏双亲委派模型」 。
正如我们上面看到的源码,破坏双亲委托模型,需要做的是,#loadClass(String name, boolean resolve)
方法中,不调用父 parent
ClassLoader 方法去加载类,那么就成功了。那么我们要做的仅仅是,错误的覆盖 ##loadClass(String name, boolean resolve)
方法,不去使用父 parent
ClassLoader 方法去加载类即可。
想要深入的胖友,可以深入看看如下文章:
? 如何自定义 ClassLoader 类?
直接参考 《Java 自定义 ClassLoader 实现 JVM 类加载》 文章即可。
? OSGI 如何实现模块化热部署?
艿艿:了解即可。
OSGI 实现模块化热部署的关键,是它自定义的类加载器机制的实现。每一个程序模块都有一个自己的类加载器,当需要等换一个模块时,就把模块连同类加载器一起换掉以实现代码的热替换。
有时我们会听到 JIT 这个概念,并说它是 JVM 的一部分,这让我们很困惑。JIT 是 JVM 的一部分,它可以在同一时间编译类似的字节码来优化将字节码转换为机器特定语言的过程相似的字节码,从而将优化字节码转换为机器特定语言的过程,这样减少转换过程所需要花费的时间。
总的来说,JVM 主要提问的点,如下脑图:
参考与推荐如下文章: