一、调优命令 Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo 。 1.1、jps JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 1.2、jstat JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令，它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 1、基本语法： # 基本格式 jstat - <vmid> [interval] [count] # 常用示例 jstat -gc 1234 1000 10 # 每秒输出进程号1234的GC信息，共输出10次 2、查看类加载统计： # 查看类加载信息 jstat -class <pid> # 输出示例： Loaded Bytes Unloaded Bytes Time 7035 14506 0 0 3.67 # 字段说明： Loaded : 加载class的数量 Bytes : 加载class的字节数 Unloaded : 未加载class的数量 Bytes : 未加载class的字节数 Time : 加载时间 3、查看GC统计： # 查看GC统计信息 jstat -gc <pid> # 输出示例： S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 1024.0 1024.0 0.0 0.0 8192.0 8171.9 20480.0 20480.0 4864.0 3720.0 512.0 324.8 2 0.002 1 0.003 0.005 # 字段说明： S0C: Survivor0区容量 S1C: Survivor1区容量 S0U: Survivor0区已使用 S1U: Survivor1区已使用 EC : Eden区容量 EU : Eden区已使用 OC : 老年代容量 OU : 老年代已使用 MC : 方法区容量 MU : 方法区已使用 YGC: 年轻代GC次数 YGCT: 年轻代GC时间 FGC: Full GC次数 FGCT: Full GC时间 GCT: 总GC时间 4、查看GC详细信息： # 查看新生代GC统计 jstat -gcnew <pid> # 输出示例： S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT 1024.0 1024.0 0.0 1024.0 1 15 1024.0 8192.0 7623.5 2 0.002 # 字段说明： TT : Tenuring threshold（晋升阈值） MTT : Maximum tenuring threshold（最大晋升阈值） DSS : Desired survivor size（期望的survivor大小） 实际使用示例： # 监控内存使用情况 while true; do # 每5秒输出一次GC信息 jstat -gc $(jps | grep MyApp | awk '{print $1}') 5000 done # 输出到文件进行分析 jstat -gc $(jps | grep MyApp | awk '{print $1}') 1000 > gc.log 5、性能分析脚本： #!/bin/bash # gc_monitor.sh PID=$1 INTERVAL=${2:-1000} # 默认1000ms COUNT=${3:-""} # 默认持续输出 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [interval] [count]" exit 1 fi # 输出表头 echo "Time YGC YGCT FGC FGCT GCT" # 监控GC情况 jstat -gc $PID $INTERVAL $COUNT | awk ' NR>1 { printf "%s %d %.3f %d %.3f %.3f\n", strftime("%H:%M:%S"), $15, # YGC $16, # YGCT $17, # FGC $18, # FGCT $19 # GCT }' 6、内存分析脚本： #!/bin/bash # memory_monitor.sh PID=$1 INTERVAL=${2:-1000} COUNT=${3:-""} if [ -z "$PID" ]; then echo "Usage: $0 <pid> [interval] [count]" exit 1 fi # 输出表头 echo "Time Eden% Old% Meta%" # 监控内存使用率 jstat -gc $PID $INTERVAL $COUNT | awk ' NR>1 { eden_used=$6 eden_capacity=$5 old_used=$8 old_capacity=$7 meta_used=$10 meta_capacity=$9 printf "%s %.1f%% %.1f%% %.1f%%\n", strftime("%H:%M:%S"), eden_used/eden_capacity*100, old_used/old_capacity*100, meta_used/meta_capacity*100 }' 7、常见用法： # 1. 查看类加载情况 jstat -class $(jps | grep MyApp | awk '{print $1}') # 2. 查看GC情况 jstat -gc $(jps | grep MyApp | awk '{print $1}') # 3. 查看JIT编译情况 jstat -compiler $(jps | grep MyApp | awk '{print $1}') # 4. 持续监控 jstat -gcutil $(jps | grep MyApp | awk '{print $1}') 1000 # 5. 输出到文件 jstat -gc $(jps | grep MyApp | awk '{print $1}') 1000 > gc.log 1.3、jmap 1、基本语法： # 基本格式 jmap [options] <pid> # 常用选项 -heap # 显示堆内存信息 -histo # 显示对象统计信息 -dump # 生成堆转储文件 查看堆内存信息： # 查看堆内存概况 jmap -heap <pid> # 输出示例： Attaching to process ID 1234, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.271-b09 using thread-local object allocation. Parallel GC with 4 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 2147483648 (2048.0MB) NewSize = 715653120 (682.5MB) MaxNewSize = 715653120 (682.5MB) OldSize = 1431830528 (1365.5MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.8MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB) Heap Usage: PS Young Generation Eden Space: capacity = 679477248 (648.0MB) used = 424089480 (404.44MB) free = 255387768 (243.56MB) 62.41% used From Space: capacity = 18087936 (17.25MB) used = 0 (0.0MB) free = 18087936 (17.25MB) 0.0% used To Space: capacity = 18087936 (17.25MB) used = 0 (0.0MB) free = 18087936 (17.25MB) 0.0% used PS Old Generation capacity = 1431830528 (1365.5MB) used = 474547616 (452.56MB) free = 957282912 (912.94MB) 33.14% used 2、生成堆转储文件： # 生成堆转储文件 jmap -dump:format=b,file=heap.hprof <pid> # 在发生OOM时自动生成堆转储文件 jmap -dump:live,format=b,file=heap.hprof <pid> # 使用示例： public class HeapDumpDemo { public static void main(String[] args) { List<byte[]> list = new ArrayList<>(); while (true) { list.add(new byte[1024 * 1024]); // 1MB Thread.sleep(100); } } } 3、查看对象统计信息： # 显示堆中对象统计信息 jmap -histo <pid> # 只显示存活对象 jmap -histo:live <pid> # 输出示例： num #instances #bytes class name ---------------------------------------------- 1: 500000 20000000 java.lang.String 2: 30000 15000000 byte[] 3: 1000 800000 java.lang.Class 4、实际应用脚本： #!/bin/bash # heap_monitor.sh PID=$1 DUMP_PATH=${2:-"./dumps"} INTERVAL=${3:-3600} # 默认每小时 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [dump_path] [interval]" exit 1 fi mkdir -p $DUMP_PATH while true; do DATE=$(date +%Y%m%d_%H%M%S) DUMP_FILE="$DUMP_PATH/heap_$DATE.hprof" # 生成堆转储文件 jmap -dump:live,format=b,file=$DUMP_FILE $PID # 生成对象统计信息 jmap -histo:live $PID > "$DUMP_PATH/histo_$DATE.txt" sleep $INTERVAL done 5、内存泄漏分析： #!/bin/bash # leak_analyzer.sh PID=$1 THRESHOLD=${2:-90} # 默认内存使用率阈值 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [threshold]" exit 1 fi while true; do # 获取堆使用率 HEAP_USAGE=$(jmap -heap $PID | grep "used" | awk '{sum+=$3} END {print sum/1024/1024}') if [ $(echo "$HEAP_USAGE > $THRESHOLD" | bc) -eq 1 ]; then echo "Memory usage above threshold: $HEAP_USAGE MB" DUMP_FILE="heap_$(date +%Y%m%d_%H%M%S).hprof" jmap -dump:live,format=b,file=$DUMP_FILE $PID fi sleep 60 done 6、常见问题分析： # 1. 查看最大的对象 jmap -histo:live <pid> | sort -k 3 -n -r | head -n 20 # 2. 检查字符串常量池 jmap -clstats <pid> # 3. 分析内存泄漏 jmap -dump:live,format=b,file=leak.hprof <pid> # 4. 查看类加载情况 jmap -clstats <pid> 7、性能优化建议： # 1. 定期监控 0 * * * * /path/to/heap_monitor.sh <pid> # 2. 设置告警 if [ $(jmap -heap <pid> | grep "used" | awk '{print $3}') -gt 1000000000 ]; then echo "High memory usage alert!" fi # 3. 自动清理旧转储文件 find /path/to/dumps -name "heap_*.hprof" -mtime +7 -delete 1.4、jhat jhat 是一个用于分析 Java 堆转储文件的工具。它可以帮助开发者查看和分析 Java 应用程序的内存使用情况。 1、基本语法： jhat [options] <heap_dump_file> 2、生成堆转储文件： 在使用 jhat 之前，首先需要生成一个堆转储文件。可以使用 jmap 或 jcmd 命令来生成堆转储文件。 # 使用 jmap 生成堆转储文件 jmap -dump:live,format=b,file=heap_dump.hprof <pid> # 或者使用 jcmd jcmd <pid> GC.heap_dump filename=heap_dump.hprof 3、使用 jhat 分析堆转储文件： # 启动 jhat 分析堆转储文件 jhat heap_dump.hprof 4、jhat 启动后的输出： 当你运行 jhat 后，它会启动一个 HTTP 服务器，通常在 http://localhost:7000 上。你可以在浏览器中访问这个地址来查看分析结果。 5、jhat 的常用选项： -J<flag>: 传递参数给 JVM。例如，-J-Xmx512m 可以设置最大堆大小。 -port <port>: 指定 jhat 服务器的端口，默认是 7000。 -h 或 --help: 显示帮助信息。 6、示例： # 生成堆转储文件 jmap -dump:live,format=b,file=heap_dump.hprof <pid> # 使用 jhat 分析堆转储文件 jhat heap_dump.hprof # 访问分析结果 # 打开浏览器，访问 http://localhost:7000 7、分析结果： 在浏览器中，你将看到以下信息： Classes: 显示堆中所有类的实例数量和占用内存。 Instances: 显示每个类的实例详细信息。 Paths: 显示从根对象到特定对象的路径，帮助分析内存泄漏。 8、使用 jhat 的注意事项： 性能: jhat 在处理大型堆转储文件时可能会消耗大量内存和时间。 过时: jhat 在某些情况下可能不再是最佳选择，考虑使用更现代的工具，如 Eclipse Memory Analyzer (MAT) 或 VisualVM。 9、现代替代方案： Eclipse Memory Analyzer (MAT): 提供更强大的分析功能和图形界面。 VisualVM: 也可以分析堆转储文件，并提供实时监控功能。 1.5、jstack jstack 是一个用于生成 Java 线程堆栈跟踪的工具，通常用于分析 Java 应用程序的线程状态和死锁情况。 1、基本语法： jstack [options] <pid> 2、查看线程堆栈： # 查看指定进程的线程堆栈 jstack <pid> 3、示例： 假设你有一个 Java 应用程序正在运行，进程 ID 为 12345，你可以使用以下命令查看线程堆栈： jstack 12345 4、输出示例： jstack 的输出将显示所有线程的状态，包括线程名称、状态、锁信息等。例如： "main" #1 prio=5 os_prio=0 tid=0x00007f8c4c001800 nid=0x1b03 waiting for monitor entry [0x00007f8c4c7f2000] java.lang.Thread.State: BLOCKED (on object monitor) at java.lang.Object.wait(Native Method) - waiting to lock <0x00000000f8c4c8b0> (a java.lang.Object) at java.lang.Object.wait(Object.java:502) at java.lang.Thread.join(Thread.java:1252) at com.example.MyClass.main(MyClass.java:10) 5、生成线程转储文件 如果想将线程堆栈输出保存到文件中，可以使用重定向： jstack 12345 > thread_dump.txt 6、检查死锁 jstack 还可以帮助你检查死锁情况。你可以在输出中查找 Found one Java-level deadlock 的信息。 # 检查死锁 jstack -l <pid> 1.6、jinfo jinfo 是一个用于获取和修改 Java 进程的配置信息的工具。它可以帮助开发者查看 JVM 参数、系统属性以及其他运行时信息。 1、基本语法： # 基本格式 jinfo [option] <pid> # 常用选项 -flag <name> # 打印指定名称的参数 -flag [+|-]<name> # 启用/禁用指定名称的参数 -flag <name>=<value> # 设置指定参数的值 -flags # 打印所有参数 -sysprops # 打印系统属性 2、查看 JVM 参数： # 查看所有 JVM 参数 jinfo -flags <pid> # 查看特定参数 jinfo -flag MaxHeapSize <pid> jinfo -flag UseG1GC <pid> # 输出示例： -XX:MaxHeapSize=2147483648 -XX:+UseG1GC 3、查看系统属性： # 查看所有系统属性 jinfo -sysprops <pid> # 输出示例： java.runtime.name=Java(TM) SE Runtime Environment java.vm.version=25.271-b09 java.home=/path/to/java user.dir=/path/to/working/directory 4、修改运行时参数： 你可以使用 -flag 选项来修改 JVM 参数。例如，修改最大堆大小： # 启用/禁用布尔类型参数 jinfo -flag +PrintGC <pid> # 启用GC日志打印 jinfo -flag -PrintGC <pid> # 禁用GC日志打印 # 设置数值类型参数 jinfo -flag MaxHeapSize=2147483648 <pid> 5、实际应用示例： public class JInfoDemo { public static void main(String[] args) throws Exception { // 打印进程ID String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; System.out.println("Process ID: " + pid); // 保持程序运行 Thread.sleep(Integer.MAX_VALUE); } } 6、监控脚本： #!/bin/bash # jvm_monitor.sh PID=$1 INTERVAL=${2:-60} # 默认60秒 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [interval]" exit 1 fi while true; do echo "=== $(date) ===" # 打印堆内存使用情况 jinfo -flag MaxHeapSize $PID jinfo -flag HeapSize $PID # 打印GC相关参数 jinfo -flag UseG1GC $PID jinfo -flag UseParallelGC $PID sleep $INTERVAL done 7、参数检查脚本： #!/bin/bash # check_params.sh PID=$1 PARAM_FILE="jvm_params.txt" if [ -z "$PID" ]; then echo "Usage: $0 <pid>" exit 1 fi # 保存当前参数 jinfo -flags $PID > $PARAM_FILE # 检查关键参数 echo "Checking critical parameters..." # 检查堆大小 MAX_HEAP=$(grep "MaxHeapSize" $PARAM_FILE | awk '{print $2}') if [ $MAX_HEAP -lt 2147483648 ]; then # 2GB echo "Warning: MaxHeapSize is less than 2GB" fi # 检查GC参数 if ! grep -q "UseG1GC" $PARAM_FILE; then echo "Warning: G1GC is not enabled" fi 8、常见用法： # 1. 查看基本信息 jinfo <pid> # 2. 查看所有标志 jinfo -flags <pid> # 3. 查看特定标志 jinfo -flag UseG1GC <pid> # 4. 修改标志 jinfo -flag +PrintGC <pid> # 5. 查看系统属性 jinfo -sysprops <pid> 二、调优工具 常用调优工具分为两类，jdk 自带监控工具：jconsole 和 jvisualvm，第三方有：MAT (Memory Analyzer Tool)、GChisto。 1、jconsole，Java Monitoring and Management Console 是从 java5 开始，在 JDK 中自带的 java 监控和管理控制台，用于对 JVM 中内存，线程和类等的监控 2、jvisualvm，jdk 自带全能工具，可以分析内存快照、线程快照；监控内存变化、GC 变化等。 3、MAT，Memory Analyzer Tool，一个基于 Eclipse 的内存分析工具，是一个快速、功能丰富的 Javaheap 分析工具，它可以帮助我们查找内存泄漏和减少内存消耗。 4、GChisto，一款专业分析 gc 日志的工具
一、java application 项目（非 web 项目） 1.1、改进前 -Xms128m -Xmx128m -XX:NewSize=64m -XX:PermSize=64m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=78 -XX:ThreadStackSize=128 -Xloggc:logs/gc.log -Dsun.rmi.dgc.server.gcInterval=3600000 -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.server.exceptionTrace=true 问题: permsize 设置较小，很容易达到报警范围 (0.8)。 没有设置 MaxPermSize，堆增长会带来额外压力。 NewSize 较大，old gen 剩余空间 64m，一方面可能会带来 old 区容易增长到报警范围（监控数据显示 oldgenused 长期在 50m 左右，接近 78%，容易出现 full gc）, 另一方面也存在 promontion fail 风险。 1.2、改进后 -Xms128m -Xmx128m -Xmn24m -XX:PermSize=80m -XX:MaxPermSize=80m -Xss256k -XX:SurvivorRatio=1 -XX:MaxTenuringThreshold=20 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection -XX:+CMSParallelRemarkEnabled -XX:CMSFullGCsBeforeCompaction=2 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:logs/gc.log -Dsun.rmi.dgc.server.gcInterval=3600000 -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.server.exceptionTrace=true 修改点： PermSize 与 MaxPermSize 都设置为 80，一方面避免 non heap warn (报警阀值 0.8 非对内存一般占用到 60M 以内），一方面避免堆伸缩带来的压力 通过设置 Xmn=24M 及 SurvivorRatio=1 使得 Eden 区 = from space=to space=8M, 降低了 Eden 区大小，降低 YGC 的时间 (降低到 3-4ms 左右), 同时通过设 MaxTenuringThreshold=20，使得 old gen 的增长很缓慢。带来的问题是 YGC 的次数明显提高了很多。 其他参数优化 修改后带来的好处见 JVM 参数设置。 1.3、再次改进 -Xms128m -Xmx128m -Xmn36m -XX:PermSize=80m -XX:MaxPermSize=80m -Xss256k -XX:SurvivorRatio=1 -XX:MaxTenuringThreshold=20 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=73 -XX:+UseCMSCompactAtFullCollection -XX:+CMSParallelRemarkEnabled -XX:CMSFullGCsBeforeCompaction=2 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:logs/gc.log -Dsun.rmi.dgc.server.gcInterval=3600000 -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.server.exceptionTrace=true 修改点： 在上面的基础上调整 Xmn 大小到 36M，设置 CMSInitiatingOccupancyFraction=73。 Dden 区与 Survivor 区大小都增加到 12M，通过 CMSInitiatingOccupancyFraction 计算公式，计算得出 value 为 73 是，可以避免 promotion faild 问题，同时满足堆内存监控报警值在 80%：内存大小 128M80%=102.4M 102.4M-36M=66.4M (老生代达到此值报警） 老生代达到 67.15M（92M0.73）将发生 Full GC，所以在老生代大小达到 66.4M 时也就是 WARN 报警时将很有可能出现 Full GC。 增大了 Eden 和 Survivor 区的值，会减小 YGC 的次数，但由于空间变大理论上也会相应的增加 YGC 的时间，不过由于新生代本身就很小（才 36M）这点儿变化可以忽略掉。实际的监控值显示 YGC 的时间在 4-5ms 之间。是可以接受范围。 SurvivorRatio 这个值还得在仔细考虑下，有待优化中。 二、示例 $JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms6000M -Xmx6000M -Xmn500M -XX:PermSize=500M -XX:MaxPermSize=500M -XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0 -Xnoclassgc -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:-CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=90 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log "; 说明一下， -XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0 就是去掉了救助空间； -Xnoclassgc 禁用类垃圾回收，性能会高一点； -XX:+DisableExplicitGC 禁止 System.gc ()，免得程序员误调用 gc 方法影响性能； -XX:+UseParNewGC，对年轻代采用多线程并行回收，这样收得快； 带 CMS 参数的都是和并发回收相关的，不明白的可以上网搜索； CMSInitiatingOccupancyFraction，这个参数设置有很大技巧，基本上满足 (Xmx-Xmn)(100-CMSInitiatingOccupancyFraction)/100>=Xmn 就不会出现 promotion failed。在我的应用中 Xmx 是 6000，Xmn 是 500，那么 Xmx-Xmn 是 5500 兆，也就是年老代有 5500 兆，CMSInitiatingOccupancyFraction=90 说明年老代到 90% 满的时候开始执行对年老代的并发垃圾回收（CMS），这时还剩 10% 的空间是 550010%=550 兆，所以即使 Xmn（也就是年轻代共 500 兆）里所有对象都搬到年老代里，550 兆的空间也足够了，所以只要满足上面的公式，就不会出现垃圾回收时的 promotion failed； SoftRefLRUPolicyMSPerMB 这个参数我认为可能有点用，官方解释是 softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap，我觉得没必要等 1 秒； -Xmx4000M -Xms4000M -Xmn600M -XX:PermSize=500M -XX:MaxPermSize=500M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log 改进方案： 上面方法不太好，因为没有用到救助空间，所以年老代容易满，CMS 执行会比较频繁。我改善了一下，还是用救助空间，但是把救助空间加大，这样也不会有 promotion failed。 具体操作上，32 位 Linux 和 64 位 Linux 好像不一样，64 位系统似乎只要配置 MaxTenuringThreshold 参数，CMS 还是有暂停。为了解决暂停问题和 promotion failed 问题，最后我设置 - XX:SurvivorRatio=1 ，并把 MaxTenuringThreshold 去掉，这样即没有暂停又不会有 promotoin failed，而且更重要的是，年老代和永久代上升非常慢（因为好多对象到不了年老代就被回收了），所以 CMS 执行频率非常低，好几个小时才执行一次，这样，服务器都不用重启了。 参考示例： $JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xmx3000M -Xms3000M -Xmn600M -XX:PermSize=500M -XX:MaxPermSize=500M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log "; 64 位 jdk 参考设置，年老代涨得很慢，CMS 执行频率变小，CMS 没有停滞，也不会有 promotion failed 问题，内存回收得很干净。
一、概念 内存溢出：简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存，导致无法申请到足够的内存，于是就发生了内存溢出。 内存泄漏：内存泄漏指程序运行过程中分配内存给临时变量，用完之后却没有被 GC 回收，始终占用着内存，既不能被使用也不能分配给其他程序，于是就发生了内存泄漏。 内存溢出 out of memory，是指程序在申请内存时，没有足够的内存空间供其使用，出现 out of memory； 内存泄露 memory leak，是指程序在申请内存后，无法释放已申请的内存空间，一次内存泄露危害可以忽略，但内存泄露堆积后果很严重，无论多少内存，迟早会被占光。 memory leak 会最终会导致 out of memory！ 二、内存溢出的常见情况 2.1、java.lang.OutOfMemoryError: PermGen space (持久带溢出) JVM 通过持久带实现了 Java 虚拟机规范中的方法区，而运行时常量池就是保存在方法区中的，因此发生这种溢出可能是运行时常量池溢出，或是由于程序中使用了大量的 jar 或 class，使得方法区中保存的 class 对象没有被及时回收或者 class 信息占用的内存超过了配置的大小。 2.2、java.lang.OutOfMemoryError: Java heap space (堆溢出) 发生这种溢出的原因一般是创建的对象太多，在进行垃圾回收之前对象数量达到了最大堆的容量限制。 解决这个区域异常的方法一般是通过内存映像分析工具对 Dump 出来的堆转储快照进行分析，看到底是内存溢出还是内存泄漏。如果是内存泄漏，可进一步通过工具查看泄漏对象到 GC Roots 的引用链，定位出泄漏代码的位置，修改程序或算法；如果不存在泄漏，就是说内存中的对象确实都还必须存活，那就应该检查虚拟机的堆参数 -Xmx (最大堆大小) 和 -Xms (初始堆大小)，与机器物理内存对比看是否可以调大。 2.3、虚拟机栈和本地方法栈溢出 如果线程请求的栈深度大于虚拟机所允许的最大深度，将抛出 StackOverflowError。 如果虚拟机在扩展栈时无法申请到足够的内存空间，则抛出 OutOfMemoryError。 三、内存泄露 内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用，尽管短生命周期的对象已经不再需要，但由于长生命周期对象持有它的引用而导致不能被回收。 以发生的方式来分类，内存泄漏可以分为4类： 1、常发性内存泄漏。发生内存泄漏的代码会被多次执行到，每次被执行的时候都会导致一块内存泄漏。 2、偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境，偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 3、一次性内存泄漏。发生内存泄漏的代码只会被执行一次，或者由于算法上的缺陷，导致总会有一块仅且一块内存发生泄漏。比如，在类的构造函数中分配内存，在析构函数中却没有释放该内存，所以内存泄漏只会发生一次。 4、隐式内存泄漏。程序在运行过程中不停的分配内存，但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏，因为最终程序释放了所有申请的内存。但是对于一个服务器程序，需要运行几天，几周甚至几个月，不及时释放内存也可能导致最终耗尽系统的所有内存。所以，我们称这类内存泄漏为隐式内存泄漏。 从用户使用程序的角度来看，内存泄漏本身不会产生什么危害，作为一般的用户，根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积，这会最终消耗尽系统所有的内存。从这个角度来说，一次性内存泄漏并没有什么危害，因为它不会堆积，而隐式内存泄漏危害性则非常大，因为较之于常发性和偶发性内存泄漏它更难被检测到。 3.1、静态集合类引起的类存泄露 像 HashMap、Vector 等的使用最容易出现内存泄露，这些静态变量的生命周期和应用程序一致，他们所引用的所有的对象 Object 也不能被释放，从而造成内存泄漏，因为他们也将一直被 Vector 等引用着。 public class Test01 { public static void main(String[] args) { Vector<Object> v = new Vector<Object>(100); for (int i = 1; i < 100; i++) { Object o = new Object(); v.add(o); o = null; } } } 在这个例子中，循环申请 Object 对象，并将申请的对象放入一个 Vector 中，如果仅仅释放引用本身（o=null），那么 Vector 仍然引用该对象，所以这个对象对 GC 来说是不可回收的。因此，如果对象加入到 Vector 后，还必须从 Vector 中删除，最简单的方法就是将 Vector 对象设置为 null。 3.2、修改 HashSet 中对象的参数值，且参数时计算哈希值的字段 当一个对象被存储到 HashSet 集合中以后，修改了这个对象中那些参与计算哈希值的字段后，这个对象的哈希值与最初存储在集合中的就不同了，这种情况下，用 contains 方法在集合中检索对象是找不到的，这将会导致无法从 HashSet 中删除当前对象，造成内存泄漏，举例如下： public class Test02 { public static void main(String[] args) { Set<Person> set = new HashSet<Person>(); Person p1 = new Person("张三","1",25); Person p2 = new Person("李四","2",26); Person p3 = new Person("王五","3",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("总共有:"+set.size()+" 个元素!"); //结果：总共有:3 个元素! p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变 set.remove(p3); //此时remove不掉，造成内存泄漏 set.add(p3); //重新添加，可以添加成功 System.out.println("总共有:"+set.size()+" 个元素!"); //结果：总共有:4 个元素! for (Person person : set){ System.out.println(person); } } } //需要重写hashCode方法，默认的实现并不会有如下效果 public class Person { private String name; private String no; private int age; public Person(String name, String no, int age) { this.name = name; this.no = no; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; if (getAge() != person.getAge()) return false; if (getName() != null ? !getName().equals(person.getName()) : person.getName() != null) return false; return getNo() != null ? getNo().equals(person.getNo()) : person.getNo() == null; } @Override public int hashCode() { int result = getName() != null ? getName().hashCode() : 0; result = 31 * result + (getNo() != null ? getNo().hashCode() : 0); result = 31 * result + getAge(); return result; } } 3.3、监听器 在 Java 编程中，我们都需要和监听器打交道，通常一个应用当中会用到很多监听器，我们会调用一个控件的诸如 addXXXListener () 等方法来增加监听器，但往往在释放对象的时候却没有记住去删除这些监听器，从而增加了内存泄漏的机会。 3.4、各种连接 比如数据库连接（dataSourse.getConnection ()），网络连接 (socket) 和 io 连接，除非其显式的调用了其 close () 方法将其连接关闭，否则是不会自动被 GC 回收的。对于 Resultset 和 Statement 对象可以不进行显式回收，但 Connection 一定要显式回收，因为 Connection 在任何时候都无法自动回收，而 Connection 一旦回收，Resultset 和 Statement 对象就会立即为 NULL。但是如果使用连接池，情况就不一样了，除了要显式地关闭连接，还必须显式地关闭 Resultset Statement 对象（关闭其中一个，另外一个也会关闭），否则就会造成大量的 Statement 对象无法释放，从而引起内存泄漏。这种情况下一般都会在 try 里面去连接，在 finally 里面释放连接。 3.5、单例模式 如果单例对象持有外部对象的引用，那么这个外部对象将不能被 JVM 正常回收，导致内存泄露。 不正确使用单例模式是引起内存泄露的一个常见问题，单例对象在被初始化后将在 JVM 的整个生命周期中存在（以静态变量的方式），如果单例对象持有外部对象的引用，那么这个外部对象将不能被 JVM 正常回，导致内存泄露，考虑下面的例子： //B类采用单例模式 public class TestB { private TestA a; private static TestB instance = new TestB(); public TestB() { } public static TestB getInstance() { return instance; } public void setA(TestA a) { this.a = a; } } public class TestA { public TestA() { TestB.getInstance().setA(this); } } 显然 B 采用 singleton 模式，它持有一个 A 对象的引用，而这个 A 类的对象将不能被回收。想象下如果 A 是个比较复杂的对象或者集合类型会发生什么情况。 避免内存泄露的几点建议： 1、尽早释放无用对象的引用。 2、避免在循环中创建对象。 3、使用字符串处理时避免使用 String，应使用 StringBuffer。 4、尽量少使用静态变量，因为静态变量存放在永久代，基本不参与垃圾回收。 四、常见的几种内存溢出及解决方法 4.1、JVM Heap（堆）溢出：java.lang.OutOfMemoryError: Java heap space JVM 在启动的时候会自动设置 JVM Heap 的值， 可以利用 JVM 提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap 的大小是 Young Generation 和 Tenured Generaion 之和。在 JVM 中如果 98% 的时间是用于 GC, 且可用的 Heap size 不足 2% 的时候将抛出此异常信息。 解决方法：手动设置 JVM Heap（堆）的大小。 Java 堆用于储存对象实例。当需要为对象实例分配内存，而堆的内存占用又已经达到 -Xmx 设置的最大值。将会抛出 OutOfMemoryError 异常。 /** * VM Args: -Xms5m -Xmx5m */ public class HeapOOM { public static void main(String[] args) { int count = 0; List<Object> list = new ArrayList<Object>(); while(true){ list.add(new Object()); System.out.println(++count); } } } 4.2、PermGen space 溢出： java.lang.OutOfMemoryError: PermGen space PermGen space 的全称是 Permanent Generation space，是指内存的永久保存区域。为什么会内存溢出，这是由于这块内存主要是被 JVM 存放 Class 和 Meta 信息的，Class 在被 Load 的时候被放入 PermGen space 区域，它和存放 Instance 的 Heap 区域不同，sun 的 GC 不会在主程序运行期对 PermGen space 进行清理，所以如果你的 APP 会载入很多 CLASS 的话，就很可能出现 PermGen space 溢出。一般发生在程序的启动阶段。 解决方法： 通过 -XX:PermSize 和 -XX:MaxPermSize 设置永久代大小即可。 方法区用于存放 java 类型的相关信息，如类名、访问修饰符、常量池、字段描述、方法描述等。在类装载器加载 class 文件到内存的过程中，虚拟机会提取其中的类型信息，并将这些信息存储到方法区。当需要存储类信息而方法区的内存占用又已经达到 -XX:MaxPermSize 设置的最大值，将会抛出 OutOfMemoryError 异常。对于这种情况的测试，基本的思路是运行时产生大量的类去填满方法区，直到溢出。这里需要借助 CGLib 直接操作字节码运行时，生成了大量的动态类。 无法复现： /** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */ public class MethodAreaOOM { public static void main(String[] args) { int count = 0; while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(MethodAreaOOM.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invoke(obj, args); } }); enhancer.create(); System.out.println(++count); } } } 运行结果 Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:792) ... 8 more 4.3、栈溢出： java.lang.StackOverflowError : Thread Stack space 栈溢出了，JVM 依然是采用栈式的虚拟机，这个和 C 和 Pascal 都是一样的。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的 “层” 太多了，以致于把栈区溢出了。 通常来讲，一般栈区远远小于堆区的，因为函数调用过程往往不会多于上千层，而即便每个函数调用需要 1K 的空间（这个大约相当于在一个 C 函数内声明了 256 个 int 类型的变量），那么栈区也不过是需要 1MB 的空间。通常栈的大小是 1-2MB 的。通俗一点讲就是单线程的程序需要的内存太大了。 通常递归也不要递归的层次过多，很容易溢出。 解决方法：1：修改程序。2：通过 -Xss: 来设置每个线程的 Stack 大小即可。 在 Java 虚拟机规范中，对这个区域规定了两种异常状况：StackOverflowError 和 OutOfMemoryError 异常。 4.3.1、StackOverflowError异常 每当java程序代码启动一个新线程时，Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。当线程调用java方法时，虚拟机压入一个新的栈帧到该线程的java栈中。只要这个方法还没有返回，它就一直存在。如果线程的方法嵌套调用层次太多(如递归调用)，随着java栈中帧的逐渐增多，最终会由于该线程java栈中所有栈帧大小总和大于-Xss设置的值，而产生StackOverflowError内存溢出异常。例子如下： public class JavaVMStackSOF { private int count = 0; public static void main(String[] args) { new JavaVMStackSOF().method(); } public void method() { System.out.println(++count); method(); } } 运行结果： 961Exception in thread "main" java.lang.StackOverflowError at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77) at sun.nio.cs.UTF_8.access$200(UTF_8.java:57) at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636) at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691) at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579) at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271) at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125) at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207) at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129) at java.io.PrintStream.newLine(PrintStream.java:545) at java.io.PrintStream.println(PrintStream.java:737) at com.payn.base.jvm.outleak.JavaVMStackSOF.method(JavaVMStackSOF.java:16) 随着 - Xss 参数值的增大，可以嵌套的方法调用层次也相应增加。综上所述，StackOverflowError 异常是由于方法调用的层次太深，最终导致为某个线程分配的所有栈帧大小总和大于 - Xss 设置的值，从而发生 StackOverflowError 异常。 4.3.2、OutOfMemoryError异常 java 程序代码启动一个新线程时，没有足够的内存空间为该线程分配 java 栈 (一个线程 java 栈的大小由 - Xss 参数确定)，JVM 则抛出 OutOfMemoryError 异常。例子如下： /** * VM Args: -Xss128k */ public class JavaVMStackOOM { public static void main(String[] args) { int count = 0; while (true) { Thread thread = new Thread(new Runnable() { public void run() { while (true) { try { Thread.sleep(5000); } catch (Exception e) { } } } }); thread.start(); System.out.println(++count); } } } 运行结果： Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:693) at com.demo.test.JavaVMStackOOM.main(JavaVMStackOOM.java:21) 4.3、所以 Server 容器启动的时候需要关心设置 JVM 的几个参数如下 实例如：JAVA_OPTS="-Xms4g -Xmx4g -Xmn1024m -XX:PermSize=320M -XX:MaxPermSize=320m -XX:SurvivorRatio=6" -Xms：java Heap 初始大小， 默认是物理内存的 1/64。 -Xmx：java Heap 最大值，不可超过物理内存。 -Xmn：young generation 的 heap 大小，一般设置为 Xmx 的 3、4 分之一 。增大年轻代后，将会减小年老代大小，可以根据监控合理设置。 -Xss：每个线程的 Stack 大小，而最佳值应该是 128K, 默认值好像是 512k。 -XX:PermSize：设定内存的永久保存区初始大小，缺省值为 64M。 -XX:MaxPermSize：设定内存的永久保存区最大大小，缺省值为 64M。 -XX:SurvivorRatio：Eden 区与 Survivor 区的大小比值，设置为 8，则两个 Survivor 区与一个 Eden 区的比值为 2:8，一个 Survivor 区占整个年轻代的 1/10。 -XX:+UseParallelGC：F 年轻代使用并发收集，而年老代仍旧使用串行收集。 -XX:+UseParNewGC：设置年轻代为并行收集，JDK5.0 以上，JVM 会根据系统配置自行设置，所无需再设置此值。 -XX:ParallelGCThreads：并行收集器的线程数，值最好配置与处理器数目相等 同样适用于 CMS。 -XX:+UseParallelOldGC：年老代垃圾收集方式为并行收集 (Parallel Compacting)。 -XX:MaxGCPauseMillis：每次年轻代垃圾回收的最长时间 (最大暂停时间)，如果无法满足此时间，JVM 会自动调整年轻代大小，以满足此值。 -XX:+ScavengeBeforeFullGC：Full GC 前调用 YGC, 默认是 true。 4.3.1、第一种 OutOfMemoryError： PermGen space 发生这种问题的原意是程序中使用了大量的 jar 或 class，使 java 虚拟机装载类的空间不够，与 Permanent Generation space 有关。解决这类问题有以下两种办法： 1、增加 java 虚拟机中的 XX:PermSize 和 XX:MaxPermSize 参数的大小，其中 XX:PermSize 是初始永久保存区域大 小，XX:MaxPermSize 是最大永久保存区域大小。如针对 tomcat6.0，在 catalina.sh 或 catalina.bat 文件中一系列环境变量名说明结束处（大约在 70 行左右） 增加一行： JAVA_OPTS="-XX:PermSize=64M -XX:MaxPermSize=128m" 如果是 windows 服务器还可以在系统环境变量中设置。感觉用 tomcat 发布 sprint+struts+hibernate 架构的程序时很容易发生这种内存溢出错误。使用上述方法，我成功解决了部署 ssh 项目的 tomcat 服务器经常宕机的问题。 2、清理应用程序中 web-inf/lib 下的 jar，如果 tomcat 部署了多个应用，很多应用都使用了相同的 jar，可以将共同的 jar 移到 tomcat 共同的 lib 下，减少类的重复加载。这种方法是网上部分人推荐的，我没试过，但感觉减少不了太大的空间，最靠谱的还是第一种方法。 4.3.2、第二种 OutOfMemoryError： Java heap space 发生这种问题的原因是 java 虚拟机创建的对象太多，在进行垃圾回收之间，虚拟机分配的到堆内存空间已经用满了，与 Heap space 有关。解决这类问题有两种思路： 1、检查程序，看是否有死循环或不必要地重复创建大量对象。找到原因后，修改程序和算法。 2、增加 Java 虚拟机中 Xms（初始堆大小）和 Xmx（最大堆大小）参数的大小。如：set JAVA_OPTS= -Xms256m -Xmx1024m 。
一、什么是类加载机制 类的加载指的是将类的 .class 文件中的二进制数据读入到内存中，将其放在运行时数据区的方法区内，然后在堆区创建一个 java.lang.Class 对象，用来封装类在方法区内的数据结构。类加载的最终产品是位于堆区中的 Class 对象，Class 对象封装了类在方法区的数据结构，并且向 Java 程序员提供了访问方法区被的数据结构的接口。 类加载器并不需要等到某个类被 “首次主动使用” 时在加载它。JVM 规范允许类加载器在预料某个类将要使用时就预先加载它，如果在预先加载的过程中遇到了 .class 文件缺失或存在错误，类加载器必须在程序首次主动使用该类时才报告错误 (LinkageError 错误) 如果这个类一直没有被程序主动使用，那么类加载器就不会报告错误 。 二、类的生命周期 其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中，加载、验证、准备和初始化这四个阶段发生的顺序是确定的，而解析阶段则不一定，它在某些情况下可以在初始化阶段之后开始，这是为了支持 Java 语言的运行时绑定（也成为动态绑定或晚期绑定）。另外注意这里的几个阶段是按顺序开始，而不是按顺序进行或完成，因为这些阶段通常都是互相交叉地混合进行的，通常在一个阶段执行的过程中调用或激活另一个阶段。 2.1、加载：查找并加载类的二进制数据 加载是类加载过程的第一个阶段，在加载阶段，虚拟机需要完成以下三件事情： 1、通过一个类的全限定名来获取其定义的二进制字节流。 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3、在 Java 堆中生成一个代表这个类的 java.lang.Class 对象，作为对方法区中这些数据的访问入口。 相对于类加载的其他阶段而言，加载阶段（准确地说，是加载阶段获取类的二进制字节流的动作）是可控性最强的阶段，因为开发人员既可以使用系统提供的类加载器来完成加载，也可以自定义自己的类加载器来完成加载。 加载阶段完成后，虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中，而且在 Java 堆中也创建一个 java.lang.Class 类的对象，这样便可以通过该对象访问方法区中的这些数据。 2.2、连接 2.2.1、验证：确保被加载的类的正确性 验证是连接阶段的第一步，这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。验证阶段大致会完成 4 个阶段的检验动作： 文件格式验证：验证字节流是否符合 Class 文件格式的规范；例如：是否以 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 元数据验证：对字节码描述的信息进行语义分析（注意：对比 javac 编译阶段的语义分析），以保证其描述的信息符合 Java 语言规范的要求；例如：这个类是否有父类，除了 java.lang.Object 之外。 字节码验证：通过数据流和控制流分析，确定程序语义是合法的、符合逻辑的。 符号引用验证：确保解析动作能正确执行。 验证阶段是非常重要的，但不是必须的，它对程序运行期没有影响，如果所引用的类经过反复验证，那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施，以缩短虚拟机类加载的时间。 2.2.2、准备：为类的静态变量分配内存，并将其初始化为默认值。 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段，这些内存都将在方法区中分配。对于该阶段有以下几点需要注意： 1、这时候进行内存分配的仅包括类变量（static），而不包括实例变量，实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 2、这里所设置的初始值通常情况下是数据类型默认的零值（如 0、0L、null、false 等），而不是被在 Java 代码中被显式地赋予的值。 假设一个类变量的定义为：public static int value = 3； 那么变量 value 在准备阶段过后的初始值为 0，而不是 3，因为这时候尚未开始执行任何 Java 方法，而把 value 赋值为 3 的 putstatic 指令是在程序编译后，存放于类构造器 <clinit>() 方法之中的，所以把 value 赋值为 3 的动作将在初始化阶段才会执行。 这里还需要注意如下几点： 对基本数据类型来说，对于类变量（static）和全局变量，如果不显式地对其赋值而直接使用，则系统会为其赋予默认的零值，而对于局部变量来说，在使用前必须显式地为其赋值，否则编译时不通过。 对于同时被 static 和 final 修饰的常量，必须在声明的时候就为其显式地赋值，否则编译时不通过；而只被 final 修饰的常量则既可以在声明时显式地为其赋值，也可以在类初始化时显式地为其赋值，总之，在使用前必须为其显式地赋值，系统不会为其赋予默认零值。 对于引用数据类型 reference 来说，如数组引用、对象引用等，如果没有对其进行显式地赋值而直接使用，系统都会为其赋予默认的零值，即 null。 如果在数组初始化时没有对数组中的各元素赋值，那么其中的元素将根据对应的数据类型而被赋予默认的零值。 3、如果类字段的字段属性表中存在 ConstantValue 属性，即同时被 final 和 static 修饰，那么在准备阶段变量 value 就会被初始化为 ConstValue 属性所指定的值。 假设上面的类变量 value 被定义为： public static final int value = 3； 编译时 Javac 将会为 value 生成 ConstantValue 属性，在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 3。可以理解为 static final 常量在编译期就将其结果放入了调用它的类的常量池中。 2.2.3、解析：把类中符号引用转换为直接引用 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程，解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标，可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 2.3、初始化 初始化，为类的静态变量赋予正确的初始值，JVM 负责对类进行初始化，主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式： 声明类变量时指定初始值 使用静态代码块为类变量指定初始值 JVM 初始化步骤： 假如这个类还没有被加载和连接，则程序先加载并连接该类。 假如该类的直接父类还没有被初始化，则先初始化其直接父类。 假如类中有初始化语句，则系统依次执行这些初始化语句。 类初始化时机：只有当类主动使用的时候才会导致类的初始化，类主动使用包括以下四种： 使用 new 关键字实例化对象、读取或者设置一个类的静态字段（被 final 修饰的静态字段除外）、调用个类的静态方法的时候。 使用 java.lang.reflect 包中的方法对类进行反射调用的时候。 初始化一个类，发现其父类还没有初始化过的时候。 虚拟机启动的时候，虚拟机会先初始化用户指定的包含 main () 方法的那个类。 以上四种情况称为主动使用，其他的情况均称为被动使用，被动使用不会导致初始化。 2.4、初始化示例说明 1、子类引用父类静态字段（非 final），不会导致子类初始化。 public class SuperClass { static { System.out.println("super static"); } public static int value = 123; } public class SubClass extends SuperClass{ static { System.out.println("sub"); } } public class TestInit { public static void main(String[] args) { System.out.println(SubClass.value); } } 运行结果 super static 123 说明：并没有初始化子类，虽然使用 SubClass.value，但实际使用的是子类集成父类的静态字段，不会初始化 SubClass。即只有直接定义这个字段的类才会被初始化。 2、对于类或接口而言，使用其常量字段（final、static）不会导致其初始化。 public class SuperClass { static { System.out.println("super static"); } public static final int value = 123; } public class TestInit { public static void main(String[] args) { System.out.println(SuperClass.value); } } 运行结果 123 说明：类或接口的常量并不会导致类或借口的初始化。因为常量在编译时进行优化，直接嵌入在 TestInit.class 文件的字节码中。 3、对于类而言，初始化子类会导致父类（不包括接口）的初始化。 public class SuperClass { static { System.out.println("super static"); } public static final int value = 123; } public class SubClass extends SuperClass{ public static int i = 3; static { System.out.println("sub"); } } public class TestInit { public static void main(String[] args) { System.out.println(SubClass.i); } } 运行结果 super sub 3 说明L：初始化子类会导致父类的初始化，并且父类的初始化在子类初始化的前面。 4、通过数组定义引用类，不会触发此类的初始化。 public class SuperClass1 { public static int value = 123; static { System.out.println("SuperClass1 init"); } } public class TestMain { public static void main(String[] args) { SuperClass1[] scs = new SuperClass1[10]; } } 5、对于接口而言，初始化子接口不会导致父接口的初始化，只有在真正使用到父接口的时候(如使用父接口中定义的常量)，才会初始化。 三、类加载器 public class ClassLoaderTest { public static void main(String[] args) { ClassLoader loader = Thread.currentThread().getContextClassLoader(); System.out.println(loader); System.out.println(loader.getParent()); System.out.println(loader.getParent().getParent()); } } 运行结果： sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@4554617c null 从上面的结果可以看出，并没有获取到 ExtClassLoader 的父 Loader，原因是 BootstrapLoader（引导类加载器）是用 c 语言实现，找不到一个确定的返回父 Loader 的方式，于是返回 null。 这几种类加载器的层次关系如下图所示： 注意：这里父类加载器并不是通过继承关系来实现的，而是通过组合实现的。 站在 Java 虚拟机的角度来讲，只存在两种不同的类加载器。 启动类加载器：它使用 C++ 实现（这里仅限于 HotSpot，也就是 JDK1.5 之后默认的虚拟机，有很多其他的虚拟机是用 Java 语言实现的），是虚拟机本身的一部分。 所有其他的类加载器：这些类加载器都是由 Java 语言实现，独立与虚拟机之外，并且全部继承自抽象类 java.lang.ClassLoader ，这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。 类加载器： 启动类加载器：Bootstrap ClassLoader：负责加载存放在 JDK\jre\lib (JDK 代表 JDK 的安装目录，下同) 下，或被 -Xbootclasspath 参数指定的路径中的，并且能被虚拟机识别的类库（如 rt.jar，所有的 java.* 开头的类均被 Bootstrap ClassLoader 加载）。启动类加载器是无法被 Java 程序直接引用的。 扩展类加载器：Extension ClassLoader：该加载器由 sun.misc.Launcher$ExtClassLoader 实现，它负责加载 JDK\jre\lib\ext 目录中，或者由 java.ext.dirs 系统变量指定的路径中的所有类库（如 javax.* 开头的类），开发者可以直接使用扩展类加载器。 应用程序类加载器：Application ClassLoader：该类加载器由 sun.misc.Launcher$AppClassLoader 来实现，它负责加载用户类路径（ClassPath）所指定的类，开发者可以直接使用该类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。 应用程序都是由这三种类加载器互相配合进行加载的，如果有必要，我们还可以加入自定义的类加载器。因为 JVM 自带的 ClassLoader 只是懂得从本地文件系统加载标准的 java class 文件，因此如果编写了自己的 ClassLoader，便可以做到如下几点： 1）在执行非置信代码之前，自动验证数字签名。 2）动态地创建符合用户特定需要的定制化构建类。 3）从特定的场所取得 java class，例如数据库中和网络中。 JVM 类加载机制： 全盘负责，当一个类加载器负责加载某个 Class 时，该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入，除非显示使用另外一个类加载器来载入。 父类委托，先让父类加载器试图加载该类，只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。 缓存机制，缓存机制将会保证所有加载过的 Class 都会被缓存，当程序中需要使用某个 Class 时，类加载器先从缓存区寻找该 Class，只有缓存区不存在，系统才会读取该类对应的二进制数据，并将其转换成 Class 对象，存入缓存区。这就是为什么修改了 Class 后，必须重启 JVM，程序的修改才会生效。 四、类的加载 类加载有三种方式： 1、命令行启动应用时候由 JVM 初始化加载 2、通过 Class.forName () 方法动态加载 3、通过 ClassLoader.loadClass () 方法动态加载 public class LoaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader loader = HelloWorld.class.getClassLoader(); System.out.println(loader); //使用ClassLoader.loadClass()来加载类，不会执行初始化块 // loader.loadClass("com.payn.base.jvm.loader.Test2"); //使用Class.forName()来加载类，默认会执行初始化块 // Class.forName("com.payn.base.jvm.loader.Test2"); //使用Class.forName()来加载类，并指定ClassLoader，初始化时不执行静态块 Class.forName("com.payn.base.jvm.loader.Test2", false, loader); } } public class Test2 { static { System.out.println("Test2 静态初始化块执行了！"); } } 分别切换加载方式，会有不同的输出结果。 Class.forName () 和 ClassLoader.loadClass () 区别： Class.forName ()：将类的.class 文件加载到 jvm 中之外，还会对类进行解释，执行类中的 static 块； ClassLoader.loadClass ()：只干一件事情，就是将.class 文件加载到 jvm 中，不会执行 static 中的内容，只有在 newInstance 才会去执行 static 块。 注：Class.forName (name, initialize, loader) 带参函数也可控制是否加载 static 块。并且只有调用了 newInstance () 方法采用调用构造函数，创建类的对象 。 五、双亲委派模型 工作流程：如果一个类加载器收到类加载的请求，它首先不会自己去尝试加载这个类，而是把请求委托给父加载器去完成，依次向上，因此所有的类加载器请求最终都应该被传递到顶层的启动类加载器中，只有当父加载器在它的搜索范围中没有找到所需的类时，即无法完成该加载，子加载器才会舱室自己去加载该类。 双亲委派机制： 1、当 AppClassLoader 加载一个 class 时，它首先不会自己去尝试加载这个类，而是把类加载请求委派给父类加载器 ExtClassLoader 去完成。 2、当 ExtClassLoader 加载一个 class 时，它首先也不会自己去尝试加载这个类，而是把类加载请求委派给 BootStrapClassLoader 去完成。 3、如果 BootStrapClassLoader 加载失败（例如在 $JAVA_HOME/jre/lib 里未查找到该 class），会使用 ExtClassLoader 来尝试加载； 4、若 ExtClassLoader 也加载失败，则会使用 AppClassLoader 来加载，如果 AppClassLoader 也加载失败，则会报出异常 ClassNotFoundException。 ClassLoader 源码分析： public Class<?> loadClass(String name)throws ClassNotFoundException { return loadClass(name, false); } protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException { // 首先判断该类型是否已经被加载 Class c = findLoadedClass(name); if (c == null) { //如果没有被加载，就委托给父类加载或者委派给启动类加载器加载 try { if (parent != null) { //如果存在父类加载器，就委派给父类加载器加载 c = parent.loadClass(name, false); } else { //如果不存在父类加载器，就检查是否是由启动类加载器加载的类，通过调用本地方法native Class findBootstrapClass(String name) c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果父类加载器和启动类加载器都不能完成加载任务，才调用自身的加载功能 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } 双亲委派模型的意义： 系统类防止内存中出现多份同样的字节码。 保证 Java 程序安全稳定运行。
一、概述 jvm 调优就是根据 gc 日志分析 jvm 内存分配、回收的情况来调整各区域内存比例或者 gc 回收的策略；更深一层就是根据 dump 出来的内存结构和线程栈来分析代码中不合理的地方给予改进。eclipse 优化主要涉及的是前者，通过 gc 日志来分析。本文主要是通过分析 eclipse gc 日志为例来示例如何根据 gc 日志来分析 jvm 内存而进行调优，像根据关闭 eclipse 启动项、关闭各种校验等措施来优化 eclipse 本文不再阐述。 二、准备环境 eclipse 默认配置：eclipse.ini： -startup plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar --launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.1.300.v20150602-1417 -clean -product org.eclipse.epp.package.jee.product --launcher.defaultAction openFile --launcher.XXMaxPermSize 256M -showsplash org.eclipse.platform --launcher.XXMaxPermSize 256m --launcher.defaultAction openFile --launcher.appendVmargs -vmargs -Dosgi.requiredJavaVersion=1.7 -Xms256m -Xmx1024m 在配置的末尾处添加如下配置文件： -XX:+PrintGCDetails // 输出GC的详细日志 -XX:+PrintGCDateStamps // 输出GC的时间戳（以日期的形式） -Xloggc:gc.log // 输出GC的详细日志 eclipse 启动计时插件： http://www.chendd.cn/information/viewInformation/experienceShare/148.a 下载后解压放置在 eclipse 或 myeclipse 的安装目录下 dropins 目录下即可。 GChisto.jar:gc 日志分析工具 jar 包一个。 直接点击 gchisto.jar 就可以运行，点 add 载入 gc.log。 Visual GC: java 自带的内存监控工具，通过 visual gc 可以实时的监控到各个内存区域的变化。 Java VisualVM 默认没有安装 Visual GC 插件，需要手动安装，在 JDK 的安装目录的 bin 目录下双击 jvisualvm.exe，即可打开 Java VisualVM，在下面的启动界面中点击工具 -> 插件 -> 可用插件 ->Visual GC。 安装完成后重启 Java VisualVM，Visual GC 界面自动打开，即可看到 JVM 中堆内存的分代情况。 三、如何分析 GC 日志 摘录 GC 日志一部分（绿色为年轻代 gc 回收；蓝色为 full gc 回收）： 2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs] 通过上面日志分析得出，PSYoungGen、ParOldGen、PSPermGen 属于 Parallel 收集器。其中 PSYoungGen 表示 gc 回收前后年轻代的内存变化；ParOldGen 表示 gc 回收前后老年代的内存变化；PSPermGen 表示 gc 回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁，耗时短；full gc 会对整个堆内存进行回城，耗时长，因此一般尽量减少 full gc 的次数。 通过两张图非常明显看出 gc 日志构成： young gc 日志： Full GC 日志： 四、启动调优 启动 eclipse 查看默认配置下启动时间大概是 70 秒左右。 根据 GChisto 分析 gc 日志看出来，启动过程中进行了 45 次 minor gc，没有进行 full gc;young gc 的时间差不多是 0.56 秒左右。 第一步优化： 为了避免内存频繁的动态扩展，直接把 -Xms 配置和 -Xmx 一致，修改如下： -Xms1024m 修改完毕，重新启动： 启动时间缩小到 17 秒，分析 gc 日志得出 young gc10 次。 第二步优化： 至此优化结束，附最终的 eclipse.ini 文件: -startup plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar --launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.1.300.v20150602-1417 -clean -product org.eclipse.epp.package.jee.product --launcher.defaultAction openFile --launcher.XXMaxPermSize 256M -showsplash org.eclipse.platform --launcher.XXMaxPermSize 256m --launcher.defaultAction openFile --launcher.appendVmargs -vmargs -Dosgi.requiredJavaVersion=1.7 -Xms1024m -Xmx1024m -Xverify:none -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
一、为什么需要垃圾回收 如果不进行垃圾回收，内存会被消耗空。 二、哪些内存需要回收 哪些内存需要回收是垃圾回收机制第一个要考虑的问题，所谓 要回收的垃圾 无非就是那些不可能再被任何途径使用的对象。 2.1、引用计数法 这个算法的实现是，给对象中添加一个引用计数器，每当一个地方引用这个对象时，计数器值 + 1；当引用失效时，计数器值 - 1。任何时刻计数值为 0 的对象就是不可能再被使用的。这种算法使用场景很多，但是，Java 中却没有使用这种算法，因为这种算法很难解决对象之间相互引用的情况。看一段代码： //虚拟机参数：-verbose:gc public class ReferenceCountingGC { private Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性唯一的作用就是占用一点内存 */ private byte[] bigSize = new byte[2 * _1MB]; public static void main(String[] args) { ReferenceCountingGC objectA = new ReferenceCountingGC(); ReferenceCountingGC objectB = new ReferenceCountingGC(); objectA.instance = objectB; objectB.instance = objectA; objectA = null; objectB = null; System.gc(); } } 运行结果： [GC (System.gc()) 9346K->832K(251392K), 0.0008103 secs] [Full GC (System.gc()) 832K->667K(251392K), 0.0044238 secs] 两个对象相互引用者，但是虚拟机还是把两个对象回收掉了。这也说明虚拟机并不是通过引用计数法来判断对象是都存活的。 2.2、可达性分析法 这个算法的基本思想是通过一系列称为 GCRoots 的对象作为起始点，从这些节点向下搜索，搜索所走过的路径称为引用链，当一个对象到 GCRoots 没有任何引用链（即 GCRoots 到对象不可达）时，则证明此对象是不可用的。 如何选取 GCRoots 对象呢？在 Java 语言中，可以作为 GCRoots 的对象包括下面几种： 1、虚拟机栈（栈帧中的局部变量区，也叫做局部变量表）中引用的对象。 2、方法区中的类静态属性引用的对象。 3、方法区中常量引用的对象。 4、本地方法栈中 JNI (Native 方法) 引用的对象。 下面给出一个 GCRoots 的例子，如下图，为 GCRoots 的引用链。 由图可知，obj8、obj9、obj10 都没有到 GCRoots 对象的引用链，即便 obj9 和 obj10 之间有引用链，他们还是会被当成垃圾处理，可以进行回收。 三、四中引用状态 在 JDK1.2 之前，Java 中引用的定义很传统：如果引用类型的数据中存储的数值代表的是另一块内存的起始地址，就称这块内存代表着一个引用。这种定义很纯粹，但是太过于狭隘，一个对象只有被引用或者没被引用两种状态。我们希望描述这样一类对象：当内存空间还足够时，则能保留在内存中；如果内存空间在进行垃圾收集后还是非常紧张，则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。在 JDK1.2 之后，Java 对引用的概念进行了扩充，将引用分为强引用、软引用、弱引用、虚引用 4 种，这 4 种引用强度依次减弱。 3.1、强引用 代码中普遍存在的类似 Object obj = new Object () 这类的引用，只要强引用还存在，垃圾收集器永远不会回收掉被引用的对象。 3.2、软引用 描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前，将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存，才会抛出内存溢出异常。Java 中的类 SoftReference 表示软引用。 3.3、弱引用 描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前，垃圾收集器工作之后，无论当前内存是否足够，都会回收掉只被弱引用关联的对象。Java 中的类 WeakReference 表示弱引用。 3.4、虚引用 这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知，被虚引用关联的对象，和其生存时间完全没关系。Java 中的类 PhantomReference 表示虚引用。 对于可达性分析算法而言，未到达的对象并非是 “非死不可” 的，若要宣判一个对象死亡，至少需要经历两次标记阶段。 1、如果对象在进行可达性分析后发现没有与 GCRoots 相连的引用链，则该对象被第一次标记并进行一次筛选，筛选条件为是否有必要执行该对象的 finalize 方法，若对象没有覆盖 finalize 方法或者该 finalize 方法是否已经被虚拟机执行过了，则均视作不必要执行该对象的 finalize 方法，即该对象将会被回收。反之，若对象覆盖了 finalize 方法并且该 finalize 方法并没有被执行过，那么，这个对象会被放置在一个叫 F-Queue 的队列中，之后会由虚拟机自动建立的、优先级低的 Finalizer 线程去执行，而虚拟机不必要等待该线程执行结束，即虚拟机只负责建立线程，其他的事情交给此线程去处理。 2、对 F-Queue 中对象进行第二次标记，如果对象在 finalize 方法中拯救了自己，即关联上了 GCRoots 引用链，如把 this 关键字赋值给其他变量，那么在第二次标记的时候该对象将从 “即将回收” 的集合中移除，如果对象还是没有拯救自己，那就会被回收。如下代码演示了一个对象如何在 finalize 方法中拯救了自己，然而，它只能拯救自己一次，第二次就被回收了。具体代码如下： public class FinalizeEscapeGC { private String name; private static FinalizeEscapeGC SAVE_HOOK = null; public FinalizeEscapeGC(String name) { this.name = name; } public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); System.out.println(this); FinalizeEscapeGC.SAVE_HOOK = this; } @Override public String toString() { return name; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC("leesf"); System.out.println(SAVE_HOOK); // 对象第一次拯救自己 SAVE_HOOK = null; System.out.println(SAVE_HOOK); System.gc(); // 因为finalize方法优先级很低，所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead : ("); } // 下面这段代码与上面的完全相同,但是这一次自救却失败了 // 一个对象的finalize方法只会被调用一次 SAVE_HOOK = null; System.gc(); // 因为finalize方法优先级很低，所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead : ("); } } } 运行结果如下： leesf null finalize method executed! leesf yes, i am still alive :) no, i am dead : ( 由结果可知，该对象拯救了自己一次，第二次没有拯救成功，因为对象的 finalize 方法最多被虚拟机调用一次。此外，从结果可以得知，一个堆对象的 this（放在局部变量表中的第一项）引用会永远存在，在方法体内可以将 this 引用赋值给其他变量，这样堆中对象就可以被其他变量所引用，即不会被回收。 四、方法区的垃圾回收 方法区的垃圾回收主要回收两个部分内容：1、废弃常量；2、无用的类。 如何判断废弃常量呢？以字面量回收为例，如果一个字符串 “abc” 已经进入常量池，但是当前系统没有任何一个 String 对象引用了叫做 “abc” 的字面量，那么，如果发生垃圾回收并且有必要时，“abc” 就会被系统移出常量池。常量池中的其他类（接口）、方法、字段的符号引用也与此类似。 如何判断无用的类呢？需要满足以下三个条件： 1、该类的所有实例都已经被回收，即 Java 堆中不存在该类的任何实例。 2、加载该类的 ClassLoader 已经被回收。 3、该类对应的 java.lang.Class 对象没有在任何地方被引用，无法在任何地方通过反射访问该类的方法。 满足以上三个条件的类就可以进行垃圾回收，但是并不是无用就被回收，虚拟机提供了一些参数供配置。 五、垃圾收集算法 5.1、标记-清除（Mark-Sweep）算法 这是最基础的算法，标记-清除算法就如同它的名字样，分为 “标记” 和 “清除” 两个阶段：首先标记出所有需要回收的对象，标记完成后统一回收所有被标记的对象。这种算法的不足主要体现在效率和空间，从效率的角度讲，标记和清除两个过程的效率都不高；从空间的角度讲，标记清除后会产生大量不连续的内存碎片， 内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时，无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。标记 - 清除算法执行过程如图： 5.2、复制（Copying）算法 复制算法是为了解决效率问题而出现的，它将可用的内存分为两块，每次只用其中一块，当这一块内存用完了，就将还存活着的对象复制到另外一块上面，然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收，内存分配时也不需要考虑内存碎片等复杂情况，只需要移动指针，按照顺序分配即可。复制算法的执行过程如图： 不过这种算法有个缺点，内存缩小为了原来的一半，这样代价太高了。现在的商用虚拟机都采用这种算法来回收新生代，不过研究表明 1:1 的比例非常不科学，因此新生代的内存被划分为一块较大的 Eden 空间和两块较小的 Survivor 空间，每次使用 Eden 和其中一块 Survivor。每次回收时，将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块 Survivor 空间上，最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 区和 Survivor 区的比例为 8:1，意思是每次新生代中可用内存空间为整个新生代容量的 90%。当然，我们没有办法保证每次回收都只有不多于 10% 的对象存活，当 Survivor 空间不够用时，需要依赖老年代进行分配担保 Handle Promotion 。 5.3、标记-整理（Mark-Compact）算法 复制算法在对象存活率较高的场景下要进行大量的复制操作，效率很低。万一对象 100% 存活，那么需要有额外的空间进行分配担保。老年代都是不易被回收的对象，对象存活率高，因此一般不能直接选用复制算法。根据老年代的特点，有人提出了另外一种标记-整理算法，过程与标记-清除算法一样，不过不是直接对可回收对象进行清理，而是让所有存活对象都向一端移动，然后直接清理掉边界以外的内存。标记-整理算法的工作过程如图： 5.4、分代收集算法 根据上面的内容，用一张图概括一下堆内存的布局。 现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的，无非是上面内容的结合，根据对象的生命周期的不同将内存划分为几块，然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的（新生代），使用复制算法，复制成本低；对象存活率高、没有额外空间进行分配担保的（老年代），采用标记-清理算法或者标记-整理算法。 六、垃圾收集器 垃圾收集器是上面讲的理论知识的具体实现。不同的虚拟机提供的垃圾收集器可能会有很大的差别，我们使用的是 HotSpot，HotSpot 这个虚拟机包含的所有收集器如图。 上图展示的 7 种作用于不同分代的收集器，如果两个收集器之间存在连线，那说明它们可以搭配使用。虚拟机所处的区域说明它是属于新生代收集器还是老年代收集器。必须明确一个观点：没有最好的垃圾收集器，更加没有万能的收集器，只能选择对具体应用最合适的收集器。 6.1、Serial 收集器 最基本、发展历史最久的收集器，这个收集器是一个采用复制算法的单线程的收集器，单线程一方面意味着它只会使用一个 CPU 或一条线程去完成垃圾收集工作，另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作，直到它收集结束为止。后者意味着，在用户不可见的情况下要把用户正常工作的线程全部停掉，这对很多应用是难以接受的。不过实际上到目前为止，Serial 收集器依然是虚拟机运行在 Client 模式下的默认新生代收集器，因为它简单而高效。用户桌面应用场景中，分配给虚拟机管理的内存一般来说不会很大，收集几十兆甚至一两百兆的新生代停顿时间在几十毫秒最多一百毫秒，只要不是频繁发生，这点停顿是完全可以接受的。Serial 收集器运行过程如下图所示： 说明：1、需要 STW（Stop The World），停顿时间长。2、简单高效，对于单个 CPU 环境而言，Serial 收集器由于没有线程交互开销，可以获取最高的单线程收集效率。 6.2、ParNew 收集器 ParNew 收集器其实就是 Serial 收集器的多线程版本，除了使用多条线程进行垃圾收集外，其余行为和 Serial 收集器完全一样，包括使用的也是复制算法。ParNew 收集器除了多线程以外和 Serial 收集器并没有太多创新的地方，但是它却是 Server 模式下的虚拟机首选的新生代收集器，其中有一个很重要的和性能无关的原因是，除了 Serial 收集器外，目前只有它能与 CMS 收集器配合工作（看图）。CMS 收集器是一款几乎可以认为有划时代意义的垃圾收集器，因为它第一次实现了让垃圾收集线程与用户线程基本上同时工作。ParNew 收集器在单 CPU 的环境中绝对不会有比 Serial 收集器更好的效果，甚至由于线程交互的开销，该收集器在两个 CPU 的环境中都不能百分之百保证可以超越 Serial 收集器。当然，随着可用 CPU 数量的增加，它对于 GC 时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与 CPU 数量相同，在 CPU 数量非常多的情况下，可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。ParNew 收集器运行过程如下图所示： 6.3、Parallel Scavenge 收集器 Parallel Scavenge 收集器也是一个新生代收集器，也是用复制算法的收集器，也是并行的多线程收集器，但是它的特点是它的关注点和其他收集器不同。介绍这个收集器主要还是介绍吞吐量的概念。CMS 等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间，而 Parallel Scavenge 收集器的目标则是打到一个可控制的吞吐量。所谓吞吐量的意思就是 CPU 用于运行用户代码时间与 CPU 总消耗时间的比值，即吞吐量 = 运行用户代码时间 /（运行用户代码时间 + 垃圾收集时间），虚拟机总运行 100 分钟，垃圾收集 1 分钟，那吞吐量就是 99%。Parallel Scavenge 收集器是虚拟机运行在 Server 模式下的默认垃圾收集器。 停顿时间短适合需要与用户交互的程序，良好的响应速度能提升用户体验；高吞吐量则可以高效率利用 CPU 时间，尽快完成运算任务，主要适合在后台运算而不需要太多交互的任务。 虚拟机提供了 -XX:MaxGCPauseMillis 和 -XX:GCTimeRatio 两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好，GC 停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切，Parallel Scavenge 收集器也被称为 “吞吐量优先收集器”。Parallel Scavenge 收集器有一个 -XX:+UseAdaptiveSizePolicy 参数，这是一个开关参数，这个参数打开之后，就不需要手动指定新生代大小、Eden 区和 Survivor 参数等细节参数了，虚拟机会根据当前系统的运行情况手机性能监控信息，动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。如果对于垃圾收集器运作原理不太了解，以至于在优化比较困难的时候，使用 Parallel Scavenge 收集器配合自适应调节策略，把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。 6.4、Serial Old 收集器 Serial 收集器的老年代版本，同样是一个单线程收集器，使用 标记-整理算法，这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。 6.5、Parallel Old 收集器 Parallel Scavenge 收集器的老年代版本，使用多线程和 标记-整理算法。这个收集器在 JDK 1.6 之后的出现，“吞吐量优先收集器” 终于有了比较名副其实的应用组合，在注重吞吐量以及 CPU 资源敏感的场合，都可以优先考虑 Parallel Scavenge 收集器 + Parallel Old 收集器的组合。运行过程如下图所示： 6.6、CMS 收集器 CMS（Conrrurent Mark Sweep）收集器是以获取最短回收停顿时间为目标的收集器。使用 标记-清除算法，收集过程分为如下四步： 1、初始标记，标记 GCRoots 能直接关联到的对象，时间很短。 2、并发标记，进行 GCRoots Tracing（可达性分析）过程，时间很长。 3、重新标记，修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录，时间较长。 4、并发清除，回收内存空间，时间很长。 其中，并发标记与并发清除两个阶段耗时最长，但是可以与用户线程并发执行。运行过程如下图所示: 说明：1、对 CPU 资源非常敏感，可能会导致应用程序变慢，吞吐率下降。2、无法处理浮动垃圾，因为在并发清理阶段用户线程还在运行，自然就会产生新的垃圾，而在此次收集中无法收集他们，只能留到下次收集，这部分垃圾为浮动垃圾，同时，由于用户线程并发执行，所以需要预留一部分老年代空间提供并发收集时程序运行使用。3、由于采用的标记-清除算法，会产生大量的内存碎片，不利于大对象的分配，可能会提前触发一次 Full GC。虚拟机提供了 -XX:+UseCMSCompactAtFullCollection 参数来进行碎片的合并整理过程，这样会使得停顿时间变长，虚拟机还提供了一个参数配置，-XX:+CMSFullGCsBeforeCompaction，用于设置执行多少次不压缩的 Full GC 后，接着来一次带压缩的 GC。 6.7、G1 收集器 G1 是目前技术发展的最前沿成果之一，HotSpot 开发团队赋予它的使命是未来可以替换掉 JDK1.5 中发布的 CMS 收集器。与其他 GC 收集器相比，G1 收集器有以下特点： 1、并行和并发。使用多个 CPU 来缩短 Stop The World 停顿时间，与用户线程并发执行。 2、分代收集。独立管理整个堆，但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次 GC 的旧对象，以获取更好的收集效果。 3、空间整合。基于标记-整理算法，无内存碎片产生。 4、可预测的停顿。能建立可预测的停顿时间模型，能让使用者明确指定在一个长度为 M 毫秒的时间片段内，消耗在垃圾收集上的时间不得超过 N 毫秒。 在 G1 之前的垃圾收集器，收集的范围都是整个新生代或者老年代，而 G1 不再是这样。使用 G1 收集器时，Java 堆的内存布局与其他收集器有很大差别，它将整个 Java 堆划分为多个大小相等的独立区域（Region），虽然还保留有新生代和老年代的概念，但新生代和老年代不再是物理隔离的了，它们都是一部分（可以不连续）Region 的集合。 七、理解 GC 日志 每种收集器的日志形式都是由它们自身的实现所决定的，换言之，每种收集器的日志格式都可以不一样。不过虚拟机为了方便用户阅读，将各个收集器的日志都维持了一定的共性，来看下面的一段 GC 日志： [GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] [GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000) eden space 2176K, 2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000) from space 256K, 0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000) to space 256K, 0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000) tenured generation total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000) the space 5312K, 3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000) compacting perm gen total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000) the space 21248K, 14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000) No shared spaces configured. 1、日志的开头 “GC”、“Full GC” 表示这次垃圾收集的停顿类型，而不是用来区分新生代 GC 还是老年代 GC 的。如果有 Full，则说明本次 GC 停止了其他所有工作线程 (Stop-The-World)。看到 Full GC 的写法是 “Full GC (System)”，这说明是调用 System.gc () 方法所触发的 GC。 2、“GC” 中接下来的 “[DefNew” 表示 GC 发生的区域，这里显示的区域名称与使用的 GC 收集器是密切相关的，例如上面样例所使用的 Serial 收集器中的新生代名为 “Default New Generation”，所以显示的是 “[DefNew”。如果是 ParNew 收集器，新生代名称就会变为 “[ParNew”，意为 “Parallel New Generation”。如果采用 Parallel Scavenge 收集器，那它配套的新生代称为 “PSYoungGen”，老年代和永久代同理，名称也是由收集器决定的。 3、后面方括号内部的 “310K->194K (2368K)”、“2242K->0K (2368K)”，指的是该区域已使用的容量 ->GC 后该内存区域已使用的容量 (该内存区总容量)。方括号外面的 “310K->194K (7680K)”、“2242K->2241K (7680K)” 则指的是 GC 前 Java 堆已使用的容量 ->GC 后 Java 堆已使用的容量 (Java 堆总容量)。 4、再往后 “0.0269163 secs” 表示该内存区域 GC 所占用的时间，单位是秒。最后的 “[Times: user=0.00 sys=0.00 real=0.03 secs]” 则更具体了，user 表示用户态消耗的 CPU 时间、sys 表示内核态消耗的 CPU 时间、real表示操作从开始到结束经过的墙钟时间。后面两个的区别是，墙钟时间包括各种非运算的等待消耗，比如等待磁盘 I/O、等待线程阻塞，而 CPU 时间不包括这些耗时，但当系统有多 CPU 或者多核的话，多线程操作会叠加这些 CPU 时间，所以如果看到 user 或 sys 时间超过 real 时间是完全正常的。 5、“Heap” 后面就列举出堆内存目前各个年代的区域的内存情况。
一、为什么要分代 分代的垃圾回收策略，基于一个事实：不同的对象的生命周期是不一样的。因此，不同生命周期的对象可以采取不同的收集方式，以便提高回收效率。 在 Java 程序运行的过程中，会产生大量的对象，其中有些对象是与业务信息相关，比如 Http 请求中的 Session 对象、线程、Socket 连接，这类对象跟业务直接挂钩，因此生命周期比较长。但是还有一些对象，主要是程序运行过程中生成的临时变量，这些对象生命周期会比较短，比如：String 对象，由于其不变类的特性，系统会产生大量的这些对象，有些对象甚至只用一次即可回收。 试想，在不进行对象存活时间区分的情况下，每次垃圾回收都是对整个堆空间进行回收，花费时间相对会长，同时，因为每次回收都需要遍历所有存活对象，但实际上，对于生命周期长的对象而言，这种遍历是没有效果的，因为可能进行了很多次遍历，但是他们依旧存在。分代垃圾回收采用分治的思想，进行代的划分，把不同生命周期的对象放在不同代上，不同代上采用最适合它的垃圾回收方式进行回收。 二、如何分代 虚拟机中的共划分为三个代：年轻代（Young Generation）、年老代（Old Generation）和持久代（Permanent Generation）。其中持久代主要存放的是 Java 类的类信息，与垃圾收集要收集的 Java 对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。 2.1、年轻代 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个 Eden 区，两个 Survivor 区 (一般而言)。大部分对象在 Eden 区中生成。当 Eden 区满时，还存活的对象将被复制到 Survivor 区（两个中的一个），当这个 Survivor 区满时，此区的存活对象将被复制到另外一个 Survivor 区，当这个 Survivor 区也满了的时候，从第一个 Survivor 区复制过来的并且此时还存活的对象，将被复制 年老区 (Tenured) 。需要注意，Survivor 的两个区是对称的，没先后关系，所以同一个区中可能同时存在从 Eden 复制过来的对象，和从前一个 Survivor 复制过来的对象，而复制到年老区的只有从第一个 Survivor 区过来的对象。而且，Survivor 区总有一个是空的。同时，根据程序需要，Survivor 区是可以配置为多个的（多于两个），这样可以增加对象在年轻代中的存在时间，减少被放到年老代的可能。 新生代有划分为 Eden、From Survivor 和 To Survivor 三个部分，他们对应的内存空间的大小比例为 8:1:1，也就是，为对象分配内存的时候，首先使用 Eden 空间，经过 GC 后，没有被回收的会首先进入 From Survivor 区域，任何时候，都会保持一个 Survivor 区域（From Survivor 或 To Survivor）完全空闲，也就是说新生代的内存利用率最大为 90%。From Survivor 和 To Survivor 两个区域会根据 GC 的实际情况，进行互换，将 From Survivor 区域中的对象全部复制到 To Survivor 区域中，或者反过来，将 To Survivor 区域中的对象全部复制到 From Survivor 区域中。 2.2、年老代 在年轻代中经历了 N 次垃圾回收后仍然存活的对象，就会被放到年老代中。因此，可以认为年老代中存放的都是一些生命周期较长的对象。 GC 过程中，当某些对象经过多次 GC 都没有被回收，可能会进入到年老代。或者，当新生代没有足够的空间来为对象分配内存时，可能会直接在年老代进行分配。 2.3、持久代 用于存放静态文件，如今 Java 类、方法等。持久代对垃圾回收没有显著影响，但是有些应用可能动态生成或者调用一些 class，例如 Hibernate 等，在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过 -XX:MaxPermSize=<N> 进行设置。 永久代实际上对应着虚拟机运行时数据区的 “方法区”，这里主要存放类信息、静态变量、常量等数据。一般情况下，永久代中对应的对象的 GC 效率非常低，因为这里的的大部分对象在运行都不要进行 GC，它们会一直被利用，直到 JVM 退出。 三、什么情况下触发垃圾回收 由于对象进行了分代处理，因此垃圾回收区域、时间也不一样。GC 有两种类型：Scavenge GC 和 Full GC。 一般情况下，当新对象生成，并且在 Eden 申请空间失败时，就会触发 Scavenge GC，对 Eden 区域进行 GC，清除非存活对象，并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行，不会影响到年老代。因为大部分对象都是从 Eden 区开始的，同时 Eden 区不会分配的很大，所以 Eden 区的 GC 会频繁进行。因而，一般在这里需要使用速度快、效率高的算法，使 Eden 去能尽快空闲出来。 对整个堆进行整理，包括 Young、Tenured 和 Perm。Full GC 因为需要对整个块进行回收，所以比 Scavenge GC 要慢，因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中，很大一部分工作就是对于 FullGC 的调节。有如下原因可能导致 Full GC： 年老代（Tenured）被写满。 持久代（Perm）被写满。 System.gc () 被显示调用。 上一次 GC 之后 Heap 的各域分配策略动态变化。
对象的内存分配，往大的方向上讲，就是在堆上分配，少数情况下也可能直接分配在老年代中，分配的规则并不是百分百固定的。其细节决定当前使用的是那种垃圾收集器组合。当然还有虚拟机中与内存相关的参数。垃圾收集器组合一般就是 Client 模式默认：Serial+Serial Old ，Server 模式默认：Parallel+Serial Old。 内存分配的动作，可以按照线程划分在不同的空间中进行，即每个线程在 java 堆中预先分配一小块内存，称为本地线程分配缓冲（Thread Local Allocation Buffer，TLAB）。那个线程需要分配内存，就在那个线程的 TLAB 上分配。虚拟机是否使用 TLAB，可以通过 -XX:+-UseTLAB 参数来设定。这么做的目的之一，也是为了并发创建一个对象时，保证创建对象的线程安全性。TLAB 比较小，直接在 TLAB 上分配内存的方式称为快速分配方式，而 TLAB 大小不够，导致内存被分配在 Eden 区的内存分配方式称为慢速分配方式。 一、对象有限在 Eden 区分配 对象通常在新生代的 Eden 区进行分配。当 Eden 区没有足够空间进行分配时，虚拟机将发起一次 Minor GC、Full GC。 Minor GC: 指发生在新生代的垃圾收集动作，非常频繁，速度较快。 Major GC: 指发生在老年代的 GC，出现 Major GC，经常会伴随一次 Minor GC，同时 Minor GC 也会引起 Major GC，一般在 GC 日志中统称为 GC，不频繁。 Full GC: 指发生在老年代和新生代的 GC，速度很慢，需要 Stop The World。 虚拟机参数为：-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 即 10M 新生代，10M 老年代，10M 新生代中 8M 的 Eden 区，两个 Survivor 区各 1M。 public class EdenAllocationTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1 = new byte[2 * _1MB]; byte[] allocation2 = new byte[2 * _1MB]; byte[] allocation3 = new byte[2 * _1MB]; byte[] allocation4 = new byte[4 * _1MB]; } } Client 模式下： [GC [DefNew: 6487K->194K(9216K), 0.0042856 secs] 6487K->6338K(19456K), 0.0043281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4454K [0x0000000005180000, 0x0000000005b80000, 0x0000000005b80000) eden space 8192K, 52% used [0x0000000005180000, 0x00000000055a9018, 0x0000000005980000) from space 1024K, 18% used [0x0000000005a80000, 0x0000000005ab0810, 0x0000000005b80000) to space 1024K, 0% used [0x0000000005980000, 0x0000000005980000, 0x0000000005a80000) tenured generation total 10240K, used 6144K [0x0000000005b80000, 0x0000000006580000, 0x0000000006580000) the space 10240K, 60% used [0x0000000005b80000, 0x0000000006180048, 0x0000000006180200, 0x0000000006580000) compacting perm gen total 21248K, used 2982K [0x0000000006580000, 0x0000000007a40000, 0x000000000b980000) the space 21248K, 14% used [0x0000000006580000, 0x0000000006869890, 0x0000000006869a00, 0x0000000007a40000) No shared spaces configured. Server 模式下： [GC (Allocation Failure) [PSYoungGen: 6262K->792K(9216K)] 6262K->4896K(19456K), 0.0023413 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap PSYoungGen total 9216K, used 7173K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc3b5c8,0x00000000ffe00000) from space 1024K, 77% used [0x00000000ffe00000,0x00000000ffec6030,0x00000000fff00000) to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000) Metaspace used 3471K, capacity 4496K, committed 4864K, reserved 1056768K class space used 381K, capacity 388K, committed 512K, reserved 1048576K 看到在 Client 模式下，最后分配的 4M 在新生代中，先分配的 6M 在老年代中；在 Server 模式下，最后分配的 4M 在老年代中，先分配的 6M 在新生代中。说明不同的垃圾收集器组合对于对象的分配是有影响的。讲下两者差别的原因： 1、Client 模式下，新生代分配了 6M，虚拟机在 GC 前有 6487K，比 6M 也就是 6144K 多，多主要是因为 TLAB 和 EdenAllocationTest 这个对象占的空间，TLAB 可以通过 -XX:+PrintTLAB 这个虚拟机参数来查看大小。OK，6M 多了，然后来了一个 4M 的，Eden + 一个 Survivor 总共就 9M 不够分配了，这时候就会触发一次 Minor GC。但是触发 Minor GC 也没用，因为 allocation1、allocation2、allocation3 三个引用还存在，另一块 1M 的 Survivor 也不够放下这 6M，那么这次 Minor GC 的效果其实是通过分配担保机制将这 6M 的内容转入老年代中。然后再来一个 4M 的，由于此时 Minor GC 之后新生代只剩下了 194K 了，够分配了，所以 4M 顺利进入新生代。 2、Server 模式下，前面都一样，但是在 GC 的时候有一点区别。在 GC 前还会进行一次判断，如果要分配的内存 >=Eden 区大小的一半，那么会直接把要分配的内存放入老年代中。要分配 4M，Eden 区 8M，刚好一半，而且老年代 10M，够分配，所以 4M 就直接进入老年代去了。为了验证一下结论，我们把 3 个 2M 之后分配的 4M 改为 3M 看一下 public class EdenAllocationTest{ private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1 = new byte[2 * _1MB]; byte[] allocation2 = new byte[2 * _1MB]; byte[] allocation3 = new byte[2 * _1MB]; byte[] allocation4 = new byte[3 * _1MB]; } } 运行结果： [GC [PSYoungGen: 6487K->352K(9216K)] 6487K->6496K(19456K), 0.0035661 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC [PSYoungGen: 352K->0K(9216K)] [PSOldGen: 6144K->6338K(10240K)] 6496K->6338K(19456K) [PSPermGen: 2941K->2941K(21248K)], 0.0035258 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] Heap PSYoungGen total 9216K, used 3236K [0x000000000af40000, 0x000000000b940000, 0x000000000b940000) eden space 8192K, 39% used [0x000000000af40000,0x000000000b269018,0x000000000b740000) from space 1024K, 0% used [0x000000000b740000,0x000000000b740000,0x000000000b840000) to space 1024K, 0% used [0x000000000b840000,0x000000000b840000,0x000000000b940000) PSOldGen total 10240K, used 6338K [0x000000000a540000, 0x000000000af40000, 0x000000000af40000) object space 10240K, 61% used [0x000000000a540000,0x000000000ab70858,0x000000000af40000) PSPermGen total 21248K, used 2982K [0x0000000005140000, 0x0000000006600000, 0x000000000a540000) object space 21248K, 14% used [0x0000000005140000,0x0000000005429890,0x0000000006600000) 看到 3M 在新生代中，6M 通过分配担保机制进入老年代了。 二、大对象直接进入老年区 需要大量连续空间的 Java 对象成为大对象，大对象的出现会导致提前出发垃圾收集以获取更大的连续空间来进行大对象的分配。虚拟机提供了：-XX:PretenureSizeThreadshold。 参数来设置大对象的阈值，超过阈值的对象直接分配到老年代。 看下面的代码，虚拟机参数为： -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 最后那个参数表示大于这个设置值的对象直接在老年代中分配，这样做的目的是为了避免在 Eden 区和两个 Survivor 区之间发生大量的内存复制。 public class OldTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation = new byte[4 * _1MB]; } } Client 模式下 Heap def new generation total 9216K, used 507K [0x0000000005140000, 0x0000000005b40000, 0x0000000005b40000) eden space 8192K, 6% used [0x0000000005140000, 0x00000000051bef28, 0x0000000005940000) from space 1024K, 0% used [0x0000000005940000, 0x0000000005940000, 0x0000000005a40000) to space 1024K, 0% used [0x0000000005a40000, 0x0000000005a40000, 0x0000000005b40000) tenured generation total 10240K, used 4096K [0x0000000005b40000, 0x0000000006540000, 0x0000000006540000) the space 10240K, 40% used [0x0000000005b40000, 0x0000000005f40018, 0x0000000005f40200, 0x0000000006540000) compacting perm gen total 21248K, used 2972K [0x0000000006540000, 0x0000000007a00000, 0x000000000b940000) the space 21248K, 13% used [0x0000000006540000, 0x00000000068272a0, 0x0000000006827400, 0x0000000007a00000) No shared spaces configured. Server 模式下 Heap PSYoungGen total 9216K, used 4603K [0x000000000afc0000, 0x000000000b9c0000, 0x000000000b9c0000) eden space 8192K, 56% used [0x000000000afc0000,0x000000000b43ef40,0x000000000b7c0000) from space 1024K, 0% used [0x000000000b8c0000,0x000000000b8c0000,0x000000000b9c0000) to space 1024K, 0% used [0x000000000b7c0000,0x000000000b7c0000,0x000000000b8c0000) PSOldGen total 10240K, used 0K [0x000000000a5c0000, 0x000000000afc0000, 0x000000000afc0000) object space 10240K, 0% used [0x000000000a5c0000,0x000000000a5c0000,0x000000000afc0000) PSPermGen total 21248K, used 2972K [0x00000000051c0000, 0x0000000006680000, 0x000000000a5c0000) object space 21248K, 13% used [0x00000000051c0000,0x00000000054a72a0,0x0000000006680000) 看到 Client 模式下 Eden 空间几乎没有被使用，而老年代的 10MB 空间被使用了 40%，也就是 4MB 的 allocation 对象直接就分配在老年代中，这是因为 PretenureSizeThreshold 被设置为 3MB（就是 3145728，这个参数不能像 -Xmx 之类的参数一样直接写 3MB），因此超过 3MB 的对象都会直接在老年代进行分配。Server 模式下 4M 还在新生代中。产生这个差别的原因是 -XX:PretenureSizeThreshold 这个参数对 Serial+Serial Old 垃圾收集器组合有效而对 Parallel+Serial Old 垃圾收集器组合无效。 三、长期存活的对象进入老年代 每个对象有一个对象年龄计数器，与前面的对象的存储布局中的 GC 分代年龄对应。对象出生在 Eden 区、经过一次 Minor GC 后仍然存活，并能够被 Survivor 容纳，设置年龄为 1，对象在 Survivor 区每次经过一次 Minor GC，年龄就加 1，当年龄达到一定程度（默认 15），就晋升到老年代，虚拟机提供了：-XX:MaxTenuringThreshold 来进行设置。 public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution * */ public static void testTenuringThreshold() { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } public static void main(String[] args) { testTenuringThreshold(); } } 运行结果： [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 790400 bytes, 790400 total : 5174K->771K(9216K), 0.0050541 secs] 5174K->4867K(19456K), 0.0051088 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) : 4867K->0K(9216K), 0.0015279 secs] 8963K->4867K(19456K), 0.0016327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4867K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 47% used [0x00000000ff600000, 0x00000000ffac0d30, 0x00000000ffac0e00, 0x0000000100000000) Metaspace used 2562K, capacity 4486K, committed 4864K, reserved 1056768K class space used 275K, capacity 386K, committed 512K, reserved 1048576K 说明：发生了两次 Minor GC，第一次是在给 allocation3 进行分配的时候会出现一次 Minor GC，此时 survivor 区域不能容纳 allocation2，但是可以容纳 allocation1，所以 allocation1 将会进入 survivor 区域并且年龄为 1，达到了阈值，将在下一次 GC 时晋升到老年代，而 allocation2 则会通过担保机制进入老年代。第二次发生 GC 是在第二次给 allocation3 分配空间时，这时，allocation1 的年龄加 1，晋升到老年代，此次 GC 也可以清理出原来 allocation3 占据的 4MB 空间，将 allocation3 分配在 Eden 区。所以，最后的结果是 allocation1、allocation2 在老年代，allocation3 在 Eden 区。 四、动态对象年龄判断 对象的年龄到达了 MaxTenuringThreshold 可以进入老年代，同时，如果在 survivor 区中相同年龄所有对象大小的总和大于 survivor 区的一半，年龄大于等于该年龄的对象就可以直接进入老年代。无需等到 MaxTenuringThreshold 中要求的年龄。 public class AllocationTest02 { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution * */ public static void testTenuringThreshold2() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { testTenuringThreshold2(); } } 运行结果： [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 1048576 bytes, 1048576 total : 5758K->1024K(9216K), 0.0049451 secs] 5758K->5123K(19456K), 0.0049968 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 5120K->0K(9216K), 0.0016442 secs] 9219K->5123K(19456K), 0.0016746 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 5123K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb00f80, 0x00000000ffb01000, 0x0000000100000000) Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K class space used 275K, capacity 386K, committed 512K, reserved 1048576K 结果说明：发生了两次 Minor GC，第一次发生在给 allocation4 分配内存时，此时 allocation1、allocation2 将会进入 survivor 区，而 allocation3 通过担保机制将会进入老年代。第二次发生在给 allocation4 分配内存时，此时，survivor 区的 allocation1、allocation2 达到了 survivor 区容量的一半，将会进入老年代，此次 GC 可以清理出 allocation4 原来的 4MB 空间，并将 allocation4 分配在 Eden 区。最终，allocation1、allocation2、allocation3 在老年代，allocation4 在 Eden 区。 五、空间分配担保 在发生 Minor GC 时，虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和，若成立，则说明 Minor GC 是安全的，否则，虚拟机需要查看 HandlePromotionFailure 的值，看是否运行担保失败，若允许，则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小，若大于，将尝试进行一次 Minor GC；若小于或者 HandlePromotionFailure 设置不运行冒险，那么此时将改成一次 Full GC，以上是 JDK Update 24 之前的策略，之后的策略改变了，只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC，否则将进行 Full GC。 冒险是指经过一次 Minor GC 后有大量对象存活，而新生代的 survivor 区很小，放不下这些大量存活的对象，所以需要老年代进行分配担保，把 survivor 区无法容纳的对象直接进入老年代。 具体的流程图如下： public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+HandlePromotionFailure * */ public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7, allocation8; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB]; } public static void main(String[] args) { testHandlePromotion(); } } 运行的结果： [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 528280 bytes, 528280 total : 7294K->515K(9216K), 0.0040766 secs] 7294K->4611K(19456K), 0.0041309 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 6818K->0K(9216K), 0.0012444 secs] 10914K->4611K(19456K), 0.0012760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4611K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 45% used [0x00000000ff600000, 0x00000000ffa80d58, 0x00000000ffa80e00, 0x0000000100000000) Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K class space used 275K, capacity 386K, committed 512K, reserved 1048576K 说明：发生了两次 GC，第一次发生在给 allocation4 分配内存空间时，由于老年代的连续可用空间大于存活的对象总和，所以 allocation2、allocation3 将会进入老年代，allocation1 的空间将被回收，allocation4 分配在新生代；第二次发生在给 allocation7 分配内存空间时，此次 GC 将 allocation4、allocation5、allocation6 所占的内存全部回收。最后，allocation2、allocation3 在老年代，allocation7 在新生代。
堆 & 栈是两块不同的内存区域，简单理解：堆是用来存放对象而栈是用来执行程序的。当然这种划分方式比较粗糙，只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块，Java 内存区域的划分实际上远比这复杂。 一、运行时的区域 Java 虚拟机（JVM）内部定义了程序运行时需要使用到的内存区域。 每个区域都有自己的用途，创建和销毁的时间。图中绿色部分就是所有线程之间共享的内存区域。而其余部分则是线程运行时独有的数据区域。 1.1、线程独有的内存区域 1.1.1、PROGRAM COUNTER REGISTER 程序计数器 这块内存区域很小，它是当前线程所执行的字节码的行号指示器，字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。 在 JVM 规范中规定，如果线程执行的是非 native 方法，则程序计数器中保存的是当前需要执行的指令的地址；如果线程执行的是 native 方法，则程序计数器中的值是 undefined。 因为程序计数器中存储的数据所占的空间大小不会随程序的执行而发生改变。因此不会发生内存溢出 OutOfMemory现象的。 总结：当前线程所执行的字节码的行号指示器；当前线程私有；不会出现 OutOfMemoryError 情况。 1.1.2、JAVA STACK 虚拟机栈 Java 栈也称作虚拟机栈（Java Vitual Machine Stack），Java 栈是 Java 方法执行的内存模型。 Java 栈中存放的是一个个的栈帧，每个栈帧对应一个被调用的方法，在栈帧中包括局部变量表 (Local Variables)、操作数栈 (Operand Stack)、指向当前方法所属的类的运行时常量池的引用 (Reference to runtime constant pool)、方法返回地址 (Return Address) 和一些额外的附加信息。当线程执行一个方法时，就会随之创建一个对应的栈帧，并将建立的栈帧压栈。 当方法执行完毕后，便会将栈帧出栈。因此可知，线程当前执行的方法所对应的栈帧必定位于 Java 栈的顶部。这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说，栈这部分空间对程序员来说是不透明的。下图表示了一个 Java 栈的模型： 局部变量表：用来存储方法中的局部变量（包括在方法中声明的非静态变量以及函数形参）。对于基本数据类型的变量，则直接存储它的值，对于引用类型的变量，则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了，因此在程序执行期间局部变量表的大小是不会改变的。 操作数栈：栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中，实际上就是不断执行语句的过程，而归根到底就是进行计算的过程。程序中的所有计算过程都是在借助于操作数栈来完成的。 指向运行时常量池的引用：因为在方法执行的过程中有可能需要用到类中的常量，所以必须要有一个引用指向运行时常量。 方法返回地址：当一个方法执行完毕之后，要返回之前调用它的地方，因此在栈帧中必须保存一个方法返回地址。 由于每个线程正在执行的方法可能不同，因此每个线程都会有一个自己的 Java 栈，互不干扰。 每个方法执行的同时都会创建一个栈帧，用于存储局部变量表、操作数栈、动态链接、方法出口等信息，每一个方法从调用直至执行完毕的过程，就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体 JVM 的实现有关，通常在 256K~756K 之间。 1.1.3、NATIVE METHOD STACK 本地方法栈 本地方法栈与 Java 栈的作用和原理非常相似。区别只不过是 Java 栈是为执行 Java 方法服务的，而本地方法栈则是为执行 本地方法 Native Method 服务的。在 JVM 规范中，并没有对本地方发展的具体实现方法以及数据结构作强制规定，虚拟机可以自由实现它。在 HotSopt 虚拟机中直接就把本地方法栈和 Java 栈合二为一。 1.2、线程间共享的内存区域 1.2.1、HEAP 堆 大多数应用，堆都是 Java 虚拟机所管理的内存中最大的一块，它在虚拟机启动时创建，此内存唯一的目的就是存放对象实例。由于现在垃圾收集器采用的基本都是分代收集算法，所以堆还可以细分为新生代和老年代，再细致一点还有 Eden 区、From Survivior 区、To Survivor 区。 总结：1、可以通过 -Xmx 和 -Xms 控制堆的大小；2、OutOfMemoryError 异常：当在堆中没有内存完成实例分配，且堆也无法再扩展时。 1.2.2、METHOD AREA 方法区 方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据，虚拟机规范是把这块区域描述为堆的一个逻辑部分的，但实际它应该是要和堆区分开的。从分代收集算法角度看，HotSpot 中，方法区≈永久代。不过 JDK 7 之后，使用的 HotSpot 应该就没有永久代这个概念了，会采用 Native Memory 来实现方法区的规划了。 1.2.3、RUNNING CONSTANT POOL 运行时常量池 上面的图中没有画出来，因为它是方法区的一部分。Class 文件中除了有类的版本信息、字段、方法、接口等描述信息外，还有一项信息就是常量池，用于存放编译期间生成的各种字面量和符号引用，这部分内容将在类加载后进入方法区的运行时常量池中，另外翻译出来的直接引用也会存储在这个区域中。这个区域另外一个特点就是动态性，Java 并不要求常量就一定要在编译期间才能产生，运行期间也可以在这个区域放入新的内容，String.intern () 方法就是这个特性的应用。 1.3、直接内存 直接内存并不是虚拟机运行时数据区的一部分，也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用，而且也可能导致内存溢出问题。JDK1.4 中新增加了 NIO，引入了一种基于通道与缓冲区的 I/O 方式，它可以使用 Native 函数库直接分配堆外内存，然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能，因为避免了在 Java 堆和 Native 堆中来回复制数据。显然，本机直接内存的分配不会受到 Java 堆大小的限制，但是，既然是内存，肯定还是会受到本机总内存（包括 RAM、SWAP 区）大小以及处理器寻址空间的限制。 总结： 1、NIO 可以使用 Native 函数库直接分配堆外内存，堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作； 2、大小不受 Java 堆大小的限制，受本机 (服务器) 内存限制； 3、OutOfMemoryError 异常：系统内存不足时。 二、对象的创建 Java 是一门面向对象的语言。Java 程序运行过程中无时无刻都有对象被创建出来。在语言层面上，创建对象（克隆、反序列化）就是一个 new 关键字而已，但是虚拟机层面上却不是如此。看一下在虚拟机层面上创建对象的步骤： 1、虚拟机遇到一条 new 指令，首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用，并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有，那么必须先执行类的初始化过程。 2、类加载检查通过后，虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定，为对象分配空间无非就是从 Java 堆中划分出一块确定大小的内存而已。这个地方会有两个问题： （1）如果内存是规整的，那么虚拟机将采用的是指针碰撞法来为对象分配内存。意思是所有用过的内存在一边，空闲的内存在另外一边，中间放着一个指针作为分界点的指示器，分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是 Serial、ParNew 这种基于压缩算法的，虚拟机采用这种分配方式。 （2）如果内存不是规整的，已使用的内存和未使用的内存相互交错，那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表，记录上哪些内存块是可用的，在分配的时候从列表中找到一块足够大的空间划分给对象实例，并更新列表上的内容。如果垃圾收集器选择的是 CMS 这种基于标记 - 清除算法的，虚拟机采用这种分配方式。 另外一个问题及时保证 new 对象时候的线程安全性。因为可能出现虚拟机正在给对象 A 分配内存，指针还没有来得及修改，对象 B 又同时使用了原来的指针来分配内存的情况。虚拟机采用了 CAS 配上失败重试的方式保证更新更新操作的原子性和 TLAB 两种方式来解决这个问题。 3、内存分配结束，虚拟机将分配到的内存空间都初始化为零值（不包括对象头）。这一步保证了对象的实例字段在 Java 代码中可以不用赋初始值就可以直接使用，程序能访问到这些字段的数据类型所对应的零值。 4、对对象进行必要的设置，例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息，这些信息存放在对象的对象头中。 5、执行 <init> 方法，把对象按照程序员的意愿进行初始化，这样一个真正可用的对象才算完全产生出来。 三、对象的访问定位 建立对象是为了使用对象，Java 程序需要通过栈上的 reference（引用）数据来操作堆上的具体对象。比如：Object obj = new Object()。 而 new Object () 之后其实有两部分内容，一部分是类数据（比如代表类的 Class 对象）、一部分是实例数据。 由于 reference 在 Java 虚拟机规范中只是一个指向对象 new Object () 的引用 obj，并没有规定 obj 应该通过何种方式去定位、访问堆中对象的具体位置，所以对象访问方式也是取决于虚拟机而定的。主流方式有两种： 1、句柄访问。java 堆中将会划分出一块内存来作为句柄池，reference 中存储的就是对象的句柄地址，而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 2、指针访问。java 堆对象的布局中必须考虑如何放置访问类型数据的相关信息，reference 中存储的就是对象地址。 HotSpot 虚拟机采用的是后者，不过前者的对象访问方式也是十分常见的。 3.1、内存组成及分配 java 内存组成介绍：堆（heap）和非堆（None-heap）内存。 按照官方的说法：“Java 虚拟机具有一个堆，堆是运行时数据区域，所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在 JVM 中堆之外的内存称为非堆内存 (Non-heap memory)”。 可以看出 JVM 主要管理两种类型的内存：堆和非堆。简单来说堆就是 Java 代码可及的内存，是留给开发人员使用的；非堆就是 JVM 留给自己用的，所以方法区、JVM 内部处理或优化所需的内存 (如 JIT 编译后的代码缓存)、每个类结构 (如运行时常数池、字段和方法数据) 以及方法和构造方法的代码都在非堆内存中。 方法栈 & 本地方法栈: 线程创建时产生，方法执行时生成栈帧。 方法区： 存储类的元数据信息常量等。 堆： java 代码中所有的 new 操作 native Memory(C heap) Direct Bytebuffer JNI Compile GC; 3.2、堆内存分配 JVM 初始分配的内存由 -Xms 指定，默认是物理内存的 1/64；JVM 最大分配的内存由 -Xmx 指定，默认是物理内存的 1/4。默认空余堆内存小于 40% 时，JVM 就会增大堆直到 -Xmx 的最大限制；空余堆内存大于 70% 时，JVM 会减少堆直到 -Xms 的最小限制。因此服务器一般设置 -Xms、-Xmx 相等以避免在每次 GC 后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。 Young Generation 即图中的 Eden + From Space + To Space Eden 存放新生的对象 Survivor Space 有两个，存放每次垃圾回收后存活的对象 Old Generation Tenured Generation 即图中的 Old Space 主要存放应用程序中生命周期长的存活对象 3.3、非堆内存分配 JVM 使用 -XX:PermSize 设置非堆内存初始值，默认是物理内存的 1/64；由 -XX:MaxPermSize 设置最大非堆内存的大小，默认是物理内存的 1/4。 Permanent Generation 保存虚拟机自己的静态 (refective) 数据主要存放加载的 Class 类级别静态对象如 class 本身，method，field 等等 permanent generation 空间不足会引发 full GC (详见 HotSpot VM GC 种类) Code Cache 用于编译和保存本地代码（native code）的内存 JVM 内部处理或优化 3.4、JVM 内存限制 (最大值) JVM 内存的最大值跟操作系统有很大的关系。简单的说就 32 位处理器虽然 可控内存空间有 4GB，但是具体的操作系统会给一个限制，这个限制一般是 2GB-3GB（一般来说 Windows 系统下为 1.5G-2G，Linux 系统 下为 2G-3G），而 64bit 以上的处理器就不会有限制了。
JVM 有两种运行模式 Server 与 Client。两种模式的区别在于，Client 模式启动速度较快，Server 模式启动较慢；但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多。这是因为 Server 模式启动的 JVM 采用的是重量级的虚拟机，对程序采用了更多的优化；而 Client 模式启动的 JVM 采用的是轻量级的虚拟机。所以 Server 启动慢，但稳定后速度比 Client 远远要快。 一、当前是 Client or Server java -versinon java version "1.8.0_144" Java(TM) SE Runtime Environment (build 1.8.0_144-b01) Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode) 二、Client 与 Server 切换 2.1、模式配置文件 JVM 启动时采用何种模式是在名为 jvm.cfg 的配置文件中配置的。 配置文件的位置： 32位的虚拟机在%JAVA_HOME%/jre/lib/i386/jvm.cfg 64位的虚拟机在%JAVA_HOME%/jre/lib/amd64/jvm.cfg 打开32位的虚拟机对应的模式配置文件： # Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved. # ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. # List of JVMs that can be used as an option to java, javac, etc. # Order is important -- first in this list is the default JVM. # NOTE that this both this file and its format are UNSUPPORTED and # WILL GO AWAY in a future release. # You may also select a JVM in an arbitrary location with the # "-XXaltjvm=<jvm_dir>" option, but that too is unsupported # and may not be available in a future release. -client KNOWN -server KNOWN 第一行的模式 -client KNOWN 被作为虚拟机默认的运行模式，如果想让 32 位的虚拟机按照 server 模式运行，将第一行和第二行代码交换位置。 jvm.cfg 中配置：（此时为 client 模式） 64 位的虚拟机对应的模式匹配文件关于运行模式代码： -server KNOWN -client IGNORE client 模式后面对应的参数为 IGNORE。说明 64 位虚拟机的 server 模式无法转换成 client 模式。 64 位只支持 server 模式。 jvm.cfg 中配置：（此时为 server 模式）