转载自:https://blog.csdn.net/zhaocuit/article/details/73826256
一、jvm内存区域
1、程序计数器
- 内存空间小
- 线程私有
- 指示当前线程执行字节码的行号
- 如果执行本地方法,计数器值为空
- 不会出现OutOfMemoryError
2、虚拟机栈
- 线程私有
- 每个方法执行的同时会创建一个栈帧,栈帧存储局部变量表、操作栈、动态连接、方法出口等信息
- 方法调用过程即为栈帧的入栈出栈过程
- jvm允许栈大小固定或者动态扩展
- 局部变量表存储基本数据类型、对象的引用、返回类型,Long、double占用2个局部变量空间(或者叫”字”)
- 影响栈大小的jvm参数,-Xss
- 影响栈深度的因素:帧栈大小(局部变量表)
- StackOverflowError:栈深度大于jvm允许的深度时(StackTest1,StackTest2)
- OutOfMemoryError:在扩展过程中,没有足够的空间来支持扩展时(StackOutOfMemory)
- 局部变量表中的字可以被重用(StackTest3)
- 局部变量表中的字使用可能会影响GC回收。如果字没有被复用,则它所引用的对象不会被GC回收(StackTest4)
3、本地方法栈
- 与虚拟机栈类似
- 虚拟机栈管理Java函数的调用
- 本地方法栈管理本地方法的调用
4、方法区
- 被所有线程共享
- 主要存储类的类型信息、常量池、域信息、方法信息,存储的信息来自class文件
- 独立于Java堆的内存空间
- 也可以被GC回收,包括常量池的回收和类元数据的回收
- 常量池回收例子(PermGenGC)
- 类元素回收条件:
该类的所有实例被回收
装载该类的ClassLoader被回收 - 可能会抛出OutOfMemoryError:PermGen space异常
5、堆
- 被所有线程共享
- 所有的对象和数组都被分配到堆中
- 堆被分为新生代(eden、s0、s1)和老年代
- 刚创建的对象在eden中,经过一次GC后,会移动到s0或s1中,到了指定的GC次数(或者同一个年龄的变量占S空间的一半)后,会被移动到老年代中
二、jvm内存分配参数
1、最大(小)堆内存参数
- -Xmx:最大堆内存参数(HeapMaxTest)。
- -Xms:最小堆内存参数,如果被设置,则jvm尽量在此值以下内运行程序,如果实在不能满足要求,才向系统申请更多的内存空间,但不能大于-Xmx,否则会抛出异常OutOfMemoryError
- -Xms如果设置过小,则GC会更频繁,GC的总体时间会更多。(HeapMinTest)
- 建议将-Xms和-Xmx设置成一样
2、堆新生代参数
- -Xmn:新生代大小。
- -XX:NewSize:最小新生代大小。
- -XX:MaxNewSize:最大新生代带大小。
- 设置-Xmn等同于-XX:NewSize=-XX:MaxNewSize。
3、堆的比例分配
- -XX:SurvivorRatio:设置eden和s0的比例,s0=s1
- 默认值为8,即eden=-Xmn/10*8,s0=-Xmn/10
- 示例代码(HeapSurvivorRatioTest)
- -XX:NewRatio:设置新生代和老年代的比例
- -XX:NewRatio=老年代/新生代
- 默认值为2,即老年代:新生代=2:1
- 示例代码(HeapNewRatioTest)
4、设置方法区参数
- -XX:PermSize:最小方法区大小。
- -XX:MaxPermSize:最大方法区大小。
- 方法区大小决定了系统可以支持多少个类定义和多少个常量。(PermGenGC1)
- 建议这2个参数值都设置成一样。
5、设置线程栈参数
- -Xss:设置线程栈大小。
- 在进行局部变量分配、函数调用时需要申请栈空间。
- 栈空间的大小决定函数调用的深度(在局部变量一定的情况下)
- 栈空间的大小决定局部变量的个数(在函数调用深度一定的情况下)
- 如果栈空间过大,则支持的线程数量越少(在栈总空间一定的情况下),示例代码(StackOutOfMemory)
6、总结
三、垃圾回收算法
1、如何判断一个Java对象是否已死?
-
引用计数算法:
给对象添加一个计数器,有一个地方引用它时就加1,引用失效时减1。
优点:简单、高效
缺点:对循环引用无效(A引用B,B引用A) -
根搜索算法:
从GC Roots节点开始搜索,能搜索到的对象则认为对象还存活,不能搜索到的对象则认为死亡,应被垃圾回收。
2、标记-清除算法
- 标记需要回收的对象,然后清除标记的对象
-
缺点:清除后会产生大量内存碎片,导致大对象在内存空间充足时无法被分配。
3、复制算法
- 将内存划分为大小相等的2块,当A块用完时,将存活的对象复制到B块中,清除A块内存中的所有对象。
- 优点:效率比标记-清除高,且不会出现内存碎片。
- 缺点:内存被分成了AB两块,相当于缩小了一半的内存,如果对象的死亡率低则会有较多的复制。
- 新生代一般都采用这种算法,因为新生代死亡率较高。
-
jdk的新生代对这个算法进行了优化,jdk将其划分为eden、s0、s1三个部分,其中s0和s1即为大小相等的两块内存。执行过程大致为:新创建的对象首先分配到eden中,当空间不够时,进行垃圾回收,将存活的对象拷贝到s0中,清除eden所有对象,继续执行;当空间再次不够时,再次进行垃圾回收,将eden和s0中的存活对象拷贝到s1中,并清除eden和s0中所有对象,如果s1的空间不足以容纳eden和s0中的存活对象,则某些对象会进入老年代;如此循环迭代。
4、标记-整理算法
- 标记需要回收的对象,将存活对象移动到一端,最后清除边界外的内存。
-
老年代一般会使用这种算法,因为老年代存活率较高。
5、分代搜集算法
- 根据对象的存活周期不同将内存划分成几块,一般是把Java堆划分成新生代、老年代。
- 根据各个年代的特点选用适当的回收算法。
四、垃圾回收器
上图中的箭头表示能搭配使用,比如新生代如果使用serial垃圾回收器,那么老年代就可以使用CMS或serial old,不能使用parallel old。
1、Serial
- 单线程垃圾收集器
- 垃圾回收时需要暂停所有用户线程(stop the world)
- 采用复制算法
- client模式下新生代默认垃圾回收器
- 优点:简单、高效(与其他回收器的单线程比)
- 缺点:单线程
2、ParNew
- 是Serial回收器的多线程版本
- server模式下首选的新生代回收器,因为他可以和CMS配合工作(CMS比较牛逼)
3、Paralle Scavenge
- 并行多线程回收器
- stop the world
- 采用复制算法
- 重点关注系统的吞吐量,即用户代码时间/(用户代码时间+垃圾回收时间)
- 主要适合后台运算任务
- 相关参数:
MaxGCPauseMillis:最大垃圾回收时间,大于0的毫秒数
GCTimeRatio:吞吐量,(0-100]的整数,默认1%的垃圾回收时间。
-XX:+UseAdaptiveSizePolicy:打开这个参数后,只需要设置-Xmx(最大堆)、GCTimeRatio(或者MaxGCPauseMillis),jvm会自动设置堆相关参数。
4、Serial Old
- 是Serial回收器的老年代版本。
- 标记-整理算法。
- 单线程回收垃圾。
- stop the world。
- client模式下老年代默认垃圾回收器。
- server模式下主要作为CMS发生concurrent model
failure的时候作为后备预案。
5、Paralle Old
- 并行多线程回收器
- stop the world
- 采用标记整理算法
- 主要配合Paralle Scavenge使用
6、CMS
- 以最短停顿时间为目标的回收器
- 主要应用在B/S架构上,重服务器的响应速度
- 采用标记清除算法
-
主要分为如下几个阶段:
初始标记:标记GC roots能直接关联到的对象,速度快,执行时间短,会stop the world。
并发标记:GC roots追踪的过程,不会stop the world,执行时间相对较长。
重新标记:修正并发标记期间产生的变动,初始标记<重新标记执行时间<并发标记或并发清除,会stop the world。
并发清除:垃圾回收,不会stop the world,执行时间相对较长。
-
缺点:
对CPU资源比较敏感:默认回收线程数=(CPU+3)/4,当CPU小于4个时,对应用程序影响较大。
无法处理浮动垃圾(并发清除阶段产生的垃圾):默认在老年代空间使用了68%后,就会进行垃圾回收(-XX:CMSInitiatingOccupancyFraction)
如果在运行期间预留的内存不够使用时,将用serial old进行垃圾回收,这样垃圾回收的时间就很长了。易产生内存空间碎片:采用标记-清除算法
-XX:+UseCMSCompactAtFullCollection:是否在FullGC后进行碎片整理,碎片整理过程会stop the world。
-XX:+CMSFullGCsBeforeCompaction:多少次FullGC后进行一次碎片整理。
7、G1
- 1.7版本才算成熟,个人感觉是CMS的替代版
- 采用标记-整理算法,没有碎片产生
- 精确控制停顿,在M毫秒内垃圾回收时间不得超过N毫秒。
- 将Java堆划分为多个独立的region,并跟踪region的垃圾堆积程度,在后台维护一个优先列表,优先回收垃圾最多的region。
- G1和CMS一样,都要经历初始标记、并发标记、重新标记和并发清除四个过程。
五、JVM相关参数汇总
六、内存分配策略
- 对象优先在eden分配(TestAllocation)
- 大对象直接进入老年代(TestPretenureSizeThreshold)
- 长期存在的对象进入老年代(TestMaxTenuringThreshold.test1())
- 动态对象年龄的判断(TestMaxTenuringThreshold.test2())
- 空间分配担保(TestMaxTenuringThreshold.test3())
建议都加上-XX:+HandlePromotionFailure