JVM 参数设置(JDK1.6)

前言

本文是对JVM flag系列文章的翻译和精简

  • JDK 作者是基于JDK6的,本人为JDK8.
  • 示例代码 命令行以$开头的为复制原作者,*λ *为本人实测

一 纯解释、纯编译还是混合

版本信息

-version

λ java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)

打印版本信息

-showversion

加强版的version,打印版本信息并执行后续任务

λ java -showversion -jar myjar.jar
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.4.2.RELEASE)

解释、编译还是混合?

-Xint

强制bytecode在解释模式运行,可以节省一点点编译时间但是运行极其缓慢(至少10倍)

-Xcomp

强制bytecode编译为原生代码后运行,编译时间稍长,但是运行效率有部分提升.最大的缺点是会导致JIT无法生效(JIT需要在运行时发现热点(hotspot)代码,并执行大量优化,JIT是hotspot vm的关键,也正是JIT的各种神优化才使java慢慢脱离了运行缓慢的非议)

-Xmixed

hotspot的默认配置, 对于热点代码优化,对于极少运行的代码以解释方式运行,在编译时间和运行效率取得平衡


二 标志分类和详细编译打印

JVM的标志分类

-abc

JVM标准标志.基本不会发生改变 如 -server -version

-Xabc

JVM实验性质标志,未来可能发生改变,虽然没有保障但仍然较为稳定,使用 java -X显示标志列表(会有遗漏)

λ java -X
    -Xmixed           混合模式执行 (默认)
    -Xint             仅解释模式执行
    -Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>
                      设置搜索路径以引导类和资源
    -Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>
                      附加在引导类路径末尾
    -Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>
                      置于引导类路径之前
    -Xdiag            显示附加诊断消息
    -Xnoclassgc       禁用类垃圾收集
    -Xincgc           启用增量垃圾收集
    -Xloggc:    将 GC 状态记录在文件中 (带时间戳)
    -Xbatch           禁用后台编译
    -Xms        设置初始 Java 堆大小
    -Xmx        设置最大 Java 堆大小
    -Xss        设置 Java 线程堆栈大小
    -Xprof            输出 cpu 配置文件数据
    -Xfuture          启用最严格的检查, 预期将来的默认值
    -Xrs              减少 Java/VM 对操作系统信号的使用 (请参阅文档)
    -Xcheck:jni       对 JNI 函数执行其他检查
    -Xshare:off       不尝试使用共享类数据
    -Xshare:auto      在可能的情况下使用共享类数据 (默认)
    -Xshare:on        要求使用共享类数据, 否则将失败。
    -XshowSettings    显示所有设置并继续
    -XshowSettings:all
                      显示所有设置并继续
    -XshowSettings:vm 显示所有与 vm 相关的设置并继续
    -XshowSettings:properties
                      显示所有属性设置并继续
    -XshowSettings:locale
                      显示所有与区域设置相关的设置并继续

-X 选项是非标准选项, 如有更改, 恕不另行通知。

-XXabc

JVM实验性质标志,未来很可能发生改变,不太稳定,但很少变化

-XX 的特殊语法

-XX:+

仅对boolean标志,开启选项,相应的-XX:-关闭选项

-XX:=

设置选项

显示JIT编译信息

-XX:+PrintCompilation

在程序运行时显示JIT的编译信息

$ java -server -XX:+PrintCompilation Benchmark
  1       java.lang.String::hashCode (64 bytes)
  2       java.lang.AbstractStringBuilder::stringSizeOfInt (21 bytes)
  3       java.lang.Integer::getChars (131 bytes)
  4       java.lang.Object:: (1 bytes)
---   n   java.lang.System::arraycopy (static)
  5       java.util.HashMap::indexFor (6 bytes)
  6       java.lang.Math::min (11 bytes)
  7       java.lang.String::getChars (66 bytes)
  8       java.lang.AbstractStringBuilder::append (60 bytes)
...

-XX:+CITime

在程序运行时显示JIT编译时间

$ java -server -XX:+CITime Benchmark
[...]
Accumulated compiler times (for compiled methods only)
------------------------------------------------
  Total compilation time   :  0.178 s
    Standard compilation   :  0.129 s, Average : 0.004
    On stack replacement   :  0.049 s, Average : 0.024
[...]

比较编译时间

拿公司的jar跑的,没跑起来,结果并不准确

  1. -Xcomp
Accumulated compiler times (for compiled methods only)
------------------------------------------------
  Total compilation time   : 45.480 s
    Standard compilation   : 44.531 s, Average : 0.001
    On stack replacement   :  0.949 s, Average : 0.045
    Detailed C1 Timings
       Setup time:         0.000 s ( 0.0%)
       Build IR:           3.282 s (37.8%)
         Optimize:            0.186 s ( 2.1%)
         RCE:                 0.038 s ( 0.4%)
       Emit LIR:           2.887 s (33.2%)
         LIR Gen:           0.633 s ( 7.3%)
         Linear Scan:       2.226 s (25.6%)
       LIR Schedule:       0.000 s ( 0.0%)
       Code Emission:      0.974 s (11.2%)
       Code Installation:  1.543 s (17.8%)
       Instruction Nodes: 2332355 nodes
  1. -Xint
Accumulated compiler times (for compiled methods only)
------------------------------------------------
  Total compilation time   :  0.000 s
    Standard compilation   :  0.000 s, Average : -1.#IO
    On stack replacement   :  0.000 s, Average : -1.#IO

  Total compiled methods   :      0 methods
    Standard compilation   :      0 methods
    On stack replacement   :      0 methods
  Total compiled bytecodes :      0 bytes
    Standard compilation   :      0 bytes
    On stack replacement   :      0 bytes
  Average compilation speed: -2147483648 bytes/s

  nmethod code size        :      0 bytes
  nmethod total size       :      0 bytes

3 -Xmixed

Accumulated compiler times (for compiled methods only)
------------------------------------------------
  Total compilation time   : 18.903 s
    Standard compilation   : 17.893 s, Average : 0.004
    On stack replacement   :  1.011 s, Average : 0.014
    Detailed C1 Timings
       Setup time:         0.000 s ( 0.0%)
       Build IR:           0.554 s (43.9%)
         Optimize:            0.033 s ( 2.6%)
         RCE:                 0.007 s ( 0.6%)
       Emit LIR:           0.438 s (34.7%)
         LIR Gen:           0.095 s ( 7.5%)
         Linear Scan:       0.339 s (26.8%)
       LIR Schedule:       0.000 s ( 0.0%)
       Code Emission:      0.141 s (11.2%)
       Code Installation:  0.128 s (10.2%)
       Instruction Nodes: 345057 nodes

  Total compiled methods   :   4749 methods
    Standard compilation   :   4679 methods
    On stack replacement   :     70 methods
  Total compiled bytecodes : 959126 bytes
    Standard compilation   : 921785 bytes
    On stack replacement   :  37341 bytes
  Average compilation speed:  50738 bytes/s

  nmethod code size        : 8556928 bytes
  nmethod total size       : 16044616 bytes

可以看出-Xint完全没有编译,即使结果不准确也很容易理解-Xmixed模式的编译时间会比-Xcomp短

解锁JVM 实验性质标志

-XX:+UnlockExperimentalVMOptions

解锁JVM实验性质标志.
如果你设置了某个-XX标志,但是程序启动后立刻输出Unrecognized VM option,一般是拼写错误.如果Error: VM option 'Xxxx' is experience and must be enabled via -XX:+UnlockXXXVMOptions.是这可能是JVM的安全策略,类似于二次确认,某些标志可能会带来意向不到的问题比如生成大量日志,所以在遇到这个问题后最好再去确定下标志的作用,比如下面这个例子

λ  java  -XX:+LogCompilation -jar myjar.jar
Error: VM option 'LogCompilation' is diagnostic and must be enabled via -XX:+UnlockDiagnosticVMOptions.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

另外有些标志是为jvm开发人员提供的,需要debug build的jvm才能使用,如果在stable build中使用会有相应的错误提示

更详细的编译信息

-XX:+LogCompilation

将更详细的编译信息(编译线程及编译的方法)保存到hotspot.log文件,需要添加UnockDiagnostic标志

-XX:+PrintOptoAssembly

显示并保存编译出的native code到hotspot.log
在debug build才能使用

λ  java    -XX:+PrintOptoAssembly -jar myjar.jar
Error: VM option 'PrintOptoAssembly' is notproduct and is available only in debug version of VM.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

打印-XX标志

-XX:+PrintFlagsFinal

打印所有生效的-XX标志,如果没有手动设置会显示默认设置

λ  java    -XX:+PrintFlagsFinal -jar myjar.jar
[Global flags]
    uintx AdaptiveSizeThroughPutPolicy              = 0                                   {product}
    uintx AdaptiveTimeWeight                        = 25                                  {product}
     bool AdjustConcurrency                         = false                               {product}
     bool AggressiveOpts                            = false                               {product}
     intx AliasLevel                                = 3                                   {C2 product}
     bool AlignVector                               = true                                {C2 product}
     intx AllocateInstancePrefetchLines             = 1                                   {product}
     intx AllocatePrefetchDistance                  = 256                                 {product}
     intx AllocatePrefetchInstr                     = 3                                   {product}
     intx AllocatePrefetchLines                     = 3                                   {product}
     intx AllocatePrefetchStepSize                  = 64                                  {product}
     intx AllocatePrefetchStyle                     = 1                                   {product}
     bool AllowJNIEnvProxy                          = false                               {product}

格式为

值类型 名称 [:]= 值 环境类型

带有:表示被手动覆盖(JVM智能分析或用户手动设置), 只有=为默认值

-XX:+PrintFlagsInitial

与上面类似,不过即使手动设置了也只显示默认值,

额外显示实现和分析相关标志

添加 -XX:+UnlockExperimentalVMOptions 和 -XX:+UnlockDiagnosticVMOptions

  • 对于 Final,未添加时为723,添加后为867
  • 对于Initial没有影响

统计文件行数可使用 wc -l filename

只显示手动设置标志

-XX:+PrintCommandLineFlags

只显示用户设置或JVM智能设置的标志(也就是上面的 :=)

λ  java    -XX:+PrintCommandLineFlags   myjar.jar
-XX:InitialHeapSize=266699648 -XX:MaxHeapSize=4267194368 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

(这里作者推荐将这个选项添加到jvm默认配置里,这样有利于jvm调优)


四 内存调优

JVM中共享内存分为堆区和方法区
对于G1之前的garbage collector

  1. 堆区按照如下方式划分
  • 新生代,包含新创建/朝生夕死的对象
  • 老年代,经历过一定次数GC仍然存活的新生代对象会移动到老年代
  1. 方法区也称,永久代,包含类信息,常量池等

对于G1,永久代被移除,变为metaSpace区,metaSpace区不再占用JVM中的内存,而是使用本地堆内存(native heap),常量池也移动到堆区(JDK7),新生代和老年代的概念保留,但是已经没有物理界限

内存设置

-Xms128(k|m|g) (or: -XX:InitialHeapSize)

设置堆内存大小为128k/m/g,ms(memory size) 总内存大小,一般表示堆内存下限

-Xms128(k|m|g) (or:-XX:MaxHeapSize)

堆内存上限,JVM的运行中会动态调整总内存,但是确保不会超过上限

值得注意的是如果要查看启动时设置的堆内存,应该查找InitialHeapSize和MaxHeapSize这两个字段,而非Xms或Xmx

堆内存溢出自动保存heap dump文件

-XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath

堆内存溢出时,默认在jvm启动目录生成dump文件,通过设置-XX:+HeapDumpPath可自定义保存位置,文件名为java_pid.hprof

由于堆内存溢出时dump文件通常会很大,所以推荐设置自定义保存位置

堆内存溢出时执行自定命令

-XX:OnOutOfMemoryError

使用示例如下

$ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp

设置永久代内存(仅对G1之前的)

-XXPermSize -XX:MaxPermSize

设置永久代内存大小,永久代是不占用堆内存的,堆内存和永久代内存是不相关的,但是这两块内存都是由JVM管理的,而JDK8的metaSpace使用的direct memory是由计算机管理而非JVM

native code缓存区

JVM有一块内存区域用来保存编译后的native code,如果这块内存满了,不会再有任何编译行为发生,也就是说JIT被停止了,现有的未编译代码会以解释模式执行,这会导致严重的性能问题

-XX:InitialCodeCacheSize -XX:ReservedCodeCacheSize

设置初始代码缓存区大小 设置最大代码缓存区大小

-XX:+UseCodeCacheFlushing

在缓存区满时释放部分缓存,可以避免缓存区满时剩余代码全部以解释模式运行的问题


五 新生代GC

新生代采用复制算法(参见另一篇文章)
新生代分为1个eden和两个survivor,两个survivor只会使用一个,每次eden区快满时会发生一次minor gc,将eden和正在使用的survivor中存活的对象复制到另一个存活的survivor中.如果一个对象存活超过一定次数minor gc就会晋升到老年代

JVM 参数设置(JDK1.6)_第1张图片
新生代GC

minor gc vs major gc vs full gc

我对这三种gc也是傻傻分不清,网上查到的资料也不是太全,下面根据自己的理解分析一下,如果错误请指正,下面只是内存快满导致的gc:

  • minor gc 新生代快满时会触发minor gc,具体流程如上
  • major gc 在触发minor gc后,会有部分对象晋升到老年代,如果这时老年代快满则会触发major gc.
  • full gc 对于jdk8之前,如果永久代快满,会导致full gc

设置新生代内存

-XX:NewSize -XX:MaxNewSize

类似于-Xms-Xmx,因为堆内存由新生代和老年代组成,推荐两者的最大内存相同

设置新生代和老年代的内存占比

-XX:NewRatio

//新生代:老年代=1:3,堆内存新生代占1/4,老年代占3/4
-XX:NewRatio=3

如果同时设置了固定范围和比率,固定范围拥有更高的优先级,考虑如下配置

$ java -XX:NewSize=32m -XX:MaxNewSize=512m -XX:NewRatio=3 MyApp

初始时JVM会尽量设计新生代和老年代的比例为1:3,但是无论如何新生代内存大于32m小于512m

设计eden和survivor的内存占比

-XX:SurvivorRatio

//eden:survivor=10:1,由于survivor是有两块的,所有新生代的内存eden占比10/12,两个survivor各占1/12
-XX:SurvivorRatio=10

eden和survivor的失衡可能会导致如下两种我们不期望发生的现象

  1. eden很大
    由于eden很大,可以容纳很多对象,minorGC的频率就会降低,如果分配的对象大都是朝生夕死的,进行minorGC时虽然survivor很小但仍然可以容纳存活下来的少数对象,这是理想状态,但是如果大多数对象不是朝生夕死的,由于survivor不能容纳这么多存活对象,这些对象只能被移动到老年代,且不论移动过程的耗费,这还会大大提高majorGC发生的几率,而majorGC的耗费远高于minorGC.
  2. eden很小
    只能容纳少量对象,minorGC发生的几率会增大.

survivor和晋升年龄调优

-XX:+PrintTenuringDistribution

打印survivor中对象,年龄信息及晋升年龄

Desired survivor size 75497472 bytes, new threshold 15 (max 15)
- age   1:   19321624 bytes,   19321624 total
- age   2:      79376 bytes,   19401000 total
- age   3:    2904256 bytes,   22305256 total

以上信息为:survivor的期望容量(并非survivor总容量)约75MB,当前晋升年龄为15(当对象年龄大于改阈值就会晋升到老年代),最大晋升年龄也是15

- age   1:   19321624 bytes,   19321624 total

年龄为1的对象约为19MB,小于等于该年龄的对象约为19MB
现在假设进行了一个minorGC,GC后如下

Desired survivor size 75497472 bytes, new threshold 2 (max 15)
- age   1:   68407384 bytes,   68407384 total
- age   2:   12494576 bytes,   80901960 total
- age   3:      79376 bytes,   80981336 total
- age   4:    2904256 bytes,   83885592 total

可以看到年龄为4的对象约为3M,和GC前年龄为3的大小相同,说明之前年龄为3的对象全部存活,年龄为3的同理.
同时目前的survivor实际总容量约为83MB,超过了survivor的期望容量,因此当前晋升年龄调整为2,下次GC时年龄大于2的对象将晋升到老年代.

survivor相关设置

-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold

初始晋升年龄和最大晋升年龄

-XX:TargetSurvivorRatio

设置survivor的期望容量占survivor总容量的比例,如-XX:TargetSurvivorRatio=90说明survivor期望容量为总容量的90%

仅在测试时使用的两个标志

-XX:+NeverTenure -XX:+AlwaysTenure

顾名思义 永不晋升和总是晋升


六 更高的吞吐还是更短的停顿?

GC算法是否优越,主要由下面两个要素决定

  1. 高吞吐
  2. 低停顿

GC由几个专用的GC线程负责,发生GC时,GC线程会与业务线程争抢时间片,因此cpu不可能总是在执行业务线程,总有一部分时间是归gc线程的.

吞吐量

业务线程执行总时间占cpu运行总时间的比例就是吞吐量,例如cpu运行了100s,在此期间业务线程运行了99s.那吞吐量就是99%.

停顿时间

gc时会暂停所有业务线程,具体原因参见Stop the world.这里业务线程的暂停时间就是停顿时间.

吞吐vs停顿

鱼与熊掌不可兼得,高吞吐和低停顿是两个相反的目标:

  • 高吞吐要求gc尽量少
  • 低延迟要求gc尽量多
    值得注意的是,通过并发设计(并发和并行是有区别的,推荐大家研究一下,我对这两个概念还是有点分不清,等讲究清楚再分享),G1收集器正在尽量达成高吞吐和低停顿.

高吞吐的设计

gc期间需要确保堆中对象的状态不能发生变化,因此工作线程在gc期间必须停止,也称为stop the world.另外

  • gc前要进行一系列预备工作
  • gc线程切换需要耗费资源
    gc最好在无法避免是才进行比如堆中无法容纳更多对象时,相比高频次的gc,节省了大量预备工作时间和线程切换时间.并以此达到高吞吐.

低延迟的设计

上面高吞吐的设计,因为堆中有大量对象需要处理,停顿时间不可避免的很长,为了达到低延迟,gc要尽可能的频繁,这样每次只需要回收少量对象,停顿时间也会大大减少.

面向高吞吐

下面几个标志都是指定gc收集器,具体参见gc收集器

  • -XX:+UseSerialGC 新生代和老年代都使用单线程收集器
  • -XX:+UseParallelGC JDK1.6表示新生代使用多线程收集器,在JDK1.7中作用与-XX:+UseParallelOldGC相同
  • -XX:+UseParallelOldGC 新生代和老年代都是用多线程收集器

设置parallelGC线程数量

-XX:ParallelGCThreads

设置ParallelGC线程数量,如果没有设置会按照以下规则进行(JDK1.6),获取cpu核心数N

  • 如果N<=8 GC线程数就为8
  • 如果N>8 GC线程数为 3+5N/8

关闭JVM智能调整功能

-XX:-UseAdaptiveSizePolicy

JDK1.5为JVM引入了称为ergonomics的机制,在运行时,如果有证明修改某些设置能够提高性能,ergonomics就会进行动态调整.它是默认开启的,个人认为一般没有理由去关闭它.

吞吐量设置

-XX:GCTimeRatio

-XX:GCTimeRatio=9表示工作时间占总运行时间的9/10.默认为99,即吞吐量为99%

最大停顿毫秒设置

-XX:MaxGCPauseMillis

也是根据此值在运行是进行分析并调整GC设置.对于新生代和老年代是分别处理的.并且最大停顿比吞吐量有更高的优先级


七 CMS收集器

CMS简介

CMS设置

-XX:+UseConcMarkSweepGC

指定老年代使用CMS

-XX:+UseParNewGC

指定新生代为并行收集,当设置了-XX:+UseConcMarkSweepGC后自动生效,记得之前的-XX:+UseParallelGC吗,为什么不复用这个标志呢?对于CMS,新生代的并行gc采用了另一种实现,因此要用一个新的标志

-XX:+CMSConcurrentMTEnabled

CMS的各个阶段使用多线程并行处理

-XX:ConcGCThreads

并发处理线程数,设置为4时表示CMS的每个阶段都使用4个线程.默认为** (ParallelGCThreads + 3)/4**

-XX:CMSInitiatingOccupancyFraction

GC阈值,当内存占用超过该点时触发gc.对于CMS不能等到内存满了再去GC,因为GC期间业务线程也是在运行的,会进行内存分配.

-XX+UseCMSInitiatingOccupancyOnly

强制使用-XX:CMSInitiatingOccupancyFraction指定的GC阈值,不再由JVM智能推断.

-XX:+CMSClassUnloadingEnabled

开启永久代收集.默认只会在永久代满时才会使用清理,并且不是并行的.

-XX:+CMSIncrementalMode

GC是定期停止gc线程,对于现在的机器意义不大.

System.gc()的优化

-XX:+ExplicitGCInvokesConcurrent
调用System.gc()后使用CMS的GC而非full gc.

XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
与上面类似,额外增加对永久代的gc.

关闭System.gc()

-XX:+DisableExplicitGC

作者建议这个标志值得添加.


八 GC日志

gc日志并不复杂,简要说明一下

-XX:+PrintGC

打印简要GC日志

//[GC类型,GC前内存占用->GC后占用(堆内存),GC消耗的真实时间]
[GC 246656K->243120K(376320K), 0,0929090 secs]
[Full GC 243120K->241951K(629760K), 1,5589690 secs]

-XX:+PrintGCDetails

打印详细日志

[GC
    [PSYoungGen: 142816K->10752K(142848K)] 246648K->243136K(375296K),
    0,0935090 secs
]
//user表示所有GC线程消耗的总时间, sys为系统态时间,应该是GC时上下文切换等的额外消耗,real为消耗的真实时间
//
[Times: user=0,55 sys=0,10, real=0,09 secs]

更详细的日志

[Full GC
    [PSYoungGen: 10752K->9707K(142848K)]
    //                                                                      表示堆内存变化
    [ParOldGen: 232384K->232244K(485888K)] 243136K->241951K(628736K)
    //永久代是不算在堆内存的
    [PSPermGen: 3162K->3161K(21504K)],
    1,5265450 secs
]
[Times: user=10,96 sys=0,06, real=1,53 secs]

根据每代gc前后的占用内存变化可以推断到底是哪一代引发的gc,像上面的例子,三代基本都没变化,那么有可能是ergonomics 引起的(即使占用未满,ergonomics也可能主动引发一次gc).对于System.gc()引发的GC,GCtype会是Full GC (System)

添加时间信息

-XX:+PrintGCTimeStamps and -XX:+PrintGCDateStamps

如下

//-XX:+PrintGCTimeStamps
0,185: [GC 66048K->53077K(251392K), 0,0977580 secs]
//-XX:+PrintGCDateStamps
2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0,0959470 secs]

把GC日志记录到文件中

-Xloggc

-Xloggc:,把gc日志记录到制定文件中,-XX:+PrintGC and -XX:+PrintGCTimeStamps会被隐式使用


结语

虽然没有看过JVM的源码,单从这些设置就能看出实现是多么复杂.

未来的路还很长.

你可能感兴趣的:(JVM 参数设置(JDK1.6))