JVM
JVM
内存模型
JVM的内存模型介绍一下
JVM内存模型里的堆和栈有什么区别?
栈中存的到底是指针还是对象?
堆分为哪几部分呢?
如果有个大对象一般是在哪个区域?
程序计数器的作用,为什么是私有的?
方法区中的方法的执行过程?
方法区中还有哪些东西?
String保存在哪里呢?
String s = new String(“abc”)执行过程中分别对应哪些内存区域?
引用类型有哪些?有什么区别?
弱引用了解吗?举例说明在哪里可以用?
内存泄漏和内存溢出的理解?
jvm 内存结构有哪几种内存溢出的情况
有具体的内存泄漏和内存溢出的例子么请举例及解决方案?
类初始化和加载
创建对象的过程?
对象的生命周期
类加载器有哪些?
双亲委派模型的作用
讲一下类加载过程?
讲一下类的加载和双亲委派原则
垃圾回收
什么是Java里的垃圾回收?如何触发垃圾回收?
判断垃圾的方法有哪些?
垃圾回收算法是什么,是为了解决了什么问题?
垃圾回收算法有哪些?
垃圾回收器有哪些?
标记清除算法的缺点是什么?
垃圾回收算法哪些阶段会stop the world?
minorGC、majorGC、fullGC的区别,什么场景触发full GC
垃圾回收器 CMS 和 G1的区别?
什么情况下使用CMS,什么情况使用G1?
G1回收器的特色是什么?
GC只会对堆进行GC吗?
基础概念
什么是 Java 虚拟机?为什么 Java 被称作是“平台无关的编程语言”?
Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。
Java 源文件被编译成能被 Java 虚拟机执行的字节码文件,Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?
你可以检查某些系统属性如 sun.arch.data.model 或 os.arch 来获取该信息。
32 位 JVM 和 64 位 JVM 的最大堆内存分别是多少?
理论上说上 32 位的 JVM 堆内存可以到达 2^32, 即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5GB,Solaris 大约3GB。64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。
自动内存管理
Java 垃圾回收机制
在 Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
GC 是什么?为什么要 GC
GC 是垃圾收集的意思,内存处理是开发人员容易出现问题的地方,忘记或者错误地内存回收会导致程序或者系统的不稳定甚至崩溃,Java 提供的垃圾回收机制可以自动检测对象是否超过作用域从而达到自动回收的目的。
GC 的两种判定方法:引用计数与引用链。
见 对象已死?
引用的分类
见 再谈引用(引用的分类)。
JVM 的内存模型(运行时数据区)及每个模块的作用
见 运行时数据区域。
详解 JVM 内存模型-JVM 的主要组成部分及其作用
JVM 包含两个子系统和两个组件,两个子系统为 Class loader(类装载)、Execution engine (执行引擎);两个组件为 Runtime data area(运行时数据区)、Native Interface (本地接口)。
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载 class 文件到 Runtime data area 中的 method area。
- Execution engine(执行引擎):执行 classes 中的指令。
- Native Interface(本地接口):与 native libraries 交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的 JVM 的内存。
作用:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
回收方法区
见 回收方法区。
分派:静态分派与动态分派
静态分派。
- 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是方法重载(根据参数的静态类型来定位目标方法)。
- 静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机执行的。
动态分派。
- 在运行期根据实际类型确定方法执行版本。
对象分配规则
- 对象优先分配在 Eden 区,如果 Eden 区没有足够的空间时,虚拟机执行一次 Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了 1 次 Minor GC 那么对象会进入 Survivor 区,之后每经过一次 Minor GC 那么对象的年龄加 1,知道达到阀值对象进入老年区。
- 动态判断对象的年龄。如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次 Full GC,如果小于检查 HandlePromotionFailure 设置,如果true 则只进行 Monitor GC,如果 false 则进行 Full GC。
什么是直接内存?
见 直接内存
Java 对象的定位方式
Java 程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有句柄和直接指针两种方式。
- 指针: 指向对象,代表一个对象在内存中的起始地址。
- 句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
直接指针。
如果使用直接指针访问,引用中存储的直接就是对象地址,那么 Java 堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。其优点如下。
- 速度更快,节省了一次指针定位的时间开销。由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
句柄访问。 Java 堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,其优点如下。
- 引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
具体构造如下图所示:
对象分配内存
类加载完成后,接着会在 Java 堆中划分一块内存分配给对象。内存分配根据 Java 堆是否规整,有两种方式: * 指针碰撞:如果 Java 堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。 * 空闲列表:如果 Java 堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。 选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
垃圾收集算法
见 垃圾收集算法。
垃圾回收器
见 经典的垃圾收集器。
为什么 CMS 两次标记时要 stop the world
G1 垃圾回收具体流程
G1 和 CMS 的比较
- 从 G1 开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配率(Allocation Rate),而不追求一次把这个 Java 堆完全清理干净。
- 都关注停顿时间的控制,都被称为“The Mostly Concurrent Collectors”。
- 收集算法不同,CMS 基于“标记-清除”算法,G1 整体基于“标记-整理”,局部基于“标记-复制”,G1 不会产生内存碎片。
- 内存占用不用,G1 的卡表实现复杂,导致 G1 的记忆集和其他内存可能占堆内存容量 20% 以上,CMS 相对较少。
- 执行负载上不同,在并发标记时,G1 实现快照搜索(SATB)需要写前、后屏障,CMS 实现增量更新,使用写后屏障。CMS 写屏障,直接的同步操作,G1 不得不实现类似消息队列,异步处理。
分代垃圾回收器是怎么工作的?
以 CMS 收集器为例说明。
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 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)时,升级为老生代。大对象也会直接进入老生代。老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
JVM 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代?
思路:先描述一下 Java 堆内存划分,再解释 Minor GC,Major GC,full GC,描述它们之间转化流程。
简述 Java 内存分配与回收策略以及 Minor GC 和 Major GC
内存分配:见 “2.9 对象分配规则”、“对象分配内存”。
回收策略:见 垃圾收集算法。
Minor GC 和 Major GC:见 分代收集理论。
JVM - 一个案例反推不同 JDK 版本的 intern 机制以及 intern C++ 源码解析
见《第 2 章 Java 内存区域与内存溢出异常》的 《2.4.3 方法区和运行时常量池溢出(intern)》 小节。
垃圾回收的优点和原理。并考虑2种回收机制
- Java 语言最显著的特点就是引入了垃圾回收机制,它使 Java 程序员在编写程序时不再考虑内存管理的问题。
- 由于有这个垃圾回收机制,Java 中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。
- 垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。
- 垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。
- 程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。
垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。
详解见:Java 垃圾回收的优点和原理
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
- 对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当 GC 确定一些对象为"不可达"时,GC 就有责任回收这些内存空间。
- 可以。
- 程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。
堆栈的区别
- 物理地址。堆的物理地址分配对对象是不连续的。因此性能慢些。在 GC 的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩);栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
- 内存分别。堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定,一般堆大小远远大于栈;栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
- 存放的内容。堆存放的是对象的实例和数组。因此该区更关注的是数据的存储;栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
- 程序的可见度。堆对于整个应用程序都是共享、可见的。栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
还有以下区别等。
- 静态变量放在方法区。
- 静态的对象还是放在堆。
JVM 的永久代(元空间)中会发生垃圾回收么?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。请参考下 Java8:从永久代到元数据区 (注:Java8 中已经移除了永久代,新加了一个叫做元空间的 native 内存区)。
Minor GC 与 Full GC 分别在什么时候发生?
Minor GC 发生: 新生代内存不够用时候发生 MGC 也叫 YGC。 Full GC 发生: 除直接调用 System.gc 外,触发 Full GC 执行的情况有如下四种。
1. 老年代空间不足
老生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行 Full GC 后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的 FullGC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
1. Permanet Generation 空间满
PermanetGeneration 中存放的为一些 class 的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation
可能会被占满,在未配置为采用 CMS GC 的情况下会执行 Full GC。如果经过 Full GC 仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免 Perm Gen 占满造成 Full GC 现象,可采用的方法为增大 Perm Gen 空间或转为使用 CMS GC。
1. CMS GC 时出现 promotion failed 和 concurrent mode failure
对于采用 CMS 进行老生代 GC 的程序而言,尤其要注意 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种状况,当这两种状况出现时可能会触发 Full GC。
promotionfailed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入老生代,而此时老生代也放不下造成的;concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老生代,而此时老生代空间不足造成的。
应对措施为:增大 survivorspace、老生代空间或调低触发并发 GC 的比率,但在 JDK 5.0+、6.0+ 的版本中有可能会由于 JDK 的 bug29 导致 CMS 在 remark 完毕后很久才触发 sweeping 动作。对于这种状况,可通过设置 -XX:CMSMaxAbortablePrecleanTime=5(单位为 ms)来避免。
1. 统计得到的 Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为 了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行 Minor GC 时,做了一个判断,如果之前统计所得到的 Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发 Full GC。
例如程序第一次触发 MinorGC 后,有 6MB 的对象晋升到旧生代,那么当下一次 Minor GC 发生时,首先检查旧生代的剩余空间是否大于 6MB,如果小于 6MB,则执行 Full GC。
当新生代采用 PS GC 时,方式稍有不同,PS GC 是在 Minor GC 后也会检查,例如上面的例子中第一次 Minor GC 后,PS GC 会检查此时旧生代的剩余空间是否大于 6MB,如小于,则触发对旧生代的回收。除了以上 4 种状况外,对于使用 RMI 来进行 RPC 或管理的 Sun JDK 应用而言,默认情况下会一小时执行一次 Full GC。可通过在启动时通过 -java -Dsun.rmi.dgc.client.gcInterval=3600000 来设置 Full GC 执行的间隔时间或通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc
Java 会存在内存泄漏吗?请简单描述
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java 是有 GC 垃圾回收机制的,也就是说,不再被使用的对象,会被 GC 自动回收掉,自动从内存中清除。但是,即使这样,Java 也还是存在着内存泄漏的情况,Java 导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是 Java 中内存泄露的发生场景。
什么情况下会发生栈内存溢出
思路:描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK 的话可以给面试官手写是一个栈溢出的 demo。
参考答案: 栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常,方法递归调用产生这种结果。
如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么 Java 虚拟机将抛出一个 OutOfMemory 异常。(线程启动过多)
参数 -Xss 去调整 JVM 栈的大小。
新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
JVM 内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为 Eden 和 Survivor
思路: 先讲一下 JAVA 堆,新生代的划分,再谈谈它们之间的转化,相互之间一些参数的配置(如: –XX:NewRatio,–XX:SurvivorRatio等),再解释为什么要这样划分,最好加一点自己的理解。 1)共享内存区划分 共享内存区 = 持久带 + 堆 持久带 = 方法区 + 其他 Java 堆 = 老年代 + 新生代 新生代 = Eden + S0 + S1 2)一些参数的配置 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定) Survivor 区中的对象被复制次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold) 3)为什么要分为 Eden 和 Survivor?为什么要设置两个 Survivor 区? 如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发 Major GC。 老年代的内存空间远大于新生代,进行一次 Full GC 消耗的时间比 Minor GC 长得多,所以需要分为 Eden 和 Survivor。 Survivor 的存在意义,就是减少被送到老年代的对象,进而减少 Full GC 的发生,Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。 设置两个 Survivor 区最大的好处就是解决了碎片化,刚刚新建的对象在 Eden 中,经历一次 Minor GC,Eden 中的存活对象就会被移动到第一块 survivor space S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0 中的存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为这种复制算法保证了 S1 中来自 S0 和 Eden 两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。
堆里面的分区:Eden,survivalfrom to,老年代,各自的特点。
- JVM中堆空间可以分成三个大区,新生代、老年代、永久代。
- 新生代可以划分为三个区,Eden区,两个幸存区。
在 JVM 运行时,可以通过配置以下参数改变整个 JVM 堆的配置比例
1. JVM 运行时堆的大小
-Xms 堆的最小值
-Xmx 堆空间的最大值
2. 新生代堆空间大小调整
-XX:NewSize 新生代的最小值
-XX:MaxNewSize 新生代的最大值
-XX:NewRatio 设置新生代与老年代在堆空间的大小
-XX:SurvivorRatio 新生代中 Eden 所占区域的大小
3. 永久代大小调整 -XX:MaxPermSize
4. 其他
-XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收
虚拟机执行子系统
类的生命周期
见 类加载的时机
类装载的执行过程
见 类加载的过程
JVM 加载 class 文件的原理机制?
类的加载是将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。通常是创建一个字节数组读入 .class 文件,然后产生与所加载类对应的 Class 对象。
加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:
- 如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类。
- 如果类中存在初始化语句,就依次执行这些初始化语句。
什么是类加载器,类加载器有哪些?
见 双亲委派模型。
Java 程序运行机制
- 首先利用 IDE 集成开发工具编写 Java 源代码,源文件的后缀为 .java;
- 再利用编译器(javac 命令)将源代码编译成字节码文件,字节码文件的后缀名为 .class;
- 运行字节码的工作是由解释器(java 命令)来完成的。

从上图可以看,java 文件通过编译器变成了 .class 文件,接下来类加载器又将这些 .class 文件加载到 JVM 中。
其实可以一句话来解释:类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。
Java 对象创建过程
首先让我们看看 Java 中提供的几种对象创建方式:
创建方式 | 描述 |
---|---|
使用 new 关键字 | 调用了构造函数 |
使用 Class 的 newInstance 方法 | 调用了构造函数 |
使用 Constructor 类的 newInstance 方法 | 调用了构造函数 |
使用 clone 方法 | 没有调用构造函数 |
使用反序列化 | 没有调用构造函数 |
下面是对象创建的主要流程:
虚拟机遇到一条 new 指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若 Java 堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS 同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行 init
方法。
Java 对象结构
Java 对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成,第一部分存储对象自身的运行时数据:对象的哈希码、GC 分代年龄、锁标识状态、线程持有的锁、偏向线程 ID(一般占 32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)。
对齐填充:JVM 要求对象起始地址必须是 8 字节的整数倍(8 字节对齐)。
程序编译与代码优化
高效并发
处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
- 对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过
-XX:+/-UserTLAB1
参数来设定虚拟机是否使用 TLAB。
JVM 实战
GC 日志分析
摘录 GC 日志一部分(前部分为年轻代 gc 回收;后部分为 full gc 回收):
2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]
2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen 属于 Parallel 收集器。其中 PSYoungGen 表示 gc 回收前后年轻代的内存变化;ParOldGen 表示 gc 回收前后老年代的内存变化;PSPermGen 表示 gc 回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少 full gc 的次数。
调优命令、调优工具
JVM 性能调优
1)堆栈配置相关
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
-Xmx3550m: 最大堆大小为 3550m。
-Xms3550m: 设置初始堆大小为 3550m。
-Xmn2g: 设置年轻代大小为 2g。
-Xss128k: 每个线程的堆栈大小为 128k。
-XX:MaxPermSize: 设置持久代大小为 16m
-XX:NewRatio=4: 设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 设置年轻代中 Eden 区与 Survivor 区的大小比值。设置为 4,则两个 Survivor 区与一个 Eden 区的比值为 2:4,一个 Survivor 区占整个年轻代的 1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。
2)垃圾收集器相关
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseParallelGC-XX:ParallelGCThreads=20-XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5-XX:+UseCMSCompactAtFullCollection:
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20: 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次 GC 以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
3)辅助信息相关
-XX:+PrintGC-XX:+PrintGCDetails
-XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
怎么打出线程栈信息
思路:可以说一下 jps,top,jstack 这几个命令,再配合一次排查线上问题进行解答,示例如下。
- 输入 jps,获得进程号。
- top -Hp pid 获取本进程中所有线程的 CPU 耗时性能。
- jstack pid 命令查看当前 java 进程的堆栈状态,或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个 txt 文件。
可以使用 fastthread 堆栈定位,fastthread.io/。
高并发 JVM 调优
参考。
汇总:
- 堆不进行自动扩展。如堆初始值:-Xms3000m,堆最大值:-Xmx3000m。
- 参数调优,在成本、吞吐量、延迟之间寻找平衡。如使用 ParNew+CMS 进行垃圾回收。
- 足够大的年轻代,增加系统吞吐,不会增加 GC 负担。
- 容量足够 Survivor 区,不会是对象通过分配担保进入年老代,减少对象晋升,使对象尽可能在年轻代进行 Minor GC,进而减少 Major GC。
- 元空间引起 Full GC 的过程,高并发格外突出,尤其适用大量动态类应用,通过调大初始值,解决该问题。
JVM GC 响应优先与吞吐优先的区别是什么?
比如原来一个程序,10 秒收集一次,停顿时间 100ms,通过调整参数,变为 5 秒收集一次,停顿时间 70ms。很明显吞吐量下来了,但响应速度上去了。 具体 JVM 猜应该是以老年代是并发还是并行为主的配置(待验证),可参考理解。
-XX:+UseParallelGC 与 -XX:+UseParNewGC 区别
附录一、参考文献
- 深入理解Java虚拟机JVM高级特性与最佳实践(第3版).pdf
- 其他
附录二、典型问题记录
- IDEA 插件
- BinEd 文件二进制编码查看
- jclasslib bytecode viewer