JVM整理
JVM基本结构
image.pngJava栈:
1.过多的线程或栈帧过多时会造成溢出StackOverflowError。
2.Java栈中的变量作为垃圾回收时的根节点。
3.栈帧中的局部变量表中的变量的槽位在其生命周期结束后可以被后面变量复用,只要槽位被复用,它指向的对象可以被回收。
方法区:
过多的动态代理动态生成过多的类会造成方法区溢出OutOfMemory:Metaspace。
直接内存:
申请内存时间较慢,读写较快,适合申请次数少、访问较频繁的场景。如何需要频繁申请内存不建议使用直接内存。
默认空间比值:
(新生代:老年代=1:2),(eden:from:to=8:1:1)
image.png
虚拟机参数
日志
命令 | 作用 |
---|---|
-XX:+PrintGC | 打印GC简单日志 |
-XX:+PrintGCDetails | 打印详细GC日志 |
-XX:+PrintHeapAtGC | 打印每次GC前后的堆信息 |
-XX:+PrintGCTimeStamps | GC日志加入JVM启动后的偏移时间 |
-XX:+PrintGCApplicationConcurrentTime | 打印应用程序的执行时间 |
-XX:+PrintGCApplicationStoppedTime | 打印应用程序的GC停顿时间 |
-XX:+PrintReferenceGC | 打印强、软、弱、虚引用、finalize队列信息 |
-XLoggc:log/gc.log | 输出gc日志到文件 log/gc.log为当前目录下log文件夹下gc.log文件 |
类加载/卸载跟踪
命令 | 作用 |
---|---|
-verbose:class | 跟踪类加载卸载信息 |
-XX:+TraceClassLoading | 跟踪类加载信息 |
-XX:+TraceClassUnloading | 跟踪类卸载信息 |
-XX:+PrintClassHistogram | 输出类的分布情况,在控制台按Ctrl+Break即可显示柱状图,按占用空间大小排序,哪个类多少个实例占用多大信息 |
系统参数查看
命令 | 作用 |
---|---|
-XX:+PrintVMOptions | 查看VM显式接收的参数 |
-XX:+PrintCommandLineFlags | 查看VM显式隐式接收的参数 |
-XX:+PrintFlagsFinal | 查看VM所有参数,500多行输出 |
堆参数配置
命令 | 作用 |
---|---|
-Xms | 初始堆设置,一旦耗尽,JVM对堆空间扩展,扩展上线为-Xmx指定值 |
-Xmx | 最大堆设置,堆中的可用内存需要减去from或to的区域,from、to的内存并不是按照真实值而是使用对齐操作进行估算得出 |
-Xmn | 设置新生代大小,一般设置为整个堆空间的1/3-1/4,这个设置对系统性能和GC有很大的影响,一般尽量给eden较大空间防止不够用直接分配在老年代,从而触发后面的FULL GC |
-XX:SurvivorRatio | 用来设置eden与from或to之间的比例关系SurvivorRatio=eden/from=eden/to |
-XX:NewRatio | 用来设置老年代/新生代的比例 |
-XX:+HeapDumpOnOutOfMemoryError | 在内存溢出时导出整个堆信息 |
-XX:HeapDumpPath | 配合上面参数使用导出到具体目录,后续可以使用可视化工具分析heap.dump |
"-XX:OnOutOfMemoryError=xxxx/printstack.bat %p" | 设置OOM时可执行的脚本文件来自救、报警、通知等 |
方法区配置
命令 | 作用 |
---|---|
-XX:PermSize | 初始永久区大小,1.6、1.7 |
-XX:MaxPermSize | 最大永久区大小,1.6、1.7 |
-XX:MaxMetaspaceSize | 元数据区最大可用值,1.8+ |
栈配置
命令 | 作用 |
---|---|
-Xss | 指定线程的栈大小 |
直接内存配置
随着NIO广泛使用,直接内存使用也较普遍,直接内存跳过了Java堆,直接访问原生堆空间,一定程度加快了内存访问速度。超过使用返回会触发GC。如果回收不能释放足够空间会报OOM。
|-XX:MaxDirectMemorySize|设置最大直接内存,默认为-Xmx的值|
虚拟机工作模式配置
命令 | 作用 |
---|---|
-server | 收集更多系统性能信息,使用复杂算法对程序进行优化,启动慢,执行速度远远快于client模式 |
-client | 启动快,适合运行时间不长需要快速启动的用户界面程序 |
垃圾回收概念与算法
最初要靠人工手动释放对象的内存,随着内存繁杂,所有有了自动回收内存的机制。
引用计数法(Reference Counting)
使用计数器,对于对象A,如果被任何其他对象引用1次,计数器+1,引用失效-1,回收计数器为0的对象。
问题:
- 无法处理循环引用的情况,如A引用B,B引用A。
- 每次引用的产生和消除需要伴随+1、-1操作对系统性能有一定影响。
标记清除法(Mark-Sweep)
可达对象:通过根对象进行其引用的搜索,最终可达为可达对象
标记阶段:从根对象搜索可达对象并进行标记。
清除阶段:清除不可达的对象,这些对象都是未被引用的。
复制算法(Copying)
通过标记清除法清除之后的内存空间碎片化比较严重,于是就有了复制算法。
复制算法:将内存空间分为两块,每次使用其中一块,回收时,使用的一块按照标记清除法找到可达对象,复制到另一块闲着的内存空间中,并保持连续。复制完将之前内存空间的所有对象删除即可。并将复制后的空间作为使用的空间。
Java新生代垃圾回收期使用了这样的算法,将内存空间分为eden(伊甸园)、survivor(幸存者空间分为from、to)、老年代空间
新生代:存放年轻对象、经历回收次数较少对象的堆空间。
老年代:存放经历回收次数较多依然存活对象的堆空间。
新生代空间:eden、from、to,from、to是用于复制的大小相等地位相同的两个空间
垃圾回收时,如从eden、from复制对象到to空间,大对象或老年对象或to空间满了时直接进入老年代空间。然后直接清空eden、from空间。
标记压缩法(Mark-Compact)
这种算法是适用于老年代的一种算法。因为老年代多为对象较多,回收率少,复制代价大的对象。
标记压缩法依然通过根对象搜索引用进行对象标记,然后对标记的对象直接压缩到内存空间的一端,边界外的对象直接清除。
等同于标记清除算法后进行了一次碎片整理。所以也可以叫做(Mark-Sweep-Compact)算法。
分代算法(Generational Collecting)
上述算法各有各的特点,所以需要根据不同的场景选择不同的算法。
将内存分为不同的区域,各区域使用不同的算法。
新生代对象回收率高,回收时间短,适合使用复制算法。
老年代对象回收率低,回收时间长,适合使用标记清除或者标记压缩发。
由于新生代高频率的回收时,需要去扫描老年代中对象是否含有新生代对象的引用,需要花费大量时间扫描老年代对象。于是使用卡表(Card Table)一个比特位(0,1)集合,来表示某老年代区域是否含有新生代的引用,这样直接找到标注1的老年代区域进行扫描会加快扫描过程。
分区算法(Region)
分代算法按照对象的生命周期进行分类处理。分区算法是将堆内存空间划分为连续的不同小空间,每个空间独立回收,由于GC时会产生停顿时间,所以控制块的多少可以控制停顿的时间。
可触及性
对象的可触及性如下
可触及的:可达对象。
可复活的:对象被清除后,可以在其finalize函数中通过外部变量等方式复活了自己,这种对象是可复活对象。如finalize中让static Object o=this。
不可触及的:对象的finalize函数被调用过后,不能复活自己的对象。因为finalize函数只能被调用一次,所以那些曾经通过finalize复活了的对象将不能再次复活。finalize并不是一个好的资源释放方案,被系统调用时间不明确并且可能引起应用外泄,可以使用try-catch-finally。
只有不可触及的对象才是可被回收的。
可触及性的强度
StringBuffer str = new StringBuffer("hello world!"); str变量分配在栈上,StringBuffer实例分配在堆上。
- 强引用:程序中一般使用的引用类型。上面的str为StringBuffer的强引用。强引用是不可被回收的。
- 软引用:a=new SoftReference<StringBuffer>(str); a为软引用,当堆上的空间不足时,堆上的软引用会被回收。
- 弱引用:b=new WeakReference<StringBuffer>(str);b为弱引用,System.gc()时,发现弱引用就回收。由于垃圾回收线程优先级较低,所以通常弱引用对象会存在较长时间。
- 虚引用:虚引用对象和没有持有引用几乎一样,必须和ReferenceQueue一起使用,用来跟踪垃圾回收过程。
软引用、弱应用适合存放一下可有可无的缓存数据,这样适当的时候它们会被回收,不用担心内存溢出的问题。
a=new SoftReference<StringBuffer>(str,new ReferenceQueue<StringBuffer>)可以使用引用队列来跟踪对象的gc情况。如输出user id 1 is deleted日志。
垃圾回收的停顿现象(Stop-The-World)
System.gc()时,识别并清除垃圾对象。为了能够正常高效的运行,垃圾回收时会要求系统进入一个停顿的状态,终止所有应用线程的执行,保证不会产生新垃圾,保证系统状态在某一瞬间的一致性,有利于垃圾回收器更好的标记可达对象。
垃圾收集器种类
新生代串行垃圾回收器
使用单线程独占式,通过复制算法进行垃圾回收。-XX:+UseSerialGC,通过这个参数可以设置新生代串行收集器、老年代串行收集器。在client模式下,这是模式的垃圾收集器。
老年代串行垃圾回收器
使用单线程抢占式,通过标记压缩算法进行垃圾回收。
-XX:+UseSerialGC:设置新生代老年代使用串行垃圾回收器。
-XX:+UseParNewGC:设置新生代为ParNew垃圾回收器,老年代为串行垃圾回收器。
-XX:+UseParallelGC:设置新生代为Parallel垃圾回收器,老年代为串行垃圾回收器。
老年代垃圾回收器的日志一般为Full GC xxx
新生代ParNew回收器
和SerailGC相比ParNew只是变为了多线程GC,在并发能力强的CPU上可以减少GC停顿的时间。相反在并发能力弱的CPU上,性能表现可能会更差。
-XX:+UseParNewGC:新生代使用ParNew,老年代使用串行回收器。
-XX:+UseConsMarkSweep:新生代ParNew,老年代SMS。
-XX:ParallelGCThreads:可以指定回收线程数量,一般最好与CPU数量相当。过多的线程数会影响垃圾收集性能。模式CPU数量小于8与CPU数量一致。
GC的输出日志其他值都一样,垃圾回收器名称[ParNew]变化。
新生代ParallelGC回收器
Parallel在ParNew的基础上添加了GC停顿时间的控制。
-XX:MaxGCPauseMillis:设置最大GC停顿时间,是一个大于0的证书,如果停顿时间设置的很小,那么JVM可能会使用一个小堆,回收的快,但会导致频繁GC,反而降低了吞吐量。
-XX:GCTimeRatio:n=0-100间的整数,表示将花费不超过1/(1+n)的时间来进行垃圾回收。比如n=99,那么垃圾回收整体时间将不会超过1%。
-XX:+UseAdaptiveSizePolicy:将开启自适应调节,自动调整eden、survivor比例,晋升老年代年龄等以达到停顿时间、吞吐量和堆大小的一个平衡点。
-XX:+UseParallelGC:新生代ParallelGC,老年代串行GC。
-XX:+UseParallelOldGC:新生代ParallelGC,老年代ParallelOldGC。
GC的输出日志垃圾回收器名称为PSYoungGen。
ParallelOldGC回收器
这是一个多线程、关注停顿时间吞吐量的垃圾回收器,采用的是标记压缩算法。
-XX:+UseParallelOldGC:指定新生代ParallelGC,老年代ParallelOldGC。
-XX:ParallelGCThreads:指定垃圾回收时的线程数量。
CMS回收器
ConcurrentMarkSweep并发标记清除回收器,使用标记清除算法,通过压缩配置可以规定在多少次CMS回收后,通过压缩算法清理碎片,流程分为:初始标记(独占式标记根对象),并发标记(标记其他对象),预清理(控制停顿时间、准备等),重新标记(独占式修正并发标记数据),并发清理,并发重置(重新初始化CMS的数据结构和数据)。
-XX:-CMSPrecleaningEnabled:开关是否进行预清理,如果关闭的话可能会导致一次GC的停顿时间很长。
通过预清理过程,将等待一次新生代GC,然后根据新生代GC规律预判下次新生代GC时间,选择一个CMS老年代的GC时间来避免和新生代GC冲突加长一次的GC停顿。
-XX:+UseConcMarkSweepGC:新生代ParNew,老年代CMS。
线程数量为(ParallelGCThreads+3)/4,如果使用的新生代为ParNew,则ParallelGCThreads为ParNew线程的数量。
-XX:ConcGCThreads或者-XX:ParallelCMSThreads来手工设定CMS的线程数量。
-XX:CMSInitiatingOccupancyFraction:初始占用分数,是CMS的内存回收阈值默认为68,即使用率超过68%开始回收,因为CMS是非独占式可以和应用线程交替运行,所以不能等到内存满了再进行回收。如果回收时内存不足导致CMS回收失败,则会使用老年代串行回收器进行回收。这个值可以调优,内存增长快可以将阈值调小来避免触发串行垃圾回收,内存增长不快可以适当调高阈值来减少CMS的GC次数。
-XX:+UseCMSCompactAtFullCollection:这个开关可以开启老年代内存碎片整理。
-XX:CMSFullGCsBeforeCompaction:可以指定在执行了几次CMS的GC后进行一次压缩来整理碎片。
-XX:CMSClassUnloadingEnabled:可以开启永久区的回收。
G1(Garbage-First)回收器
特点
- 使用了分代算法,同时兼顾新生代、老年代。
- 使用了分区算法,对GC停顿时间有很好的控制。
- 并行性,多线程处理垃圾回收。
- 并发性,部分程序和应用线程同时执行,部分程序和应用线程交替执行。
- 空间整理,每次GC都会进行内存碎片整理。
G1是在之前的回收器基础上,增加了分区算法等优化的新生代、老年代一起处理的垃圾回收器。
G1将堆划分为若干个区域(Region)
image.png
其中的Humongous是存放巨型对象的区,一个对象如果占用超过50%则会被标记为Humongous区,如果一个H区不够用,会寻求连续的几个H区存放,如果存在碎片则会触发Full GC。
流程分为4个阶段
-
新生代GC
新生代的GC和之前的处理并没有太大区别,都是使用的标记清除复制算法。对其中的标记过程做一下说明: -
并发标记周期
-
混合回收
-
如果需要,进行Full GC
相关题目
1.虚拟内存、物理内存、运行内存、CPU缓存区别?
我们把正在运行的程序分为3级
运行程序级别 | 名称 | 命中率 | 硬件 | 大小 | 作用 | 其他 |
---|---|---|---|---|---|---|
第一级 | CPU缓存中程序 | 最高 | SRAM(静态RAM) | 几MB或几十MB | 解决CPU跟内存之间数据传输速度不匹配的问题 | 属于存储金字塔的第二层仅次与CPU寄存器分为L1、L2、L3 |
第二级 | 物理内存(主存)中程序 | 中等 | DRAM(内存条动态RAM) | 4、8、16、32G等 | 用户态内核态等进程使用内存 | |
第三级 | 虚拟内存中 | 最低 | 磁盘 | 很大500G、几T等 | 解决计算机在运行较大的程序时内存不足的情况 | 速度比主存慢得多。解决物理地址与虚拟地址转换;调度哪些程序、数据进入主存;替换主存中的哪些程序、数据;更新保证程序、数据一致性 |
其他:
ROM只读存储器 存放BIOS程序及系统设置等。
运行内存=物理内存
JVM -Xmx -Xmx 设置的为物理内存+虚拟内存
2.高并发场景的进程卡死如何处理?
高并发时,由于大量对象会迅速填满内存,频繁触发GC,导致GC停顿积压请求进而导致恶性循环,长时间的GC停顿让程序看起来像卡死了一样。1.提升内存空间;2.线程中尽量少用1次性对象;3.使用对象池再利用对象;4.迫不得已时需要自己编写适合业务的垃圾回收机制。
3.都有哪些情况的内存溢出?
1.线程太多栈帧太多会导致Java栈溢出;2.动态代理太多会导致元数据区溢出;3.一次读取过多数据创建超大对象导致堆内存溢出;4.循环或者超大量的创建对象致使频繁GC,当98%时间GC,Heap量不足2%会报堆内存溢出。
3.GC的并发与并行?
并发是指GC线程与应用线程交替运行,并行是指应用程序停止,多个GC线程一起执行GC。
并行:是一个工作启动多个线程同时完成。
并发:是多个工作每个工作的线程可以交替执行。
4.CMS的预清理做了什么事情?
5.卡表CardTable是什么?
新生代GC时,为了不去频繁地扫描老年代对象而产生的一种数据结构。
对老年代区域进行了point-out(自己引用了谁)的记录,其数据结构是一个比特位集合。
6.RememberSet是什么?
在G1垃圾回收器中,堆被分为一个一个的Region,每个Region对应了一个RememberSet,描述的是point-in(谁哪个Region引用了自己Region),它的数据结构一般为HashTable,key为其他Region的起始地址,value为CardTable。在新生代回收时同样根据E、S Region中的RS快速找到哪些Old Region中的哪些Card中的对象引用了本Region中的哪些对象,将这些对象标记为可达,不进行回收。