桥下の民 - JVM
type: channel
link: https://wordplay.work/index.php/category/JVM
items: JVM 调优
items: 生产环境参数实例分析
items: 内存溢出&内存泄露
items: Java 类加载机制
items: Eclipse JVM 调优
items: Java 垃圾回收 GC 机制详解
items: JVM 分代垃圾回收策略的基础概念
items: JVM 内存分配与回收策略
items: JVM 内存划分
items: JVM 的 Client 模式与 Server 模式

JVM 调优
type: item
date: 2024-03-05T08:07:00+00:00
description: 一、调优命令 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%" # 监控内存使用率…

生产环境参数实例分析
type: item
date: 2023-02-13T14:49:00+00:00
description: 一、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…

内存溢出&内存泄露
type: item
date: 2023-02-13T14:33:00+00:00
description: 一、概念 内存溢出：简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存，导致无法申请到足够的内存，于是就发生了内存溢出。 内存泄漏：内存泄漏指程序运行过程中分配内存给临时变量，用完之后却没有被 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 …

Java 类加载机制
type: item
date: 2023-02-13T14:23:00+00:00
description: 一、什么是类加载机制 类的加载指的是将类的 .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 …

Eclipse JVM 调优
type: item
date: 2023-02-13T13:51:00+00:00
description: 一、概述 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…

Java 垃圾回收 GC 机制详解
type: item
date: 2023-02-13T13:31:00+00:00
description: 一、为什么需要垃圾回收 如果不进行垃圾回收，内存会被消耗空。 二、哪些内存需要回收 哪些内存需要回收是垃圾回收机制第一个要考虑的问题，所谓 要回收的垃圾 无非就是那些不可能再被任何途径使用的对象。 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 表示虚引用。 对于可达性分析算法而言，未到达的对象并非是 “非死不可” 的，…

JVM 分代垃圾回收策略的基础概念
type: item
date: 2023-02-13T13:18:00+00:00
description: 一、为什么要分代 分代的垃圾回收策略，基于一个事实：不同的对象的生命周期是不一样的。因此，不同生命周期的对象可以采取不同的收集方式，以便提高回收效率。 在 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，清除非存活对象，并且把尚且存…

JVM 内存分配与回收策略
type: item
date: 2023-02-13T12:52:00+00:00
description: 对象的内存分配，往大的方向上讲，就是在堆上分配，少数情况下也可能直接分配在老年代中，分配的规则并不是百分百固定的。其细节决定当前使用的是那种垃圾收集器组合。当然还有虚拟机中与内存相关的参数。垃圾收集器组合一般就是 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…

JVM 内存划分
type: item
date: 2023-02-13T12:35:00+00:00
description: 堆 & 栈是两块不同的内存区域，简单理解：堆是用来存放对象而栈是用来执行程序的。当然这种划分方式比较粗糙，只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块，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 方法区 方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据，虚拟机规范是把这…

JVM 的 Client 模式与 Server 模式
type: item
date: 2023-02-13T12:17:00+00:00
description: 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 模式）