Table of Contents
虚拟机内存结构详解
程序计数器
虚拟机栈 JVM stack
本地方法栈 native method stack
常量池
堆 heap
方法区 method area
为什么要放弃永久代permnent generation?
局部变量和类变量内存布局
String对象存储?
对象的内存布局?虚拟机栈中的引用如何和堆中的对象产生关联的?
句柄方式:
直接指针方式:
垃圾收集
对象需要进行垃圾收集吗?
可以视为GC root的对象都有哪几种?
垃圾收集算法分析
问题:eden survivor区的比例,为什么是这个比例,eden survivor的工作过程?
是否可以GC直接内存?
HotSpot的算法实现
安全点与安全区域
常用的JVM调优参数
dump文件分析
java有没有主动触发GC的方式?
java内存模型与线程, happens before原则,内存模型与前面的内存区域的区别?
主内存与工作内存
与java内存区域的区别
happens-before原则
jvm的优化
虚拟机类加载机制及类加载执行子系统
在jdk1.8之前的虚拟机管理的内存如图
在jdk1.8之中,发生了一些变化,虚拟机栈和本地方法栈合二为一了,但是可以想到的是他们在内部的逻辑必然仍然是按照虚拟机栈和本地方法栈划分。jdk1.8的虚拟机管理的内存如图:
我们主要关注的是运行时数据区.而运行时数据区当中白色的几块为线程私有的,灰色的堆以及方法区是线程共享的数据区,因此GC针对的区域也是这两个区域:即堆和方法区。其中方法区的垃圾收集行为是可配的,也比较少出现。而GC并不针对其他区域的原因是程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭,因此不会存在需要垃圾收集的情况。
(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中
(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。
一块特殊的内存区域,存放常量,如基本类型的包装类(Integer、Short)和String。在jdk1.7以及以前,常量池存放在方法区(永久代),注意常量池位于堆中。
在jvm规范中,每个类型都有自己的常量池。常量池是某类型所用常量的一个有序集合,包括直接常量(基本类型,String)和对其他类型、字段、方法的符号引用。之所以是符号引用而不是像c语言那样,编译时直接指定其他类型,是因为java是动态绑定的,只有在运行时根据某些规则才能确定具体依赖的类型实例,这正是java实现多态的基础。
在JVM中,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。而解析阶段即是虚拟机将常量池内的符号引用替换为直接引用的过程。
Person p = new Persoon("小明",18);
p 是指针,存放在栈中。
new Persoon("小明",18) 是对象 ,存放在堆中。
Person 类的相关信息存放在方法区。
引申 :对象的实例保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。
在java8中JVM的PermGen空间被移除:取代它的是Metaspace。metaspace的存放内存区域不再是虚拟机的内存而是本地内存。这大大的减少了这个区域出现OOM的情况。
JDK1.6 | Java 类信息、常量池、静态变量都存储在 Perm(永久代)里。类的元数据和静态变量在类加载的时候分配到 Perm,当类被卸载的时候垃圾收集器从 Perm 处理掉类的元数据和静态变量。当然常量池的东西也会在 Perm 垃圾收集的时候进行处理。 |
JDk1.7 | 常量池移到了堆中,符号引用转移到了native heap,类的静态变量转移到了java heap |
JDK1.8 | JDK 1.8 的对 JVM 架构的改造将类元数据放到本地内存中(即metaspace),另外,将常量池和静态变量放到 Java 堆里。HotSopt VM 将会为类的元数据明确分配和释放本地内存。 这样类的元素信息就可以突破 -XX:MaxPermSize 的限制,可以使用更多的本地内存。解决了原来在运行时生成大量类的造成经常 Full GC 问题,如运行时使用反射、代理等。 |
从发版历史来说,从jdk1.7开始,jvm开始逐步淘汰PermGen,在jdk1.7当中转移了一部分存储的变量,在jdk1.8当中完全移除。
如下图所示:
我们可以在JVM启动的时候通过-XX:MaxMetaspaceSize 指定Metaspace的大小。如果不指定的话,则metaspace空间的大小会在本地内存当中自动增大。若指定了大小,则Metaspace的空间超过最大值之后,jvm仍然会抛出OOM错误。
方法区:一个概念上的定义。
永久代:方法去这个概念的一个具体实现。
在jdk1.8当中虚拟机的永久代permnent generation被替换为了metaspace,元数据区,其中的一些理由如下:
在Hotspot虚拟机当中,对象在内存中存储的布局分为三块区域:对象头header,实例数据Instance Data,对齐填充 Padding。如下图:
对象头
Mark Word: HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
类型指针:指向对象的类元素的指针。JVM通过这个指针确定这个对象是哪个类的实例。
若对象是数组,对象头还需要记录数组长度。
实例数据则是各种类型的字段:包括子类定义的,和从父类继承的
而对象的访问定位。两种方式主要有:
引用中存储的是对象的句柄地址,而这些句柄存储在堆中的句柄池中,而池中的句柄存储了对象实例数据地址和对象类型数据的地址,通过对象实例数据地址可以在堆中找到对象实例数据,通过对象类型数据地址可以在方法区中找到类型数据. 例子如下:
优点:句柄方式最大的优点是当对象被移动的时候,只会改变句柄中对象实例数据地址,而引用(reference)本身中句柄地址不变。
引用中存储的是对象地址,通过对象地址在堆中可以找到对象实例数据,而对象实例数据中存储了类型数据的指针,通过这个指针可以到方法区中找到类型数据,即是哪个类的对象。例子如下:
优点: 减少了一次指针定位的时间开销,速度更快,HotSpot虚拟机正使用了这种方式。
一:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因
在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。
(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中
(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。
二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量
(1)当声明的是基本类型的变量其变量名及其值放在堆内存中的
(2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
String是一个特殊的包装类数据,可以用:
String str = new String("abc");
String str = "abc";
两种形式来创建,第一种是用new来创建对象的,它会在堆中按照new对象的流程创建一个新的对象出来,每调用一次就会创建一个新的对象;而第二种是先在栈中创建一个对String类的对象引用str ,然后查找常量池中有没有存放"abc",如果没有,则将"abc"存放常量池,并令str 指向"abc",如果已经有"abc",则直接令str指向"abc"。
关于这部分的详细内容可以参考:[Java] String类解析。
关于Java垃圾收集机制,需要重点把握两个问题
下面我们就围绕这两个问题进行展开
引用计数法:给对象中添加一个引用计数器,有一个地方引用它,计数器加一,引用失效就减1,计算器为0的对象就是不再被使用的。
存在的问题:两个或多个对象之间可能会互相引用对方,导致这样的对象无法被回收。
可达性算法:通过一系列的称为"GC Roots"的对象作为起点,从这些起点开始向下搜索,搜索所走过的路径称为Reference Chain引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的。这种方式显然解决了引用计数法的问题。
关于gc root的用法,下面这个图很好的进行了说明:
除了本地方法栈的内存图如下:
关于本地方法栈,它和虚拟机栈存在着下面这样一种调用关系:
java虚拟机内部线程执行的情况,可能一直在执行java方法,操作java栈;也可能在java栈和本地方法栈中来回切换。
GC主要是针对java堆的操作,那么GC算法自然也就只和java堆的对象有关。GC算法的前提是前面提到的GC roots可达性算法,通过可达性算法确定哪些对象能被回收是GC算法的前提。GC算法准确的说一共三种
1.标记清除(基础算法)
但是有两个问题
2.复制算法(现代商业虚拟机用来回收新生代)
为解决效率问题。
思想:将可用内存一分为二,每次只使用其中一块,当A用完就把A所有存活对象赋值到B上,然后清空A。但是研究表明绝大多数对象都是朝生夕灭,因此A:B=1:1不合理,采取的是A:B=8:1:1.即前面提到的Eden和survivor方式。
3.标记整理(针对老年代)
根据老年代对象存活时间长的特点。
4.分代收集。分代收集只是针对新生代和老年代采取不同的算法,但还是上面的这三种
现代虚拟机都采用的分代收集。并没有新的思想,只是对新生代采用复制算法,对老年代采用标记整理或者标记清除算法。
首先hotspot虚拟机对eden survivor是对新生代的内存划分方式,如下图:
eden区域占据8份,两个survivor区域各占1份。大多数的新生代都是采用的复制清除法作为垃圾回收算法。当对新生代进行minor gc时,会把Eden中和Survivor from中的存活对象复制到另一块survivor to的区域中。
因为新生代中98%的对象都是"朝生夕死"的,只有很少会存活下来,因此就设定了10%的空间来存放活下来的。但是万一还是出现了不止10%的对象存活下来呢?岂不是放不下了?虚拟机考虑到了这个情况并有一个分配担保机制:在这种情况下让它们直接进入老年代即可。从新生代晋升到老年代有以下三种方式:
那么基于比较常见的第一种方式,为什么我们需要两块survivor区域呢,如果不划分两块survivor区域会有什么问题吗?如果只有一个Survivor区,GC时Eden区的可以进入Survivor区,那Survivor区的去到哪里呢?虽然在每一次gc时可能会有对象存活下来,但是这些存活下来的对象并不一定会晋升,这意味着每次gc时可能survivor区中都有一些已经在里面的对象,如果只有一个survivor区的话,对于这个区域的处理就会非常麻烦。而如果有两个survivor区的话,则只需将eden区中的存活对象,和一个survivor区中的存活的像,都拷贝到另一个survivor区中,然后再直接清除eden区和survivor from区即可。
这里很有意思的一点是,survivor from和 survivor to之间应是不停互换角色的。
首先需要明确的是,直接内存是指什么。
直接内存,实际上就是系统物理内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
直接内存的分配不受java堆大小限制,但是必然会受到本机内存大小等的限制。
因此不可GC直接内存?问题 虚拟机gc native stack的时候会回收直接内存吗?
上面介绍的内容是垃圾回收算法的理论知识。那么作为商业虚拟机的HotSpot是怎么实现垃圾回收的呢?
在实际的项目当中,会面临这样一些问题:方法区的内容可能会非常多,引用数量庞大。且每时每刻,一个运行着的java程序的引用情况都是在发生变化的,因此在进行可达性分析的时候,对象引用的关系在某个时间段上,必须是保持不变的,这一点是导致进行GC进行时必须停顿所有java执行线程的一个重要原因——Sun将这件事称为"Stop the world".
Hotspot虚拟机中使用一组称为OopMap的数据结构来维护哪些地方存放着对象引用。
在OopMap的协助下,Hotspot虚拟机可以快速且准确地完成GC Roots枚举,但是选择恰当的时机进行GC仍然非常重要,这些特定的GC位置被称为安全点SafePoint。
Safepoint的特点是:是否具有让程序长时间执行的特点。比较典型的如方法调用,循环跳转等都是safepoint的选择。
除了安全点之外还有安全区域。所谓安全区域就是指线程进入到某个区域时就可以被JVM的GC直接忽略。典型的应用场景是线程处于Sleep或者Blocked状态的场景。
GC 命令行选项 | 描述 |
-Xms | 设置Java堆大小的初始值/最小值。例如:-Xms512m (请注意这里没有”=”). |
-Xmx | 设置Java堆大小的最大值 |
-Xmn | 设置年轻代对空间的初始值,最小值和最大值。请注意,年老代堆空间大小是依赖于年轻代堆空间大小的 |
-XX:PermSize= |
设置持久代堆空间的初始值和最小值 |
-XX:MaxPermSize= |
设置持久代堆空间的最大值 |
老年代什么情况下会发生gc?
除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 老年代空间不足。
老年代代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2. 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。
3. 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)来避免。
4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 当新生代采用PSGC时,方式稍有不同,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。
场景分析:一台4核8g的服务器,每隔两小时就要出现一次老年代gc,现在有日志,怎么分析是哪里出了问题
我们可以在代码里调用:
System.gc();
Runtime.getRuntime().gc();
java.lang.management.MemoryMXBean.gc()
但这些方法的作用只是告诉JVM尽快GC一次,不会立即执行GC。虚拟机的规范是通知虚拟机尽快执行,没有强制规定执行时间,因此按照这个规范,答案是没有。
happens-before是JMM的核心。jmm即java memory model也就是java内存模型。java内存模型的用意是来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致饿内存访问效果。在jdk1.5之后成熟和完善了起来。
线程,主内存,工作内存三者的交互关系如图:
这里所说的主内存,工作内存与之前的java内存区域中的java堆,栈,方法区等并不是同一个层次的划分,两者基本上没有关系。如果一定要对应起来,则从定义上来看,主内存主要对应于java堆上的对象实例数据部分(对象除了实例数据部分还有对象头的信息),工作内存则对应与虚拟机栈中的部分区域。
也叫先行发生原则,我们在写java并发代码时并没有感觉。下面是happens-before原则规则:
设置参数,设置jvm的最大内存数
垃圾回收器的选择
关于这一部分的内容,笔者单独写了一篇博客[JVM]虚拟机类加载机制