1 JVM 内存结构
1.1 运行时数据区
1.1.1 栈(虚拟机栈)
每个线程在创建时都会创建一个私有的Java虚拟机栈,在执行每个方法时都会打包成一个栈帧,存储了局部变量表
、操作数栈
、动态链接
、方法出口
等信息,然后放入栈中。方法的执行对应着栈帧出栈的过程。栈的大小默认为1M,可通过参数-Xss调整大小,如-Xss256k。
1.1.2 本地方法栈
与虚拟机栈类似,只是对应的是本地方法
1.1.3 程序计数器
程序计数器用于记录当前线程所执行的字节码指令的行号。
1.1.4 方法区
用于存储已经被虚拟机加载的类信息、常量、静态变量等数据。可通过参数(JDK1.8以后)-XX:MetaspaceSize, -XX:MaxMetaspaceSize调节,如-XX:MaxMetaspaceSize=3M。
1.1.5 堆
堆是Java中最大的一部分,它用于存储对象实例。所有的对象实例以及数组都在堆上分配内存。堆是由垃圾回收器自动管理的,因此开发者不需要关心内存的回收问题。但是,由于垃圾回收器的存在,堆上的数据可能会影响程序的性能。
可通过以下参数调节:-Xms:堆的最小值;-Xmx:堆的最大值;-Xmn:新生代的大小;-XX:NewSize:新生代最小值;-XX:MaxNewSize:新生代最大值;例如-Xmx256m堆划分为新生代和老年代,新生代又分为Eden区、Survivor1(from)区、Survivor2(to)区。
1.2 直接内存
调用native函数直接分配的堆外内存,使用直接内存避免了JAVA堆与native堆来回复制数据,能够提高效率。默认与-Xmx 参数值相同为 100M。可以通过-XX:MaxDirectMemorySize 来单独设置直接内存的大小。
1.3 Java的垃圾回收(Garbage Collection)机制
Java的垃圾回收是JVM自动回收不再使用的对象所占用的内存的过程。它减少了开发者手动管理内存的负担,避免了内存泄漏的问题。
垃圾回收器通过跟踪每个对象的引用关系来决定哪些对象是可达的,哪些对象是不可达的。当一个对象没有任何引用指向它时,它就被认为是不可达的,因此可以被垃圾回收器回收。
垃圾回收器的工作过程大致如下:
- 从根对象(Root Object)开始遍历整个堆内存,标记所有被引用的对象;
- 遍历整个堆内存,找到没有被标记的对象,将其回收;
- 更新堆的大小。
在执行垃圾回收时,JVM会将所有线程暂停一段时间,这个过程被称为“Stop-The-World”。这可能会对程序的性能产生影响,因此优化垃圾回收器的性能是提高JVM性能的一个重要方向。
1.4 垃圾回收算法
1.4.1 标记-清除
首先标记所有需要回收的对象,在标记完成后,统一回收掉所有标记的对象(也可以标记存活对象,回收未标记的对象)。 缺点:执行效率不稳定(随着对象数量增长而降低)、内存空间碎片化。
1.4.2 标记-复制(简称复制算法)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活的对象复制到另一块,然后再把已使用过的内存空间一次清理掉。
缺点:内存缩小为原来的一半。
商用虚拟机大多采用这种算法回收新生代,大多数对象是朝生夕死的(约98%对象熬不过第一轮收集),因此并不需要按照1:1来划分新生代的内存空间。
Appel式回收将新生代划分为一块较大的Eden区和两块较小的Survivor区,每次分配内存只使用Eden区和其中一块Survivor区, 发生垃圾回收时,将Eden区和Survivor区中仍然存活的对象一次性复制到另外一块Survivor区上,然后清理掉Eden区和使用过的Survivor区。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,只有一个Survivor空间,即10%的新生代会被浪费。任何时候都没法保证每次回收后只有少于10%的对象存活,当Suvivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(大多是老年代)进行分配担保。
1.4.3 标记-整理
标记-复制算法在对象存活率较高时要进行较多的复制操作,效率会降低。如果不想浪费50%的空间,就需要额外的空间进行分配担保,所以老年代不直接使用标记-复制算法。
标记-整理算法的标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向内存空间的一端移动,然后清理掉边界以外的内容。
移动存活对象并更新所以引用这些对象的地方是一个极为负重的操作,必须全程暂停用户程序(称为Stop The World)才能进行。
1. 5 类加载的过程
1.5.1 加载
加载阶段,虚拟机需要完成三件事:
- **获取类的二进制流:**通过类的全限定名获取类的二进制字节流。
- 转化成运行时数据结构 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 生成类对象: 在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
1.5.2 验证
是连接的第一步,目的是确保Class文件的字节流中包含的信息符合**《JAVA 虚拟机规范》**的全部约束,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。 大致需要完成四个阶段的检验动作: 文件格式验证 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。验证包括:魔数、主次版本号、常量池等等。 元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合《JAVA 语言规范》的要求。验证包括:类是否有父类、类是否继承了不允许被继承的类、类的字段和方法是否与其父类产生矛盾等。 字节码验证 通过数据流分析和控制流分析,确定程序的语义是合法的、符合逻辑的。 符号引用验证 验证符号引用全限定名代表的类是否能够找到,对应的域和方法是否能找到,访问权限是否合法。
1.5.3 准备
为类中定义的类变量(被static修饰的变量)分配内存并设置初始值。通常情况下初始化是零值,只有变量被final修饰时,才会初始化为指定的值。
1.5.4 解析
将常量池中的符号引用替换为直接引用的过程。
1.5.5 初始化
根据程序员编码制订的主观计划区初始化类变量和其他资源。
1.6 类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身来确定其在JAVA虚拟机中的唯一性。每一个类加载器都都拥有一个独立的类名称空间。
主要有四种类加载器:
-
启动类加载器(Bootstrap ClassLoader): 用来加载JAVA 核心库(JAVA_HOME\lib目录下)。
-
扩展类加载器(Extension ClassLoader): 用来加载JAVA扩展库(JAVA_HOME\lib\ext目录下)。
-
应用类加载器(Application ClassLoader,也称系统类加载器): 负责加载用户类路径(ClassPath)上的所有类库。可通过ClassLoader.getSystemClassLoader()来获取。如果应用中没有自定义的类加载器,一般情况下为应用程序中的默认类加载器。
-
自定义类加载器(User ClassLoader): 通过继承java.lang.ClassLoader类实现。
1.7 双亲委派模式
如果一个类加载器接收到了加载类的请求,它首先不会去自己加载这个类,而是委托给父类加载器加载,依次递归。只有当父类加载器不能完成加载任务时,自己才去加载。 双亲委派模式的好处:JAVA中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如,java.lang.Object类,无论哪个类加载器加载,最终都会委托给最顶层的启动类加载器加载,因此Object类在程序的各种类加载器环境中都能保证是同一个类。
Tomcat打破双亲委派模型。
1.8 JVM性能优化
1.8.1 内存溢出
程序在申请内存时,没有足够的空间。情况如下:
-
栈溢出。 循环调用(StackOverflowError)、线程太多(OutOfMemoryError)。
-
堆溢出。 不断创建对象,分配大对象堆内存。
-
方法区溢出。在经常动态生产大量 Class 的应用中,CGLIb 字节码增强,动态语言,大量 JSP(JSP 第一次运行需要编译成 Java 类),基于 OSGi 的应用(同一个类,被不同的加载器加载也会设为不同的类)。
-
直接内存溢出。可以通过-XX:MaxDirectMemorySize来设置。
1.8.2 内存泄露
程序在申请内存后,无法释放已申请的内存。 可能存在内存泄露的情况:
-
长生命周期的对象持有短生命周期的对象。如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放。
-
连接未关闭。如数据库连接、网络连接、IO连接等。 变量作用作用域不合理 一个变量定义的作用范围大于其使用范围,并没有及时设置为null。
-
内部类持有外部类。Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏。
-
Hash值改变。在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露。
1.8.3 JVM 参数配置
-Xmx3550m: 最大堆大小为 3550m。
-Xms3550m: 设置初始堆大小为 3550m。
-Xmn2g: 设置年轻代大小为 2g。
-Xss128k: 每个线程✁堆栈大小为 128k。
-XX:MaxPermSize: 设置持久代大小为 16m
-XX:NewRatio=4: 设置年轻代(包括Eden 和两个Survivor 区)与年老代
的比值(除去持久代)。
-XX:SurvivorRatio=4: 设置年轻代中Eden 区与 Survivor 区的大小比值。
设置为 4,则两个 Survivor 区与一个 Eden 区✁比值为 2:4,一个 Survivor区占整个年轻代的 1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为 0
的话,则年轻代对象不经过 Survivor 区,直接进入年老代。
1.8.4 JDK提供的优化工具
命令行工具:
jps
列出当前机器上正在运行的虚拟机进程。
jps 显示进程和启动类名称
jps –q 只显示进程
jps –m 输出主函数传入的参数
jps –l 输入主函数完整包名
jps –v 输入启动程序制定的jvm参数
jstat
用于监视虚拟机各种运行信息的命令行工具。显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT(即时编译)等运行数据。
用法:jstat –xx 进程号
jstat –class 类加
jstat –compiler JIT
jstat –gc GC堆状态
jstat –gccapacity 各区大小
jstat –gccause 最近一次gc统计和原因
jstat –gcnew 新生代统
jstat –gcnewcapacity 新生代大小
jstat –gcold 老年代统计
jstat –gcoldcapacity 老年代大小
jstat –gcpermcapacity 永久代大小
jstat –gcutil gc统计汇总
jstat –printcompilation 虚拟机编译统计
jinfo
查看和修改虚拟机参数
用法:jinfo [option] <pid>
jinfo –sysprops 获取虚拟机参数,等价于System.getProperties()
jinfo –flag <name> 输出对应名称的参数
jinfo –flag [+|-]<name> 开启或关闭对应名称的参数
jinfo –flags 输出所有的参数
jmap
用于查看堆和永久代的详细信息、生成堆转储快照(dump文件)
用法:jmap [option] <pid>
jmap –heap 查看堆
jmap –finalizerinfo查询 finalize 执行队列
jmap –dump 导出堆dump文件 例如:jmap –dump:live –format=b,file=D:\heap.bin 1740
jhat
生成dump文件分析,可在浏览器上访问:http://localhost:7000
jhat <dump文件>
jstack
查看虚拟机线程信息。
可视化工具:
jconsole
JAVA_HOME\bin\jconsole.exe
jvisualvm
JAVA_HOME\bin\jvisualvm.exe