JVM-内存模型
线程栈空间
包括程序计数器、虚拟机栈和本地方法栈
程序计数器
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。
虚拟机栈
当创建一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。
操作数栈
我们知道,Java 字节码是 Java 虚拟机所使用的指令集。因此,它与 Java 虚拟机基于栈的计算模型是密不可分的。
在解释执行过程中,每当为 Java 方法分配栈桢时,Java 虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。
具体来说便是:执行每一条指令之前,Java 虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
以加法指令 iadd 为例。假设在执行该指令前,栈顶的两个元素分别为 int 值 1 和 int 值 2,那么 iadd 指令将弹出这两个 int,并将求得的和 int 值 3 压入栈中。
局部变量区
Java 方法栈桢的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中。
实际上,Java 虚拟机将局部变量区当成一个数组,依次存放 this 指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。和操作数栈一样,long 类型以及 double 类型的值将占据两个单元,其余类型仅占据一个单元。
局部变量数组的加载、存储指令都需要指明所加载单元的下标。举例来说,aload 0 指的是加载第 0 个单元所存储的引用,指的便是加载 this 指针。
本地方法栈
本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的。
方法区
HotSpot 虚拟机使用永久代来实现方法区,但在其它虚拟机中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一说。因此,方法区只是 JVM 中规范的一部分,可以说,在 HotSpot 虚拟机中,设计人员使用了永久代来实现了 JVM 规范的方法区。
并且方法区只是一个内存模型的逻辑概念,在Java8中,方法区是包括元空间本地内存和堆内存
Metaspace属于本地内存,Java8中其实只保存了类信息,而常量和静态变量都是保存在堆上的。
Java8 为什么使用元空间替代永久代,这样做有什么好处呢?
- 移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
- 永久代内存经常不够用或发生内存溢出,爆出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。
堆
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2。
其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
新生代
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。单个区域的空间比例是8:1:1。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
新建对象内存分配规则
1、默认会在Eden和From Survivor上分配空间。
2、当申请的空间大小超过新生代的空闲空间时,会直接将对象创建在老生代。当然也可以显示指定这个阈值,只要超过这个阈值的对象,都会直接创建在老生代中。
3、当对象所需的空间大小超过了老生代的空闲空间,则直接回抛出OOM:heap的异常。
内存回收
JVM 的内存区域中,程序计数器、虚拟机栈和本地方法栈这 3 个区域是线程私有的,随着线程的创建而创建,销毁而销毁;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存分配和回收都具有确定性。那么垃圾回收的重点就是关注堆和方法区中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。
对象在什么时候可以被回收?
那 JVM 又是怎样判断一个对象是可以被回收的呢?一般一个对象不再被引用,就代表该对象可以被回收。目前有以下两种算法可以判断该对象是否可以被回收。
引用计数算法
这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用,引用计数器就会加 1;每当引用失效,计数器就会减 1。当对象的引用计数器的值为 0 时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。
可达性分析算法
GC Roots 是该算法的基础,GC Roots 是所有对象的根对象,在 JVM 加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法。以上两种算法都是通过引用来判断对象是否可以被回收。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为了以下四种:
GC Root对象的包括如下几种:
- Java虚拟机栈中的引用的对象 ;
- 本地方法栈中JNI的引用的对象。
- 方法区中的类静态属性引用的对象 ;
- 方法区中的常量引用的对象 ;
回收算法
新生代回收:(复制算法)
新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。 当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。
在堆中,新生代主要存放的是哪些很快就会被GC回收掉的或者不是特别大的对象(是否设置了-XX:PretenureSizeThreshold 参数)。复制算法的新生代分为3个区:较大的Eden和两个较小的Survivor(默认的Eden:Survivor = 8:1)。发生在新生代的GC为Minor GC 。在Minor GC时会将新生代中还存活着的对象复制进一个Survivor中,然后对Eden和另一个Survivor进行清理。所以,平常可用的新生代大小为Eden的大小+一个Survivor的大小。
Minor GC 的另外一个好处是不用对整个堆进行垃圾回收。但是,它却有一个问题,那就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。这样一来,岂不是又做了一次全堆扫描呢?
卡表
HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。
在进行 Minor GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Minor GC 的 GC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。由于 Minor GC 伴随着存活对象的复制,而复制需要更新指向该对象的引用。因此,在更新引用的同时,我们又会设置引用所在的卡的标识位。这个时候,我们可以确保脏卡中必定包含指向新生代对象的引用。
在 Minor GC 之前,我们并不能确保脏卡中包含指向新生代对象的引用。其原因和如何设置卡的标识位有关。首先,如果想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么 Java 虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。这个操作在解释执行器中比较容易实现。但是在即时编译器生成的机器码中,则需要插入额外的逻辑。这也就是所谓的写屏障(write barrier,注意不要和 volatile 字段的写屏障混淆)。
写屏障需要尽可能地保持简洁。这是因为我们并不希望在每条引用型实例变量的写指令后跟着一大串注入的指令。因此,写屏障并不会判断更新后的引用是否指向新生代中的对象,而是宁可错杀,不可放过,一律当成可能指向新生代对象的引用。
这么一来,写屏障便可精简为下面的伪代码[1]。这里右移 9 位相当于除以 512,Java 虚拟机便是通过这种方式来从地址映射到卡表中的索引的。最终,这段代码会被编译成一条移位指令和一条存储指令。
CARD_TABLE [this address >> 9] = DIRTY;
老年代回收:(标记-清除算法/标记-整理算法)
老年代则是存放那些在程序中经历了好几次回收仍然还活着或者特别大的对象(是否设置了-XX:PretenureSizeThreshold 参数)。
默认的方式首先是尽量清空新生代 ( YoungGen ),因此在调 System.gc() 时,新生代 ( YoungGen ) 中存活的对象会提前进入老年代
老年代采用的是标记-清除或者标记-整理算法,这两个算法主要看虚拟机采用的哪个收集器,两种算法的区别是:标记-清除可能会产生大量连续的内存碎片。
标记:(一致)遍历GC Roots,将存活的对象标记
整理:移动所有存活对象,按照内存地址次序依次排列,将末端内存地址以后的内存全部回收
新生代进入老年代的规则
1.分配担保机制:当Minor GC时,新生代存活的对象大于Survivor的大小时,这时一个Survivor装不下它们,那么它们就会进入老年代。
2.如果设置了-XX:PretenureSizeThreshold3M 那么大于3M的对象就会直接就进入老年代。
3.在新生代的每一次Minor GC 都会给在新生代中的对象+1岁,默认到15岁时就会从新生代进入老年代,可以通过-XX:MaxTenuringThreshold来设置这个临界点。
相比较而言,在老年代中的对象比新生代中的对象不易回收许多。
永久代回收:(即方法区回收)
JVM的方法区,也被称为永久代。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。
回收器
CMS
首先cms在1.9已经被标记为废弃,主要原因在于标记清除下的悬浮内存,导致内存空间碎片化,进而导致fullGC的发生。不过其并行执行垃圾回收的性能还是值得认可的,至少1.9后主推的G1在常规情况下也是不如它的效率好的。
CMS 收集器的 GC 周期主要由 7 个阶段组成,其中有两个阶段会发生 stop-the-world,其它阶段都是并发执行的。
G1
G1的堆内存结构比较特殊,虽然也有年代划分,但从物理角度上却不一样。G1将整块内存分配成若干个同等大小的reg。新生代(两个sub区加ed区)和老年代各自由不同数量的reg组成。
垃圾回收的算法应该算是标记整理。所以其规避了cms内存碎片化的问题,大大降低了fullGC的频率。是一个分代垃圾收集器,既负责年轻代,也负责老年代的垃圾回收。
在 G1 中,还有一种叫 Humongous 区域,用于存储特别大的对象。G1 内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代的 YoungGC 中被回收掉。
G1 分为 Young GC、Mix GC 以及 Full GC。
G1 Young GC 主要是在 Eden 区进行,当 Eden 区空间不足时,则会触发一次 Young GC。将 Eden 区数据移到 Survivor 空间时,如果 Survivor 空间不足,则会直接晋升到老年代。此时 Survivor 的数据也会晋升到老年代。Young GC 的执行是并行的,期间会发生 STW。
当堆空间的占用率达到一定阈值后会触发 G1 Mix GC(阈值由命令参数 -XX:InitiatingHeapOccupancyPercent 设定,默认值 45),Mix GC 主要包括了四个阶段,其中只有并发标记阶段不会发生 STW,其它阶段均会发生 STW。
所以它虽然常态性能略输于cms但却没有cms特殊情况下的极端性能问题,总体更稳定。
值得一提的是G1中各代的内存区域里reg间不一定是连续的,所以对于cpu缓存加载机制并不是特别友好,而且大对象占据超过一个reg时还带来内存浪费的问题。
所以总的来说1.8可以用G1但得考虑场景,首先这个内存空间要大,保证每个reg尽量大,以减少内存浪费,保守估计8g以上用g1。实际公司很少会去升级jdk版本,大部分都是1.8,好在oracle一些1.9 10 11 12的特性都有以补丁的方式落到1.8。所以1.8还是比较安全实用的。
G1 具备 Pause Prediction Model ,即停顿预测模型。用户可以设定整个 GC 过程中期望的停顿时间,用参数 -XX:MaxGCPauseMillis 可以指定一个 G1 收集过程的目标停顿时间,默认值 200ms。G1 会根据这个模型统计出来的历史数据,来预测一次垃圾回收所需要的 Region 数量,通过控制 Region 数来控制目标停顿时间的实现。
性能衡量指标
停顿时间
指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
垃圾回收频率
多久发生一次指垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。
查看 & 分析 GC 日志
已知了性能衡量指标,现在我们需要通过工具查询 GC 相关日志,统计各项指标的信息。首先,我们需要通过 JVM 参数预先设置 GC 日志,通常有以下几种 JVM 参数设置:
-XX:+PrintGC 输出GC日志 -XX:+PrintGCDetails 输出GC的详细日志 -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式) -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息 -Xloggc:../logs/gc.log 日志文件的输出路径
这里使用如下参数来打印日志:
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs
如果是长时间的 GC 日志,我们很难通过文本形式去查看整体的 GC 性能。此时,我们可以通过GCViewer工具打开日志文件,图形化界面查看整体的 GC 性能。
这里我再推荐一个比较好用的 GC 日志分析工具,GCeasy是一款非常直观的 GC 日志分析工具,我们可以将日志文件压缩之后,上传到 GCeasy 官网即可看到非常清楚的 GC 日志分析结果。
GC 调优
GC 调优策略找出问题后,就可以进行调优了,下面介绍几种常用的 GC 调优策略。
1. 降低 Minor
增大新生代和eden区的空间,只适用于大量对象存活较短的情况,因为很多对象可能存活不过一个gc周期,但如果对象存活很长就没啥优势了。
GC 频率通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。
可能你会有这样的疑问,扩容 Eden 区虽然可以减少 Minor GC 的次数,但不会增加单次 Minor GC 的时间吗?如果单次 Minor GC 的时间增加,那也很难达到我们期待的优化效果呀。
我们知道,单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,那么正常情况下,Minor GC 的时间为 :T1+T2。当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:两次扫描新生代,即 2T1。
可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。通常在虚拟机中,复制对象的成本要远高于扫描成本。
如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。
2. 降低 Full GC 的频率
- 业务上尽量控制创建大对象;
- 增大旧生代空间;
- 初始和最大堆空间设成一样的,避免小空间的频繁gc;
- 选取合适垃圾回收器
STW
https://www.jianshu.com/p/1f897ab1eed0
在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
不同垃圾收集器的Stop-The-World情况,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。
-
Java中一种全局暂停的现象,jvm挂起状态
-
全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
-
多半由于jvm的GC引起,如:
1.老年代空间不足。
2.永生代(jkd7)或者元数据空间(jkd8)不足。
3.System.gc()方法调用。
4.CMS GC时出现promotion failed和concurrent mode failure
5.YoungGC时晋升老年代的内存平均值大于老年代剩余空间
6.有连续的大对象需要分配 -
除了GC还有以下原因:
1.Dump线程--人为因素。
2.死锁检查。
3.堆Dump--人为因素。
链接:https://www.jianshu.com/p/d686e108d15f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在虚拟机进行GC的时候会需要让所用的线程都停止工作,等待他GC完成,那么他是如何保证所有线程全部都停止工作的呢?
只有当所有线程都跑到了安全点,或者进入安全区域之后,才会进行GC
安全点
https://time.geekbang.org/column/article/13091
当然,安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。
举个例子,当 Java 程序通过 JNI 执行本地代码时,如果这段代码不访问 Java 对象、调用 Java 方法或者返回至原 Java 方法,那么 Java 虚拟机的堆栈不会发生改变,也就代表着这段本地代码可以作为同一个安全点。只要不离开这个安全点,Java 虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。由于本地代码需要通过 JNI 的 API 来完成上述三个操作,因此 Java 虚拟机仅需在 API 的入口处进行安全点检测(safepoint poll),测试是否有其他线程请求停留在安全点里,便可以在必要的时候挂起当前线程。
除了执行 JNI 本地代码外,Java 线程还有其他几种状态:解释执行字节码、执行即时编译器生成的机器码和线程阻塞。阻塞的线程由于处于 Java 虚拟机线程调度器的掌控之下,因此属于安全点。
其他几种状态则是运行状态,需要虚拟机保证在可预见的时间内进入安全点。否则,垃圾回收线程可能长期处于等待所有线程进入安全点的状态,从而变相地提高了垃圾回收的暂停时间。
对于解释执行来说,字节码与字节码之间皆可作为安全点。Java 虚拟机采取的做法是,当有安全点请求时,执行一条字节码便进行一次安全点检测。
执行即时编译器生成的机器码则比较复杂。由于这些代码直接运行在底层硬件之上,不受 Java 虚拟机掌控,因此在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测。
那么为什么不在每一条机器码或者每一个机器码基本块处插入安全点检测呢?原因主要有两个。
第一,安全点检测本身也有一定的开销。不过 HotSpot 虚拟机已经将机器码中安全点检测简化为一个内存访问操作。在有安全点请求的情况下,Java 虚拟机会将安全点检测访问的内存所在的页设置为不可读,并且定义一个 segfault 处理器,来截获因访问该不可读内存而触发 segfault 的线程,并将它们挂起。
第二,即时编译器生成的机器码打乱了原本栈桢上的对象分布状况。在进入安全点时,机器码还需提供一些额外的信息,来表明哪些寄存器,或者当前栈帧上的哪些内存空间存放着指向对象的引用,以便垃圾回收器能够枚举 GC Roots。由于这些信息需要不少空间来存储,因此即时编译器会尽量避免过多的安全点检测。
不过,不同的即时编译器插入安全点检测的位置也可能不同。以 Graal 为例,除了上述位置外,它还会在计数循环的循环回边处插入安全点检测。其他的虚拟机也可能选取方法入口而非方法出口来插入安全点检测。
内存溢出分类
除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError,简单总结如下:
- 堆内存不足是最常见的 OOM 原因之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小;或者出现 JVM 处理引用不及时,导致堆积起来,内存无法释放等。
- 而对于 Java 虚拟机栈和本地方法栈,这里要稍微复杂一点。如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
- 对于老版本的 Oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 Intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。
- 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。
- 直接内存不足,也会导致 OOM
jvm配置参数
下面只列举其中的几个常用和容易掌握的配置选项
-Xms |
初始堆大小。如:-Xms256m |
-Xmx |
最大堆大小。如:-Xmx512m |
-Xmn |
新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss |
JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio |
新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio |
新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize |
永久代(方法区)的初始大小 |
-XX:MaxPermSize |
永久代(方法区)的最大值 |
-XX:+PrintGCDetails |
打印 GC 信息 |
-XX:+HeapDumpOnOutOfMemoryError |
让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用 |
堆外内存
DirectByteBuffer
https://www.jianshu.com/p/e8ec0f4a8f13
ByteBuffer.allocateDirect(int capacity) 进行构造。
NIO 的 Buffer 除了做了缓冲块优化之外,还提供了一个可以直接访问物理内存的类 DirectBuffer。普通的 Buffer 分配的是 JVM 堆内存,而 DirectBuffer 是直接分配物理内存 (非堆内存)。我们知道数据要输出到外部设备,必须先从用户空间复制到内核空间,再复制到输出设备,而在 Java 中,在用户空间中又存在一个拷贝,那就是从 Java 堆内存中拷贝到临时的直接内存中,通过临时的直接内存拷贝到内存空间中去。此时的直接内存和堆内存都是属于用户空间。
为什么 Java 需要通过一个临时的非堆内存来复制数据呢?如果单纯使用 Java 堆内存进行数据拷贝,当拷贝的数据量比较大的情况下,Java 堆的 GC 压力会比较大,而使用非堆内存可以减低 GC 的压力。DirectBuffer 则是直接将步骤简化为数据直接保存到非堆内存,从而减少了一次数据拷贝。
也就是堆外的本地内存。由于DirectByteBuffer申请的是非 JVM 的物理内存,所以并不是直接由 JVM 负责垃圾回收,但在 DirectBuffer 包装类被回收时,会通过 Java Reference 机制来释放该内存块,其实就是通过虚引用的通知机制来回收非JVM的内存的。用的时候就像使用其他IO一样。
堆外内存创建和销毁的代价很高,一般会创建一个堆外内存池回收使用。
MappedByteBuffer
而在 NIO 中,还存在另外一个 Buffer 类:MappedByteBuffer,跟 DirectBuffer 不同的是,MappedByteBuffer 是通过本地类调用 mmap 进行文件内存映射的,map() 系统调用方法会直接将文件从硬盘拷贝到用户空间,只进行一次数据拷贝,从而减少了传统的 read() 方法从硬盘拷贝到内核空间这一步。
我们可以使用FileChannel.map创建 MappedByteBuffer。