JVM原理和机制 GC调优

本文详细介绍了JVM的工作原理,包括其跨平台特性、数据类型、堆栈内存分配以及方法区。重点讲解了JVM的垃圾收集机制,包括不同GC算法如标记清除、复制、标记-压缩和分代收集,以及新生代、老年代和永久代的概念。文章还探讨了GC参数设置、监控和调优,强调了合理设置JVM参数和优化代码以减少Full GC的重要性。
摘要由CSDN通过智能技术生成

前言
Java的JVM,可能学Java的都知道这个名字,博客或者百度也有一大堆,因为面试原因,大致也能说得上一些东西,今天重新梳理一下,一方面复习,一方面加深理解

JVM的原理
JVM可以理解成一个虚构出来的计算机,一个特点是跨平台型,将源码编译成目标代码,这个目标代码就是字节码(也就是Java里面的*.class文件),在任何平台上,windows、linux、类unix、tru64等机器上,只要安装对应的jdk,即可实现在一个平台上编译,不需要其他的修改。

数据类型
Java的类型分为两大类:基础数据类型、非基础数据类型

基础数据类型: 
byte: 1字节 
short: 2字节 
int: 4字节 
long: 8字节 
float: 4字节单精度浮点数 
double: 8字节双精度浮点数 
char: 2字节无符号Unicode字符

非基础数据类型(其他数据类型): 
object:对象,所有的自己编写的类都是对象:占用4个字节,在Java虚拟机中的堆中存在(后面会深入讲到)

编译及运行
通过调用jre来进行将Java源码编译成*.class文件

JVMå è½½æºå¶

堆和栈
堆:是一个运行时数据区,类的实例(对象)和其他数组从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。 
(主要用于存放对象,存取速度慢,可以运行时动态分配内存,生存期不需要提前确定)

栈:一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。

(主要用来执行程序,存取速度快,大小和生存期必须确定,缺乏灵活性)

堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!

总结:

堆区: 
存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区: 
每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
方法区: 
又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
GC
1、GC不会立即收集这个我们都知道 
2、GC主要是回收堆和方法区的内存

堆内存的存活
通过计数器:每一个对象有一个计数器,新增一个对象+1,释放之后-1,计数为0时候可以回收。这个方式存在一个问题:假如B引用A,但是B=null;这个时候B是没办法回收的
可达性算法:GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
垃圾收集算法
标记清除算法 
先标记,再清除 
它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法 
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低
标记-压缩算法 
在对象存活率较高时就要执行较多的复制操作,效率将会变低。 
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法 
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
问题:

1.什么是新生代(Young),老年代(Old),永久代(Perm)? 
简单的说: 
GC会分区域回收垃圾,为了方便对内存进行管理,分为新生代、老年代和永久代 
新生代就是:频繁收集的区域就是新生代 
老年代:收集频率较少的区域 
永久带:基本上不回收的区域成为永久代 
堆=新生代+老年代

2.新生代、老年代、永久代各保存哪些信息?jvm是怎么划分 
新生代:新生代划分三个区域,Eden、SurvivorA和SurvivorB,当对象第一次创建的时候,都是存放在新生代中的Eden中,当Eden没有空间的时候,会执行一次GC,如果存在某些对象不能引用,会把这些对象复制到SurvivorA或者SurvivorB中的任意一个。但是肯定有一个Survivor是空的,当Survivor里面的对象超过一定回收次数的阀值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)),这部分对象会被转移到老年代

备注: 
1)可以根据程序需要,增加设置多个Survivor区域,减少对象被放入到老年代 
2)GC回收的时候程序会挂起,也就是这个时候程序会很”卡”,jvm调优应该尽量降低GC的时间,减少FULL GC的次数

老年代:当新生代不能回收的对象,就保存在老年代中,在老年代回收的频率会比较低 
永久代:存在静态文件,静态的东西,几乎不会被GC

3.Minor GC、Major GC、Full GC到底有什么区别? 
Minor GC:就是对堆中的新生代进行一次GC 
Major GC:对堆中老年代进行一次GC,速度一般比Minor GC慢10倍 
Full GC:全部回收,对堆中的新生代和老年代都进行GC,并且也会栈执行GC,比Major GC更慢

4.Full GC是对堆全部GC,那么会挂起时间比较长,Minor GC(新生代空间满)和Major GC(一定的年龄阀值)都有一定的策略,那么什么情况下会进行Full GC呢?1)程序中显示的调用System.gc()。当程序员在程序中调用System.gc()的时候,会产生full gc,应该禁止显示调用System.gc(),让虚拟机自己去管理它的内存,如果程序中有RMI接口或者,应该禁止远程RMI调用GC,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc 
2)老年代空间不足 
当老年代空间不足的时候,会产生Full GC。但是当GC后仍然空间不足的话,会产生 
Java.lang.OutOfMemoryError: Java heap space 
所以在jvm调优中应该尽量让对象在Major GC中被回收,以及不要创建过大的数组和对象

3)永久代空间不足 
当永久代空间不足,并且没有配置CMS GC的时候也会执行Full GC,当Full GC回收后,仍然不够空间,会产生 
java.lang.OutOfMemoryError: PermGen space

JVM参数: 
堆设置 
-Xms :初始堆大小 
-Xmx :最大堆大小 
-XX:NewSize=n :设置年轻代大小,设为整个堆大小的1/3或者1/4,和XX:MaxNewSize两个值设为一样大。 
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 
-XX:MaxPermSize=n :设置持久代大小 
收集器设置 
-XX:+UseSerialGC :设置串行收集器 
-XX:+UseParallelGC :设置并行收集器 
-XX:+UseParalledlOldGC :设置并行年老代收集器 
-XX:+UseConcMarkSweepGC :设置并发收集器 
垃圾回收统计信息 
-XX:+PrintHeapAtGC GC的heap详情 
-XX:+PrintGCDetails GC详情 
-XX:+PrintGCTimeStamps 打印GC时间信息 
-XX:+PrintTenuringDistribution 打印年龄信息等 
-XX:+HandlePromotionFailure 老年代分配担保(true or false) 
并行收集器设置 
-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。 
-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间 
-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 
并发收集器设置 
-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。 
-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

JVM的任何一个参数都不是越大越好,也都不是越小越好,通过设置jvm的参数,我们要达到三个目的: 
1)GC的时间足够的小 
2)GC的次数足够的少 
3)发生Full GC的周期足够的长

1和2是想反的,我们设想,要想GC时间少,那么堆肯定要设置小一点,但是要保证GC时间足够的少,我们又要保证堆空间大一点,所以,我们必须根据实际情况平衡

(1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值 
(2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小

更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间
(3)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集 
(4)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

整理: 
1、多数的Java应用不需要在服务器上进行GC优化;

2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;

3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);

4、减少创建对象的数量;

5、减少使用全局变量和大对象;

6、GC优化是到最后不得已才采用的手段;

7、在实际使用中,分析GC情况优化代码比优化GC参数要多得多;

JVM监控
JDK自带工具jstat使用
1)先确定tomcat或者jetty等运用服务器的pid 
windows:使用netstat -ano |findstr “8080”(假如服务器端口是8080) 

 

 

linux:linux使用ps aux|grep “tomcat”命令(根据实际情况修改)

2)上面我在windows上面已经确定了pid是6396,那么在命令行输入: 
jstat -gcutil 6396 1000 5表示监控6396端口,每1000毫秒执行一次打印,总共打印五次

Options — 选项,我们一般使用 -gcutil 查看gc情况 
vmid — VM的进程号,即当前运行的java进程号 
interval– 间隔时间,单位为秒或者毫秒 
count — 打印次数,如果缺省则打印无数次è¿éåå¾çæè¿°

 

 

如图: 
S0 — Heap上的 Survivor space 0 区已使用空间的百分比 
S1 — Heap上的 Survivor space 1 区已使用空间的百分比 
E — Heap上的 Eden space 区已使用空间的百分比 
O — Heap上的 Old space 区已使用空间的百分比 
P — Perm space 区已使用空间的百分比 
YGC — 从应用程序启动到采样时发生 Young GC 的次数 
YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒) 
FGC — 从应用程序启动到采样时发生 Full GC 的次数 
FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒) 
GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)

我们可以对每个参数进行分析

JDK自带工具jconsole(windows)
在windows目录有C:\Program Files\Java\jdk1.7.0_55\bin\jconsole.exe

双击jconsole.exe,可以看到 

è¿éåå¾çæè¿°

 

è¿éåå¾çæè¿°

 

 

è¿éåå¾çæè¿°

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值