本篇的面试题基于网络整理,和自己编辑。在不断的完善补充哦。
Java 虚拟机,是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件(
.class
)。Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
但是,跨平台的是 Java 程序(包括字节码文件),,而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM 。
不同平台,不同 JVM
JVM 的结构基本上由 4 部分组成:
注意打钩的 4 个地方。
内存区,将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等。
执行引擎,执行引擎的任务是负责执行 class 文件中包含的字节码指令,相当于实际机器上的 CPU 。
1、Sun 有一个 Java System 属性来确定JVM的位数:32 或 64。
sun.arch.data.model=32 // 32 bit JVM
sun.arch.data.model=64 // 64 bit JVM
2、我可以使用以下语句来确定 JVM 是 32 位还是 64 位:
System.getProperty("sun.arch.data.model")
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB ,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。
64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64 ,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB 。甚至有的 JVM,如 Azul ,堆内存到 1000G 都是可能的。
Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位或者 4 个字节。意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。
JVM 运行内存的分类如下图所示:
注意,这个图是基于 JDK6 版本的运行内存的分类。
Java 线程私有,类似于操作系统里的 PC 计数器,它可以看做是当前线程所执行的字节码的行号指示器。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
Java线程私有,虚拟机栈描述的是 Java 方法执行的内存模型:
每个方法在执行的时候,都会创建一个栈帧用于存储局部变量、操作数、动态链接、方法出口等信息。
每个方法调用都意味着一个栈帧在虚拟机栈中入栈到出栈的过程。
和 Java 虚拟机栈的作用类似,区别是该区域为 JVM 提供使用 Native 方法的服务。
所有线程共享的一块区域,垃圾收集器管理的主要区域。
目前主要的垃圾回收算法都是分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等,默认情况下新生代按照
8:1:1
的比例来分配。根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘一样。
各个线程共享的一个区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
运行时常量池:是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。
直接内存(Direct Memory),并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中农定义的内存区域。在 JDK1.4 中新加入了 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通脱一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
实际上,后续的版本,主要对【方法区】做了一定的调整
JDK8 的改变
-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 合二为一。
java.lang.StackOverFlowError
错误;如果是堆内存没有可用的空间存储生成的对象,JVM 会抛出 java.lang.OutOfMemoryError
错误。-Xss
选项设置栈内存的大小,-Xms
选项可以设置堆的开始时的大小。当然,如果你记不住这个些,只要记住如下即可:
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
注意,加粗的文字部分
JAVA 对象创建的过程,如下图所示:
JAVA 对象创建的过程
1)检测类是否被加载
当虚拟机遇到 new
指令时,首先先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就执行类加载过程。
2)为对象分配内存
类加载完成以后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了。只需要在堆上分配所需要的内存即可。
具体的分配内存有两种情况:第一种情况是内存空间绝对规整,第二种情况是内存空间是不连续的。
多线程并发时会出现正在给对象 A 分配内存,还没来得及修改指针,对象 B 又用这个指针分配内存,这样就出现问题了。解决这种问题有两种方案:
-XX:+/-UseTLAB
参数决定。3)为分配的内存空间初始化零值
对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。
4)对对象进行其他设置
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的 hashcode ,GC 分代年龄等信息。
5)执行 init 方法
执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于 Java 程序来说还需要执行 init 方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了 init 方法之后,这个对象才真正能使用。
到此为止一个对象就产生了,这就是 new 关键字创建对象的过程。过程如下:
JAVA 对象创建的过程
另外,这个问题,面试官可能引申成 “
A a = new A()
经历过什么过程”的问题。
对象的内存布局包括三个部分:
对象的访问定位有两种:
对比两种方式?
这两种对象访问方式各有优势。
我们目前主要虚拟机 Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
在 Java 虚拟机中规范的描述中,除了程序计数器外,虚拟机内存的其它几个运行时区域都有发生的 OutOfMemoryError(简称为“OOM”) 异常的可能。
方法区和运行时常量池溢出
从 JDK8 开始,就变成元数据区的内存溢出。
Java 堆溢出的原因,有可能是内存泄露,可以使用 MAT 进行分析。
由于在 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于 HotSpot 来说,虽然 -Xoss
参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由 -Xss
参数设定。
关于虚拟机栈和本地方法栈,在 Java 虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。
StackOverflowError 不属于 OOM 异常哈。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
因为 JDK7 将常量池和静态变量放到 Java 堆里,所以无法触发运行时常量池溢出。如果想要触发,可以使用 JDK6 的版本。
因为 JDK8 将方法区溢出,所以无法触发方法区的内存溢出溢出。如果想要触发,可以使用 JDK7 的版本。
实际上,方法区的内存溢出在 JDK8 中,变成了元数据区的内存溢出。所以,重现方式,只是说,需要增加 -XX:MaxMetaspaceSize=10m
VM 配置项。
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因)。然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收也会发生内存泄露。例如说:
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()
方法什么时候被调用?它的目的是什么?
#finallize()
方法,是在释放该对象内存前由 GC (垃圾回收器)调用。
有两种方式:
每个对象有一个引用计数属性,新增一个引用时计数加 1 ,引用释放时计数减 1 ,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。目前在用的有 Python、ActionScript3 等语言。
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。目前在用的有 Java、C# 等语言。
可以,因为 Java 采用可达性分析的判断方式。
方法区可以被回收,但是价值很低,主要回收废弃的常量和无用的类。
如何判断无用的类,需要完全满足如下三个条件:
java.lang.Class
对象没有在任何地方被引用,无法在任何地方利用反射访问该类。Java 一共有四种引用类型:
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率。
不像 C 语言,我们可以控制内存的申请和释放,在 Java 中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对 GC 回收时机不可控的妥协。有以下几个使用场景可以充分的说明:
有四种算法:
标记-清除(Mark-Sweep)算法,是现代垃圾回收算法的思想基础。
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就可以了)。然后,在清除阶段,清除所有未被标记的对象。
标记-清除算法
标记整理算法,类似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
标记-整理算法
复制算法,可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。
复制算法
当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。
分代收集算法
1:1
的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。8:1:1
,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。SafePoint 安全点,顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定(the thread’s representation of it’s Java machine state is well described),比如记录OopMap 的状态,从而确定 GC Root 的信息,使 JVM 可以安全的进行一些操作,比如开始 GC 。
SafePoint 指的特定位置主要有:
如何使线程中断
主动式
主动式 JVM 设置一个全局变量,线程去按照某种策略检查这个变量一旦发现是 SafePoint 就主动挂起。
HostSpot 虚拟机采用的是主动式使线程中断。
被动式
被动式就是发个信号,例如关机、Control+C ,带来的问题就是不可控,发信号的时候不知道线程处于什么状态。
安全区域
如果程序长时间不执行,比如线程调用的 sleep 方法,这时候程序无法响应 JVM 中断请求这时候线程无法到达安全点,显然 JVM 也不可能等待程序唤醒,这时候就需要安全区域了。
安全区域是指一段代码片中,引用关系不会发生变化,在这个区域任何地方 GC 都是安全的,安全区域可以看做是安全点的一个扩展。
- 线程执行到安全区域的代码时,首先标识自己进入了安全区域,这样 GC 时就不用管进入安全区域的线程了.
- 线程要离开安全区域时就检查 JVM 是否完成了 GC Roots 枚举(或者整个 GC 过程),如果完成就继续执行,如果没有完成就等待直到收到可以安全离开的信号。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
新生代收集器
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 的 Full GC 都是单线程 mark sweep compact 算法,直到 JDK10 才优化为并行的。
配置 | 描述 |
---|---|
-XX:+UserSerialGC | 串行垃圾收集器 |
-XX:+UserParrallelGC | 并行垃圾收集器 |
-XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
-XX:ParallelCMSThreads | 并发标记扫描垃圾回收器 =为使用的线程数量 |
-XX:+UseG1GC | G1垃圾回收器 |
对象优先分配在 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 。
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 倍以上。
对象优先在新生代 Eden 区中分配,如果 Eden 区没有足够的空间时,就会触发一次 Young GC 。
Full GC 的触发条件有多个,FULL GC 的时候会 STOP THE WORD 。
System#gc()
方法时。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 自带
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()
方法,返回最大内存的字节数。配置 | 描述 |
---|---|
-Xms | 初始化堆内存大小 |
-Xmx | 堆内存最大值 |
-Xmn | 新生代大小 |
-XX:PermSize | 初始化永久代大小 |
-XX:MaxPermSize | 永久代最大容量 |
-XX:SurvivorRatio | 设置年轻代中 Eden 区与 Survivor 区的比值 |
-XX:Xmn | 设置年轻代大小 |
如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果我们仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。
Java8 :从永久代到元数据区 (注:Java8 中已经移除了永久代,新加了一个叫做元数据区的 native 内存区)。
类加载器(ClassLoader),用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java
文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class
文件)。
类加载器,负责读取 Java 字节代码,并转换成 java.lang.Class
类的一个实例。
Class#newInstance(...)
方法,就可以创建出该类的一个对象。虚拟机严格规定,有且仅有 5 种情况必须对类进行加载:
注意,有些文章会称为对类进行“初始化”。
new
、getstatic
、putstatic
、invokestatic
这四条字节码指令时,如果类还没进行初始化,则需要先触发其初始化。java.lang.reflect
包的方法对类进行反射调用的时候,如果类还没进行初始化,则需要先触发其初始化。#main(String[] args)
方法,虚拟机则会先初始化该主类。java.lang.invoke.MethodHandle
实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。类加载器 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()
来获取系统类加载器。系统加载器的加载路径是程序运行的当前路径。每个类加载器都有自己的命名空间(由该加载器及所有父类加载器所加载的类组成。
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
比如一个 Java 类 com.example.Sample
,编译之后生成了字节代码文件 Sample.class
。两个不同的类加载器 ClassLoaderA 和 ClassLoaderB 分别读取了这个 Sample.class
文件,并定义出两个 java.lang.Class
类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException 。
1、当前 ClassLoader 首先从自己已经加载的类中,查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
2、当前 ClassLoader 的缓存中没有找到被加载的类的时候
让我们来简单撸下源码。代码如下:
// java.lang.ClassLoader
// 删除部分无关代码
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,从缓存中获得 name 对应的类
Class> c = findLoadedClass(name);
if (c == null) { // 获得不到
try {
// 其次,如果父类非空,使用它去加载类
if (parent != null) {
c = parent.loadClass(name, false);
// 其次,如果父类为空,使用 Bootstrap 去加载类
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) { // 还是加载不到
// 最差,使用自己去加载类
c = findClass(name);
}
}
// 如果要解析类,则进行解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
2、隔离功能:主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String ,同时也避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛 java.lang.ClassCaseException
。
这也就是说,即使我们自己定义了一个
java.util.String
类,也不会被重复加载。
本篇文章梳理了java虚拟机相关高频的面试题,希望对你有帮助哦。
JAVA面试题传送门:
Java面试之集合篇
Java面试之并发篇(一)
Java面试之并发篇(二)
Java面试之并发篇(三)