个人JVM总结

前言算法

本篇博客将结合博主在实际工做中对JVM的认识,以及前一段时间春节大流量下对Java后台服务的一些参数设置的一个总结。若是你对JVM还不熟悉,能够参考博主之前的博客:《对Java内存结构的一点思考和实践》缓存



JVM体系结构
网络

wKiom1ieuq_g3eCNAAOXIoMxwRo250.png

上图,包含了组成JVM的各个要素,下面咱们简单先来介绍下它们。多线程

类加载子系统:负责从文件系统或者网络上加载CLASS信息,加载的信息存放在内存空间中的方法区。并发

方法区:就是存放类信息、常量信息、常量池信息等。ide

Java堆:在JVM启动的时候会创建Java堆,Java堆是Java程序最主要的内存区域,也是JVM进行垃圾回收的核心区域,几乎全部的对象都放置在Java堆中,而且堆空间是全部线程共享的。工具

Java栈:每一个虚拟机线程都会有一个私有的栈空间,即Java栈。在Java栈中保存着局部变量、方法参数、方法调用、返回值等信息。总之,这个空间和多线程有关系,和方法的执行有关系。性能

本地方法栈:要知道一些Java类的实现是依赖于native方法的,也就是一般用C编写的本地方法,本地方法栈和Java栈相似。spa

垃圾收集系统:Java进行垃圾清理的机制,后文在详细介绍。线程

PC寄存器:寄存器也是每一个线程私有的空间,JVM会为每一个线程建立PC寄存器,在任意时刻,一个JAVA线程老是在执行一个方法,即当前线程的当前方法。实际上,若是当前方法不是本地方法,那么会将当前方法的信息存入PC寄存器。在PC寄存器中,实际上,就是一个指针的概念,表明了当前线程的一个执行环境。在多线程进行上下文切换的时候,PC寄存器就发挥做用了。

执行引擎:负责执行虚拟机的字节码。


堆、栈、方法区

能够说,在内存当中,咱们最为关心的就是堆、栈、方法区。下面咱们重点剖析下,它们三者之间的关系。

wKioL1ie0d7ixOaiAAA1b5S3xxg889.png

好比,有一个User类,有2个实例对象u1/u2,那么存储结构信息就如上图所示。

堆,解决的是数据存储的问题,即数据怎么放,放在哪里。即User类的2个实例对象都存放在堆中。

栈,解决的是程序的运行问题,即程序如何执行。说白了,u1/u2这2个局部变量,对真实对象的引用就存放在栈中。

方法区,是堆、栈的一个辅助区域,或者说是先决条件,没有类信息等,如何建立对象呢。


wKiom1ie1mLhZrw9AAAsvKqAcLI352.png

Java堆能够细分为新生代、老年代。其中新生代存放新生的对象或者年龄不大的对象;老年代则存放老年对象。新生代分为Eden、S0、S1这三个区域,SO/S1也称为from/to区域。S0/S1这两个区域是大小相等而且能够互换角色的空间,在后文的复制回收算法中在详细描述它们。

在绝大多数状况下,对象首先分配在Eden区域,在一次新生代回收后,若是对象还存活,则会进入S0/S1区域,以后每通过一次新生代回收,若是对象存活,它的年龄就加一,当年龄达到阀值后,就会进入老年代。


Java栈的结构

wKiom1ie2aqRhIoWAABYT4VAuJg941.png


Java方法区,也称之为永久区(Perm)。方法区,它保存系统的类信息,好比类的字段、方法、常量池等信息。方法区的大小决定了系统能够保存多少个类,若是系统定义了太多的类,而方法区空间不足,就有可能抛出内存溢出错误。



JVM参数设置


-XX:+PrintGC                   使用这个参数,虚拟机启动后,只要遇到GC,就会打印日志        

-XX:+PrintGCDetails            能够查看详细信息,包括各个区的状况

-XX:+PrintCommandLineFlags     将给虚拟机设置的参数所有打印出来


-Xms200m                       设置JAVA程序启动时初始堆大小

-Xmx500m                       设置JAVA程序能够得到的最大堆大小

注意:在实际开发中,咱们通常状况下,都会将堆的初始值和最大值设置成同样的。这样的好处在于减小程序运行时的垃圾回收次数,从而提升性能。


-Xmn70m                        设置新生代的大小

注意:若是设置一个比较大的新生代,那么势必减小老年代的大小,因此这个参数对系统性能以及GC行为有很大影响。在实际中,通常将新生代大小设置为整个堆大小的1/3到1/4左右。也就是说新生代:老年代=1/2 到 1/3。总而言之,咱们应该尽量将对象预留在新生代,减小老年代的GC次数。


-XX:SurvivorRatio              用于新生代中Eden/from=Eden/to的比例 


-XX:+HeapDumpOnOutOfMemoryError 

-XX:HeapDumpPath=/log/xxx.dump   

开启堆内存溢出OOM错误导出堆信息功能,并打印到指定路径下

注意:实际上这个堆内存导出文件,能够用相关内存分析工具进行分析。


-Xss1m                         线程的最大栈空间

注意:这个参数直接决定了方法能够调用的最大深度,特别是递归调用。


-XX:PermSize=64M 

-XX:MaxPermSize=64M            方法区的初始、最大大小设置


-XX:MaxDirectMemorySize        

注意:这个参数用于配置直接内存。什么是直接内存,想想NIO,你就会明白。若是不设置,默认就是最大堆大小。所以若是直接内存若是没有有效释放空间,或者达到堆空间的最大大小,就会OOM。


-client/-server                事实上,在JDK1.7 64之后,就已经没有这方面的事情了。


-XX:MaxTenuringThreshold=15    设置新生代对象进入老年代的对象的年龄阀值,默认就是15次


-XX:PretenureSizeThreshold     

若是对象的大小比较大,没法在Eden直接分配呢?会直接进入老年代!可是须要注意TLAB的优先分配。


-XX:+UseTLAB

-XX:+PrintTLAB

-XX:+TLABSize

使用、打印、设置TLAB大小。后文会介绍TLAB。


-XX:UseSerialGC                 设置新生代、老年代使用串行回收器


-XX:UseParNewGC                 

-XX:ParallelGCThreads

设置新生代ParNew回收器,以及回收线程的个数


-XX:UseParallelOldGC            


CMS相关参数后文介绍。



GC


垃圾收集算法


引用计数法

一句话,若是对象存在被引用,则计数器加一,失效则减一;若是计数器为0,就能够回收。

没法解决循环引用的问题,并且计数器常常加减,操做频繁,性能不佳。实际工做中并无使用。


标记清除法

分为标记、清除阶段。那么怎么标记呢?并非采用上面的引用计数法,而是基于ROOT树寻找路径来肯定是否应该被标记。被标记将被清除,这会致使空间碎片的问题。垃圾回收后的空间不是连续的,显然不连续的内存空间的效率要低于连续内存空间的效率。


复制算法

核心思想就是将内存空间分为2块,每次只是用其中一块。在垃圾回收时,将其中一块A的没法回收的留存对象所有COPY到另外一块B内存中,而后清除块A内存中的全部对象。垃圾回收时,反复去交换这2块空间的角色,完成垃圾收集工做。这里,显然避免了内存碎片的问题,可是须要注意的是复制的成本。若是有不少对象须要复制呢?咱们知道新生代的不少对象很不稳定,是会被频繁回收的,所以对于新生代而言,这种算法的复制成本比较小,因此被普遍应用。相比老年代而言,大量的对象是稳定的,甚至是不会被回收的,所以复制成本太大了,不适合。


标记压缩法

在标记清除算法的基础上作了些改进,就是把存活的对象压缩到内存的另外一端,然后进行垃圾清理,从而避免了内存碎片的问题。事实上,老年代采用的就是这种算法。


其实,为何分为新生代、老年代?说白了,就是想根据对象的特色进行分类,不一样的特色就可使用不一样的算法,这就是分代的好处。对于新生代、老年代而言,新生代回收频率很高,可是每次回收耗时很短;而老年代回收频率很低,耗时会相对较长,因此应该尽可能减小老年代的GC。


停顿现象

垃圾回收的任务就是识别和回收垃圾对象,完成内存清理动做。为了让GC高效的执行,大部分状况下,会要求系统进入一个停顿的状态,也就是暂停了全部的应用线程。由于只有这样才不会有新的垃圾产生,同时停顿也保证了系统在某一瞬间的一致性,有益于垃圾的标记。所以在进行GC的时候,会产生应用程序的停顿。


TLAB

Thread Local Allocate Buffer,线程本地分配缓存,一个线程专用的内存分配区域。在实际中,每一个线程势必会用到内存,为何不提早就为每一个线程建立一个较小的专属的内存区域呢?没必要等到线程使用到内存的时候在去申请。这样的话,会加速线程的运行速度,并且也避免了多线程的问题。固然,TLAB区域并不会太大。



垃圾收集器

wKioL1ifxqaQzYIiAADf33N-IsA143.png


串行回收器

所谓串行,就是单线程进行垃圾回收工做。新生代、老年代均可以使用。若是机器的并行性能比较差,能够考虑这种,由于它的专一性、独占性会有不错的表现。


并行回收器:ParNew

在串行回收器的基础上进行功能加强,就是使用多线程。适用于新生代。显然,对于并行能力较强的计算机而言,会有效缩短垃圾回收的实际时间。


并行回收器:ParallelGC

多线程独占+复制算法,新生代回收,很是关注系统的吞吐量,由于它提供参数来进行吞吐量的控制,好比设置最大垃圾收集停顿时间-XX:MaxGCPauseMillis。


并行回收器:ParallelOldGC

多线程独占+标记压缩算法,老年代回收。


目前最主流的回收器:CMS

CMS,即Concurrent Mark Sweep,并发标记清除,采用标记算法,适用老年代,关注系统停顿时间。

须要注意的是,CMS并非独占的回收器,也就是说CMS回收过程当中,应用程序仍然在不断工做,这是CMS回收器的一大优势。也正由于如此,CMS回收,应用程序不停顿运行,垃圾继续会产生,须要足够的内存空间。CMS并不会等到应用程序饱和才开始回收,而是根据指定的阀值开始回收。好比-XX:CMSInitiatingOccupancyFration设置为68,就是说当老年代空间使用达到68%的时候,CMS开始回收。另外,CMS为了不标记算法产生的内存碎片问题,提供了功能,好比-XX:+UseCMSCompactAtFullCollection可让CMS回收一次后进行内存碎片整理,-XX:CMSFullGCBeforeCompaction参数能够设置多少次CMS回收后,对内存进行一次压缩整理


G1回收器

目前应用并不普遍。其主要的思想在于分区算法,把内存区域划分为N多个小的独立的空间,其实是想细粒度的对内存进行划分,这样就能够细粒度的回收,而不是对整个空间进行垃圾回收,从而提高性能,并减小了GC停顿时间。


到这里,JVM总结就结束了,但愿对你有用吧~

相关文章
相关标签/搜索