JVM的定义
JVM(Java Virtual Machine,Java虚拟机)。
JVM是JAVA实现跨平台的主要手段,事实上jvm是有较多的实现版本,各种系统下的JVM不同。
由于jvm是处在Java语言和操作系统之间的,所以它要向上提供对Java的支持,向下与操作系统良好交互。
JVM中的数据结构
主要分为基本类型和引用类型。
基本类型
- 基本类型
- 引用类型
- 类类型
- 数组类型
- component类型
(数组去掉最外层维度后剩下的类型,可能还是一个数组类型(对于多维数组)) - element类型
- component类型
- 接口类型
JVM中的内存结构
运行时公有数据区(线程共享)
- 堆(GC发生的主要区域)
对象实例和数组- 新生代 1/3
- Eden 区 80%
- From Survivor 区 10%
- To Survivor 区 10%
- 老年代 2/3
- 新生代 1/3
- 方法区(永久代,Java 8后就把方法区的实现移到了本地内存中的元空间中)
用于存储被 JVM 加载的常量、静态变量、类信息、即时编译器编译后的代码等数据。- 运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
- 运行时常量池
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
- 堆(GC发生的主要区域)
运行时私有数据区(线程私有)
pc寄存器(程序计数器)
(计数器记录的是虚拟机字节码指令的地址(当前指令的地址 (可以理解为代码的行号,线程被挂起了,再恢复回来执行时需要定位原来执行到的地方)是由字节码执行引擎更新)。如果是Native方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域)jvm栈(虚拟机栈)
- 栈帧( Frame)
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)
用于存储局部变量表、操作数栈、动态链接(方法区的地址,找到方法区存的代码)、方法出口(原调用方法的调用时的信息)等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,即局部变量是对象时为对象在堆中的内存地址)和 returnAddress 类型(指向了一条字节码指令的地址)StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
- 栈帧( Frame)
native方法栈(本地方法栈)
调用非java方法时使用
直接内存
- 元空间(java8以后将永久代移动到了元空间中)
- 直接内存
非虚拟机运行时数据区的部分
HotSpot 虚拟机对象
在 HotSpot 虚拟机中,分为 3 块区域:
对象头(Header)
实例数据(Instance Data)
对齐填充(Padding)
对象头(Header)
- 第一部分 (Mark Word)
用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。 - 第二部分 类型指针
即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。实例数据(Instance Data)
程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。对齐填充(Padding)
不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。
JVM垃圾回收
主要在堆中进行,java8之前还有永久代(也就是方法区),java8后方法区移到了直接内存中的元空间中去了,所以只有堆进行GC,对象实例和数组都是在堆上分配的,GC 也主要对这两类数据进行回收。
引用计数法
最容易想到的一种方式是引用计数法,啥叫引用计数法,简单地说,就是对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收,但此方法无法解决循环引用问题,循环引用后,即使没有真正的引用了,但其引用无法归0。所以现代虚拟机都不用引用计数法来判断对象是否应该被回收。
可达性算法
现代虚拟机基本都是采用这种算法来判断对象是否存活,可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。。。(这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收。
fullGC时虚拟机会STW(stop the world),好用来判断GC Root,(如果客户线程不停的话可能之前标记为非垃圾的对象,后面由于用户线程继续执行,栈帧后面被销毁,按可达性算法,又变为了垃圾)。
GC Roots
哪些对象可以作为 GC Root 呢,有以下几类
- 虚拟机栈(栈帧中的局部变量表) 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
虚拟机栈中引用的对象
如下代码所示,a 是栈帧中的本地变量,当 a = null 时,由于此时 a 充当了 GC Root 的作用,a 与原来指向的实例 new Test() 断开了连接,所以对象会被回收。1
2
3
4
5
6publicclass Test {
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}方法区中类静态属性引用的对象
如下代码所示,当栈帧中的本地变量 a = null 时,由于 a 原来指向的对象与 GC Root (变量 a) 断开了连接,所以 a 原来指向的对象会被回收,而由于我们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!1
2
3
4
5
6
7
8public class Test {
public static Test s;
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}方法区中常量引用的对象
如下代码所示,常量 s 指向的对象并不会因为 a 指向的对象被回收而回收1
2
3
4
5
6
7public class Test {
public static final Test s = new Test();
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}本地方法栈中 JNI 引用的对象
所谓本地方法就是一个 java 调用非 java 代码的接口,该方法并非 Java 实现的,可能由 C 或 Python等其他语言实现的, Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法。
当调用 Java 方法时,虚拟机会创建一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法。
对象可回收,就一定会被回收吗?
并不是,对象的 finalize 方法给了对象一次垂死挣扎的机会,当对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
注意: finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!
垃圾回收主要方法
Full GC就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;
- 标记清除算法
顾名思义,先标记可回收的对象,进行清除。
缺点:剩余的内存不连续 - 复制算法
将内存分为两块,一块空的,一块是已用的,在已用的里面标记出存活的对象,再复制到另一块中,再把原来的那块都清了。
缺点:可用内存减半;复制的效率低 - 标记整理法
前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列(如图示),再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。
缺点:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。
分代收集算法
根据大多数java对象的存活特征,所以分代收集算法根据对象存活周期的不同将堆分成==新生代和老生代==(Java8以前还有个永久代),默认比例为 ==1 : 2==,新生代又分为Eden 区, from Survivor 区(简称S0),to Survivor 区(简称 S1),三者的比例为 8: 1: 1 ,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)*(不严谨 但可这么理解)。
- 当 Eden 区将满时,触发 Minor GC。采用复制删除算法。Eden标记出来存活对象 -> 复制到S0中
- 第二次:Eden 和 S0(或S1)存活对象一起移到 S1(Eden 和 S0 的存活对象年龄+1), 同时清空 Eden 和 S0 的空间。(后续S0和S1的角色会以此互换以重复上述过程)
具体过程:link
FULL GC采用标记整理算法
升级到老年代
- 当对象的年龄达到了我们设定的阈值,则会从S0(或S1)晋升到老年代
- 大对象 当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代,因为如果把大对象分配在 Eden 区, Minor GC 后再移动到 S0,S1 会有很大的开销(对象比较大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,所以干脆就直接移到老年代.
- 还有一种情况也会让对象晋升到老年代,即在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。
空间分配担保
在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。
JVM调优
GC分析
jstack,jmap,jinfo,
1.检查jvm配置
- 通过以下命令查看JVM的启动参数:
ps aux | grep "applicationName=adsearch"
2.观察老年代的内存变化
- 通过观察老年代的使用情况,可以看到:每次FGC后,内存是否有效回收了,否则考虑内存泄漏等原因。
3.通过jmap命令查看堆内存中的对象
jmap -histo 7276 | head -n20
4. 进一步dump堆内存文件进行分析
通过以下命令dump堆内存:JVisualVM工具导入dump出来的堆内存文件,同样可以看到各个对象所占空间1
2jmap -dump:format=b,file=heap 7276
jmap -heap pid
在调试程序时,有时需要查看程序加载的类、内存回收情况、调用的本地接口等。这时候就需要-verbose命令。在myeclipse可以通过右键设置(如下),也可以在命令行输入java -verbose来查看。
1 | -verbose:class 查看类加载情况 |
阿里jvm工具
JVM调优工具_arthas(阿尔萨斯)。
直接java -jar即可使用
jvm参数详解
-Xms:设置Java程序启动时初始化堆大小
-Xmx:设置Java程序能获得最大的堆大小
-Xmn:可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响,新生代大小一般会设置整个堆空间的1/3到1/4左右。
-Xss:栈内存大小,设置单个线程栈大小,一般默认512~1024kb。单个线程栈大小跟操作系统和JDK版本都有关系-Xss = -XX:ThreadStackSize
-XX:SurvivorRatio:用来设置新生代中eden空间和from/to空间的比例。含义:-XX:SurvivorRatio=eden/from=eden/to。
不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略:尽可能将对象预留在新生代,减少老年代的GC次数。
-XX:MetaspaceSize 初始元数据空间大小
-XX:MaxMetaspaceSize=128m
其他
可以通过
1 | javap -c ***.class |
命令进行代码反汇编
类