一、什么是jvm
二、jvm模型
1.一级结构和流程
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)来实现整个程序的功能。
2.二级内存模型(内存结构)
1.JVM的内存模型:
(1)虚拟机栈(线程私有)
每个方法在执行的时候也会创建一个栈帧,存储了局部变量,操作数,动态链接,方法返回地址 每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
通常所说的栈,一般是指在虚拟机栈中的局部变量部分。
局部变量所需内存在编译期间完成分配,
如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。
如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError
(2)本地技术栈(线程私有)
和虚拟机栈类似,只不过虚拟机栈是服务 Java 方法的,本地栈主要为虚拟机使用到的Native方法服务。也会抛出StackOverflowError 和OutOfMemoryError
(3)程序计数器(线程私有)
当前线程所执行的字节码的行号指示器,每条线程都有一个独立的程序计数器,这类内存也称为“线程私有”的内存。字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是Natice方法,则为空。
(4)堆(线程共享)
被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实例。
当队中没有内存可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
(5)方法区(线程共享)
被所有方法线程共享的一块内存区域。
用于存储已经被虚拟机加载的类信息,常量,静态变量**等。
这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。
**_方法区在1.7和1.8有了改变:_**在1.7中方法区,习惯叫永久代,但是两者不等价。 在1.8中**元空间**取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:**元数据空间并不在虚拟机中,而是使用本地内存**
3.内存模型扩展问题
1.为什么程序计数器为线程私有呢?
于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,
在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,
因此,为了线程切换后能恢复到正常的执行位置,每条线程都需要一个独立的程序计数器,
各线程之间计数器互不影响,独立存储,为线程私有的内存
目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;
新生代又被划分成 Eden 空间、 From Survivor 和 To Survivor 三块区域
2.深拷贝和浅拷贝
浅拷贝:(shallowCopy)只是增加了一个指针指向已存在的内存地址,
**深拷贝:**(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
**浅复制**:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
**深复制:**在计算机中开辟一块新的内存地址用于存放复制的对象。
3.堆栈的区别?
(1)物理地址:
**堆**的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
**栈**使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性性快。
(2)内存分别:
**堆**因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
**栈**是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
(3)存放的内容
**堆**存放的是对象的实例和数组。因此该区更关注的是数据的存储
**栈**存放的是局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
(4)程序的可见度:
**堆**对于整个应用程序都是共享、可见的。
**栈**只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
(5)补充:静态变量放在方法区,静态的对象还是放在堆。
4.Native 方法是什么?
JDK 中有很多方法是使用 Native 修饰的。Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)。个人理解Native 方法是与操作系统直接交互的。比如通知垃圾收集器进行垃圾回收的代码 System.gc(),就是使用 native 修饰的。
5.栈中存储的是什么?
**栈帧**是栈的元素。**每个方法在执行时都会创建一个栈帧**。栈帧中存储了**局部变量表、操作数栈、动态连接和方法出口**等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
4.java内存模型
Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。
Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。
三、jvm之核心GC
垃圾回收器:回收垃圾的一种执行者
垃圾回收算法:回收垃圾过程的一种理论概念
1.1如何定位垃圾
1、引用计数法 reference ccunt
有一个引用指向这个对象的时候就会累加1,如上图,当3变成0时,没有引用指向这个对象,这个对象就变成了一个垃圾,就会被垃圾回收器通过不同的垃圾回收算法回收。java里面一般不采用这种算法。
2.根可达算法|根搜索算法,Roots searching,也叫可达性分析析法
Gc roots根对象
通过这个Gc roots根对象,向下搜索,当这条线上搜索不到的时候为止,其他未被搜索到的都是垃圾,如果有两个连在一起,但是不再这条根上,就是一对垃圾。
**什么是根??**:1.jvm stack:main方法栈帧 2.native method stack 本地方法栈
3.runtime constant pool 运行常量池
4.stack references in method area方法区内的静态引用
5.Clazz
1.垃圾回收算法
就三种:Mark—sweep 标记清除算法
copying 拷贝 |复制算法
Mark--Compact 标记压缩算法,这三种排列组合,也叫标记整理
Mark—sweep 标记清除算法:
优点:就是标记可回收的对象垃圾,然后清除,很简单,操作效率高
缺点:磁盘空间碎片化,很容易没有连续的空间,分配大对象的时候就放不下了
Copying:复制算法:
优点:效率非常高,没有碎片。新生代中的算法,mino gc
缺点:占用内存,浪费空间。一直有一小部分的空间没有用
Mark—Compact 标记压缩算法
优点:清除的干净,空间连续性高相对标记清除来说
缺点:效率低,清理的时间长,标记清除以后多了一步整理到连续的空间一侧
2 十大垃圾回收器
jdk1到现在,共有这十大垃圾回收器,左边这6中是分代模型的垃圾回收器,右边这三种至少在物理上不再分代了
G1在逻辑上依旧分代,还有一个特殊的Epslion
这里面有个东西叫分代,这个分代不是算法,是一种管理模型,逻辑和物理上都把内存分为两部分,也就是老年代和新生代。
每一个jdk版本,都有自己对应的垃圾回收器,1dk1.8默认的是parallel scavenge加parallelold简称ps和po
怎么查看默认的垃圾回收器是哪种?
java参数有三种:-开头,标准,所有的hotspot版本都支持
-x开头,非标准特定版本支持的命令
-xx开头,不稳定,下个版本可能消失,java -xx;+printflagsFinal,在cmd中打印所有的非标准参数 大概有728行参数,但是常调的不到100种左右,经常调的有2/30种场景
调优,针对的是哪种版本的哪种垃圾回收器
下面红框中的是经常配合的垃圾回收器
3分代模型之新生代老年代
survivor from,survivor to或者叫survivor0 ,survivor1
**新生代对象特点**,大概只有5-10%的对象能存活,90-95%的对象都会被清理,因为标记90-95%的垃圾时间太长,所以用复制算法。只需要给他一小块内存放这些存活对象,这个就是from区,当第二次清理edan时,把edan和from区的都放到to区,同时给from到to区的对象标记加1,然后此时逻辑上,to就变成第一次的from区(时刻有对象)了,from就变成to区(短暂性可能没有对象),这个效率特别高。当from区中标记等于15时会复制到老年代中,**老年代中一般都是大对象和老不死对象**<br />这个标记值和垃圾回收器相关,这个默认值参数叫 --xx:MaxTenuringThreshold 进入老年代的最大阈值<br /> 不能0个,0个就是没有suvivor区,那edan区复制后就会直接进入老年代,频繁触发老年代的full gc ,这样会更慢,<br /> 不能1个,1个的话,edan复制到这一块,这一块很快就会满,频繁触触mingr GC,而且mingr时间会变长<br />**补充:**<br /><br /><br />小对象而且没有逃逸,是可以分配到栈上的,栈上分配的对象,不需要gc介入,效率那是很高的,栈帧下移直接弹出这个垃圾对象。<br />什么是**逃逸**,就是在栈中,有很多栈帧,当有其他引用指向这个栈帧不能随便逃逸,就叫逃逸<br />无法进入栈:如果很大就进入老年代,不是很大就进入tlab:thread local alication buffer,线程本地缓存区,其实这个就是防止对象进入edan区时,对象争抢内存,这个tlab就是给这个对象一块独享的很小的内存区,然后进入edan,避免争抢过程<br />Age就是默认值15
4.垃圾回收器的组合使用
第一种:serial 和old serial
serial串行
a-stop-the-word,copying collector which uses a single Gc thread
a-stop-the-word:stw 就是所有线程停止,等着垃圾回收器清理完垃圾后再运行
copying collector which uses a single Gc thread:单线程的gc复制算法—-年轻代
mark -sweep -compact collector that uses a single Gc thread:单线程的标记压缩算法—-老年代
这种现在已经不用了,效率太低,jdk1.0-1.2,那时候处理的内存小,随着内存越来越大,内存变大,swt的过程中,清理垃圾的时间会变的很长,工作线程业务线程是暂停的,用户得不到任何反馈,表现出来就是服务器卡死。时间最长的3天,常见的几十秒,几分钟,几天就很牛逼了。12306,每天晚上不能买票
jvm调优之一:重启,容易面试的时候挨打!!慎重,一个大嘴巴子上来,出门左滚!!!
为了解决serial不能解决内存逐渐变大的问题,出现了parallel scavenge和parallel old组合
第二种:parallel scavenge和parallel old
jdk1.8默认的组合方式,9.10.11.12.13都是g1,但是11开始可以使用ZGC,12开始可以使用shenandoah
目前为止,任何垃圾回收器都不能避免swt这个过程
copying collector which uses multiple Gc thread:多线程的垃圾回收器,会有多个垃圾回收器过来清理
ps po就是多个垃圾回收器来回收这个垃圾,但是随着线程数越来越多的时候,cpu会把资源用在线程切换上,因此,就延伸到了CMS
第三种parnew 和cms
parnew和parallel scavenge差不多,是他的变种,和cms配对更方便,原理和parallel一样,这里重点讲解cms
CMS毛病和bug特别多,有个最大的bug,就是老年代的算法导致老年代碎片化特别严重,这个时候cms会做一件特别极端的行为,就是用于一个单线程去清理老年代所有的垃圾,这个过程太长太长。所以任何一个版本默认的都不是cms,虽然cms从1.4就诞生了,不太容易调优,1.8以上推荐的是G1
但是cms是为新的垃圾回收算法开辟了一种新的思想,CMS之后的g1,zgc都是在cms基础上发展的
**CMS是什么?**<br /><br />concurrent:并发,并发是一边运行线程,一边回收垃圾,垃圾回收线程和工作线程同时进行<br />parallel:并行,并行是多个垃圾回收器同时来回收垃圾,多人同行的意思,但是也是在stw基础上的<br />low-pause collector 暂停时间特别短的一种算法<br /><br />**为什么要有4步**?
1.cms在老年代,算法时标记清除算法,自带的问题就是碎片化,空间不连续,虽然有参数可以调优,但是无法避免。
initial mark 初始标记,只把根对象标记出来,因为main方法产生的对象特别少,引用少所以对象少, 虽然这个过程也是stw,但是标记的时间特别短
concurrent mark 并发标记:一边工作,一边跟踪标记
remark:重新标记:在前两步中,会有在标记过程中,被标记了但是又被引用的对象,就需要重新标记,这个过程是stw的,因为在前两个过程中标记错误的,是很少的一部分,所以这个stw的时间也很短
concurrent并发清理
初始标记
并发标记
重新标记 紫色是标记中重新被引用的
并发清理
难点是标记中,可能会漏标错标
remark阶段的算法:三色标记算法|三色扫描算法黑白灰,三种颜色
zgc是颜色指针算法
分布式锁的续命问题,如果碰上垃圾回收,那么就无法续命,假如3秒续一次,当内存很大的时候,是很容易遇见垃圾回收的,垃圾回收的时间不止3秒,所以,分布式锁续命就会出现问题
四、jvm调优实战
什么是调优?
1.根据需求进行jvm规划和预调优
2.优化运行jvm运行环境
3.解决jvm运行过程中出现的各种问题(解决oom问题)
1、JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。
程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。
这里有几个比较重要的指标:
- 内存占用:程序正常运行需要的内存大小。
- 延迟:由于垃圾收集而引起的程序停顿时间就是stw。
- 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。
当然,和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。
2、JVM调优工具
(1)调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。
①系统运行日志:系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。
②堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,比如根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;根据“java.lang.StackOverflowError”可以判断是栈溢出;根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。
③GC日志:程序启动时用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序运行时把gc的详细过程记录下来,或者直接配置“-verbose:gc”参数把gc日志打印到控制台,通过记录的gc日志可以分析每块内存区域gc的频率、时间等,从而发现问题,进行有针对性的优化。
1.一个实战问题
xmx和xms设置相同的内存200mb如果这块确定就是给Java用的,那么就设置为一样,可以防止内存抖动
年轻代占了54656K,50m,开始回收直到1074k,总共是200M,用时0.0144379秒
起始值,cpu和占用的内存
最后随着垃圾不断回收,cpu和占用的内存,反而增加
这个问题不仅仅会有泄露,还可能会有溢出
如何定位这个问题:可以使用最原始的命令,太不方便,可以使用远程可视化工具观察服务器,但是不合适。
在linux轻轻松松开一个端口,远程可视化界面访问,但是这是不可能的,不允许的,安全性和其他考虑
2. arthas—实际调优工具
阿里开源的在线诊断工具
首先安装启动这个自行搜索,我也不会
最常用的命令dashboard:可以观察到跟踪的cpu的使用
还有一些其他参数
help命令
thread命令:可以看到这个进程里面包括了哪些线程,如下面的39号线程,cpu占用22,太高,time_wait,这是由死锁产生
thread 39:可以看到39号线程里,方法之间的调用,还有堆栈的信息,找到类信息,就可以看代码的问题
查死锁用一个命令就可以:thread —help
线程死锁:thread -b,下面是没有死锁的
3 . 针对2.1实战的解决方案
2.1的问题,cpu和内存不断增加,是因为现在的内存在产生继续侵占现有内存的对象,需要用到arthas唯一不具备的jvm中的功能,**jmap**<br /><br />**jps观察哪个进程有问题**<br />**jmap -histo 1196**,把1196号线程里面对象的一些信息打印出来,这个打印出来的信息量是很大的,比如这个有1013条信息,然后把前面的信息打印出来<br /><br />**jmap -histo 1196 | head -20**,当然,可以直接用这个命令,**这条命令是定位问题的关键**<br /><br /> 再来分析这张图,这个1-17,num是按照对象占用空间大小的逆序排列,第一个63907200bytes,最后一个306440bytes。第一个:887600个ScheduledFutureTask对象,一般在 前几行就能查出来是哪些对象占用了这些内存空间,导致一致在吃内存,但是这也会有一个临界,当达到临界的时候就会触发对应的垃圾回收算法,比如full gc ,如下图,还有可能出现另外一个问题:<br /><br />**频繁的触发full gc :**jvm常见的一个问题
4. cpu飙高场景题
线上运行的系统,cpu突然飙高,怎么定位,怎么解决?
飙高,先把所有线程列出来,看哪个线程飚的最高:可能是垃圾回收线程或者是业务线程
如果是垃圾回收线程,可能是full gc频率特别高,或者是full gc 时间特别长
如果是业务线程,就把这个线程所在的堆栈类的信息调出来
假如,查到是方法有问题,arthas提供的有相应的命令:trace跟踪,watch
- Minor GC执行时间不到50ms;
- Minor GC执行不频繁,约10秒一次;
- Full GC执行时间不到1s;
- Full GC执行频率不算频繁,不低于10分钟1次;
五、JVM调优理论
1.JVM调优目标
**使用较小的内存占用来获得较高的吞吐量或者较低的延迟。使得程序在正常运行的前提下,获得更高的用户体验和运行效率。**常见问题:比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃。
2.重要的三大指标
**内存占用**:程序正常运行需要的内存大小。<br /> **延迟**:由于垃圾收集而引起的程序停顿时间。<br /> **吞吐量**:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。<br /> 和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。
3.JVM调优核心
(1)调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。
4.常见问题Full GC
Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。
5.导致Full GC的原因
**1.老年代被写满**<br />调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。<br /> **2.持久代空间不足**<br />增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例<br /> **3.System.gc()被显示调用**<br />垃圾回收不要手动触发,尽量依靠JVM自身的机制,在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节,下面详细介绍对应JVM调优的方法和步骤。
6.JVM性能调优方法和步骤
1.监控GC的状态
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。
举一个例子: 系统崩溃前的一些现象:
- 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也由之前的0.5s延长到4、5s
- FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
- 老年代的内存越来越大并且每次FullGC后老年代没有内存被释放
之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
2.生成堆的dump文件
通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。
3.分析dump文件
打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件:
- Visual VM
- IBM HeapAnalyzer
- JDK 自带的Hprof工具
- Mat(Eclipse专门的静态内存分析工具)推荐使用
文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。
4.分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
注:如果满足下面的指标,则一般不需要进行GC:
- Minor GC执行时间不到50ms;
- Minor GC执行不频繁,约10秒一次;
- Full GC执行时间不到1s;
- Full GC执行频率不算频繁,不低于10分钟1次;
5.调整GC类型和内存分配
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。
6.不断的分析和调整
通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。
六、JVM调优参数参考
1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;
2.新生代和老年代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。
3.新生代和年老代设置多大才算合理
1)更大的新生代必然导致更小的老年代,大的新生代会延长普通GC的周期,但会增加每次GC的时间;小的老年代会导致更频繁的Full GC
2)更小的新生代必然导致更大老年代,小的新生代会导致普通GC很频繁,但每次的GC时间会更短;大的老年代会减少Full GC的频率
**如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。
在抉择时应该根 据以下两点:
(1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。
(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。
4.在配置较好的机器上(比如多核、大内存),可以为老年代选择并行收集算法: -XX:+UseParallelOldGC 。
5.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太大了,一般256K就足用。
理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
七、常见的JVM调优的问题
我觉得应该很少有面试官会上来就问JVM调优怎么调,毕竟这显得有点奇怪。大概率会这样问
如果你的系统CPU/内存占用100%了你怎么办?
如果你的系统忽然不能响应了你怎么排查?
如果你的系统压测数据上不去你除了加负载还有没有其他的好办法?
这类问题你要回答的满意,肯定会涉及到怎么使用jmap, jstat,JConsole…balabala这些工具来排查和定位问题。那么接下来的问题就引出了两个,
请说说你上诉说的这些工具的使用方式
比如你定位出了老年代内存一直回收不掉,你应该怎么处理呢?
接下来才会引出JVM调优的问题
你还知道JVM有其他的什么可以调优的参数选项吗?
JVM调优参数
-Xmx4g Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4
-XX:SurvivorRatio=8 -XX:PermSize=100m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15
如果是堆内存不够: 尝试调整-Xmx,–Xms选项,这个值代表最大堆内存和初始化堆内存的大小
如果是想提高系统的并发性能: 可以尝试降低–Xss的值,这个值代表每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。别调太小了,太小了栈溢出了。
调整对象在年轻代存活的时间: -XX:MaxTenuringThreshold 默认值15,这个值代表垃圾最大年龄,对于老年代比较多的应用,减少这个值可以提高效率。对于年轻代比较多的应用,增加这个值可以增加数据在年轻代即被回收的概率。这个值调整需要尤其注意,设置小了可能引发老年代频繁full GC,设置大了可能导致某些数据长期存活于新生代,每一次Minor GC都要拷贝它,很影响性能的。
调整CMS垃圾回收器并行线程数: -XX:ConcGCThreads=4 CMS垃圾回收器并行线程线,推荐值为CPU核心数。
记得把最小值和最大值设置成同一个: 应尽量把永久代的初始值与最大值设置为同一值,因为永久代的大小调整需要进行FullGC才能实现。设置为同一个就可以防止内存抖动。
八、类加载器、双亲委派机制
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
1.类加载的过程
1.加载:加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
这里有两个重点:
字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,
从远程网络,以及动态代理实时编译
类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
2.链接:又细分为三个小部分:验证,准备,解析
验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误
准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值不是代码中具体写的初始化的值,而是 Java虚拟机根据不同变量类型的默认初始值。
解析:将常量池内的符号引用替换为直接引用的过程。
两个重点:
符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
3.初始化:这个阶段主要是对类变量初始化,是执行类构造器的过程。换句话说,只对static修饰的变量或语句进行初始化。
当你写完了一个.java文件的时候,编译器会把他编译成一个由字节码组成的class文件,当程序运行时,JVM会首先寻找包含有main()方法的类,把这个class文件中的字节码数据读入进来,转化成JVM中运行时对应的Class对象。执行这个动作的,就叫类加载器。(运行时的前奏做类加载);
2.类加载器分类
- Bootstrap ClassLoader(启动类加载器)这个类加载器负责将一些核心的,被JVM识别的类加载进来,用C++实现,与JVM是一体的。
Extension ClassLoader(扩展类加载器)这个类加载器用来加载 Java 的扩展库
Applicaiton ClassLoader(本地类加载器)用于加载我们自己定义编写的类
User ClassLoader (用户自己定义实现的加载器)当实际需要自己掌控类加载过程时才会用到,一般没有用到。
注:在java中锁能够(synchronized)能够保证类文件完整加载进内存
3.双亲委托机制
1.先检查需要加载的类是否已经被加载,如果没有被加载,则委托父加载器加载,父类继续检查,尝试请父类加载,这个过程是从下到上
2.如果走到顶层发现类没有被加载过,那么会从顶层开始往下逐层尝试加载,这个过程是从上 ———> 下;注意:事实上加载器之间不是通过继承,而是通过组合的方式来实现整个加载过程,即每个加载器都持有上层加载器的引用(不一定从本地类加载器优先,该图以用户自定义类而言)
3.那么问题来了,jvm是怎么判断类是否被重复加载呢?
JVM除了比较类是否相等还要比较加载这两个类的类加载器是否相等,只有同时满足条件,两个类才能被认定是相等的。
4.双亲委托机制采用三层结构加载类的好处?
实际上,三层类加载器代表了JVM对于待加载类的三个信任层次,当需要加载一个全限定名为java.lang.Object的类时,JVM会首先信任顶层的引导类加载器,即优先用这个加载器尝试加载,如果不行,JVM会选择继续信任第二层的拓展类加载器,往下,如果三层都无法加载,JVM才会选择信任开发者自己定义的加载器。这种”父类“优先的加载次序有效的防止了恶意代码的加载。
作用:每一个类都只会被加载一次,避免了重复加载,每一个类都会被尽可能的加载,维护Java类加载的安全
(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)有效避免了某些恶意类的加载(比如自定义了Java.lang.Object类,一般而言在双亲委托机制下会加载系统的Object类而不是用户自定义的Object类)。
父加载器中加载的类对于所有子加载器可见
子类之间各自加载的类对于各自是不可间的(达到隔离效果)