JVM 浅析(一)
1-JVM
JVM,全称 Java Virtual Machine,针对 Java 计算机编程语言,虚拟化的计算机。如同计算机一样,针对 Java 程序,具备内存的分配、CPU 指令的执行以及进程和线程的管理等等功能。
运行时数据区
1、堆内存(Heap)
堆内存所有线程共享,用于存放几乎所有对象的实例,是垃圾回收的主要区域。堆是存储的单元,保存引用数据类型的实例对象,即保存对象信息,属于线程共享
2、方法区(Method Area)
方法区为所有线程共享,主要存储类信息、常量、静态变量、即时编译器编译后的代码
3、程序计数器(Program Counter Register)
程序计数器线程私有,占用内存较小,作用是当前线程执行的字节码的行号指示器,字节码解释器工作时通过改变计数器的值来选取下一条字节码指令
4、栈内存(Stack)
栈帧(Stack Frame),支持虚拟机进行方法调用和方法执行的数据结构
5、本地方法栈(Native Method Stack)
作用与虚拟机栈相似,区别在于虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚拟机执行 Native 方法服务
2-字节码运行
JVM 运行 Java 字节码
3-类加载机制
一、类加载过程
二、加载
加载,是指查找字节流,并且据此创建类的过程。
双亲委派模型
子类加载器接受加载请求时,先将请求转发给父类加载器,在父类加载器加载请求的类失败后,再由子类加载器去尝试加载。
三、连接
连接,将加载时创建的类变为可执行的类的过程。
四、初始化
4-JVM 内存模型与垃圾收集
GC(garbage collection)
垃圾收集机制是 Java 的一大特性,对于不需要使用的对象,进行清除。GC(garbage collection),垃圾收集器是 Java 虚拟机中的进程,其运行机制如下:
扫描堆内存,标记哪些对象被使用,哪些對象不被使用,并且清除无用的对象
问题的复杂性在于,Java 中的对象如何进行标记,这是需要涉及算法的,不可能所有的对象都需要进行扫描排查,否则,性能太差了。而大量的垃圾对象实际上是在堆内存中产生的,所以,接下来针对堆内存进行分析。
堆内存空间
堆内存是 Java 中保存实例对象的内存空间。整個 JVM 的堆内存空间实际上分为以下三块:
年轻代:保存新对象和沒有达到一定时间的对象的内存区域
老年代:保存长时间使用的对象,老年代的内存空間应比年轻代大
元空间:某些方法中的临时操作对象,直接使用物理内存保存
注意:
1、JDK1.8 之前 HotSpot 存在永久代,永久代的作用类似于元空间,但是其内存空间是在堆内存里进行划分。而取消永久代的目的在于 Oracle 将 SUN 的 hotspot 和 BEA 的 jrockit 的各自标准合为一个;
2、从下图堆内存的划分,可以发现,每块子内存区都有伸缩区,其目的在于可以进行动态扩展;
3、堆内存划分为新生代、老年代、元空间,其实便于管理,用于判断哪些对象用于使用,哪些对象用于清空。
*堆内存的划分,如同一个人的生命周期一样。先有孕育(伊甸园),然后存活下来(存活区),能够独立成长之后,进入成年老年期(老年代)
*同时也说明,人一定要活的久,你才能够干更多的事情
图 1 堆内存组成(JDK1.8 之后)
图 2 堆内存组成(JDK1.8 之前)
GC 垃圾回收流程
堆内存用于存放对象数据,既有临时对象,也有常驻对象。常驻对象如单例设计模式的实例对象。JVM 为了保证性能,对于堆内存空间中的对象处理,采用以下流程方式:
圖 3 GC 处理流程
注意:
1、新对象的生成,涉及到内存分配的问题,平均每个对象,栈内存存 4K,堆内存存 8K;
2、GC 的处理针对新生代和老年代,元空间(永久代)不在 GC 处理范围内,如果永久代存在大量的垃圾对象,是不能处理的,这也是为什么取消永久代的原因;
3、Minor GC 在新生代(伊甸园区和存活区)发生作用,Major GC(Full GC)在老年代发生作用;
4、所有对象的空间都在伊甸园区申请;
5、对象存储搬移的顺序由 Eden 区到 survivor 区,再到 tenured 区;
6、当 Eden 区不足,survivor 区充足,則会先把 Eden 区活跃的对象保存至 survivor 区,随后新对象再保存至 Eden 区;
7、当 Eden 区不足,survivor 区也不足,而 tenured 区充足,則会先把 survivor 区活跃的对象保存至 tenured 区,然后,再把 Eden 区活跃的对象保存至 survivor 区,最后再把新对象保存至 Eden 区;
8、当 Eden 区、survivor 区、tenured 区都不足的情況下,抛出 OutOfMemoryError 错误。
堆内存调优(关键)
實際上每一塊子内存空間都存在可變的伸縮區,黨空間不足時,在可變的範圍内動態的擴展内存空間,一段時間之後,空間不緊張時,再釋放可變的伸縮區空間。
基于以上的思想理念,可变伸缩区的动态扩展,既增大和缩小是需要耗费时间复杂度的。因此,建议最好不要有可变伸缩区。
圖 4 JVM 整體内存調整
堆内存空间调整参数
Java 類中可通過 Runtime 類獲取堆内存的信息,部分調用方法如下:
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long totalMemory = runtime.totalMemory();
*.java.lang.Runtime 类是 Java 类库中的重要代表,用于获取 Java 程序运行时的相关信息,包括堆内存初始化分配大小和最大内存大小。同时,需要注意,该类也是访问权限中,单例设计的代表。
爲了提高性能,避免可变伸縮區动态扩展的可調策略,可以配置初始堆内存大小等於最大堆内存大小。示例如,包含打印 GC 信息:
java -Xms10m -Xmx10m -XX:+PrintGCDetails HeapDemo
打印 GC 信息如下:
注意:新年代的内存大小占 3/8,老年代的内存大小占 5/8。存活区 from(s0)和存货区 to(s1)内存大小相同。
为了查看 Full GC 起作用,可以编写以下代码,观察打印信息。
代码:
打印信息:
[Full GC (Ergonomics) [PSYoungGen: 470K->100K(1024K)] [ParOldGen: 477K->477K(512K)] 948K->578K(1536K), [Metaspace: 2646K->2646K(1056768K)], 0.0098331 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
为了能够对运行程序进行监控分析,可使用以下工具。
可視化工具:jvisualvm(路徑:$java_home/bin/jvisualvm.exe,查看對應程序 pid 下的信息)
命令行工具:jmap(路徑:$java_home/bin/jmap.exe,jmap -heap pid,其中 pid 在 Windows 下通過 tasklist 查找,在 Linux 下通過 ps 查找)
企业级应用调优补充(极其重要):
鉴于 Java 主要应用于企业级项目,也就是服务器端程序开发,那么,实际上对于 JVM 的调优,就需要懂得如何在中间件上进行内存分配管理。通常使用的是免费的中间价 Tomcat。由于服务器端项目运行依赖于容器,故只需要对 Tomcat 下的 bin 目录中的 catalina.sh 文件进行修改即可。在文件内容中 cygwin=false 位置前,增加 JAVA_OPTS 属性即可。示例如下:
# OS specific support. $var _must_ be set to either true or false.
JAVA_OPTS="-Xms256m -Xmx512m -Xss1024K -XX:PermSize=128m -XX:MaxPermSize=256m"
cygwin=false
4.1、年轻代
年轻代是堆内存空间的一部分,新产生的对象保存至年轻代的 Eden 区。当 Eden 区的对象经过多次的 minor gc 后仍然存活,則发生晋升,即对象保存至 survivor 区。但是 survivor 区必须有两块,且两者大小相等,并有一个是空的。原因是其中的一块用于活跃对象的晋升,另一块用于对象的回收。
图 6 年轻代
年轻代使用的 minor gc,其算法采用的是复制算法。
*对于复制,很想克隆。Java 中进行克隆,依赖 Object 类中的 clone()方法,但是对象必须实现 cloneable 接口。
图 7 Minor GC 复制算法
由上图可知,Eden 区存放着大量的临时对象,因此會频繁的调用 minor gc,为了加快 Eden 区内存空间的操作,采用以下两种技术实现
圖 8 Bump-The-Pointer
圖 9 TLAB
年轻代内存调整参数
年轻代核心概念:
1、通常情况下,不需要设置年轻代内存大小;
2、年轻代使用的是 Minor GC,该 GC 采用的是复制算法;
3、总有一个存活区是空的,两个存活区大小相同。
4.2、老年代
老年代保存年轻代经过多次 minor gc 后存活下來的对象,当 Eden 区保存的对象大小超过了设置的大小,则直接保存至老年代。当老年代内存不足时,则会调用 full gc(major gc)
老年代采用两种算法结合的模式进行 GC 处理:整理-压缩
图 11 标记-清除
在回收清除的过程中,老年代并沒有对内存进行整理,因此最大的问题在于产生空间碎片
图 12 标记-压缩
由上,在老年代尽量保存长久使用的对象且不会被轻易回收的大对象
老年代内存调整参数
老年代核心思想:
1、老年代使用 Full GC(Major GC),该 GC 采用整理-清除两种算法结合的模式;
2、老年代存储长期存活和数据较大的对象。
4.3、永久代(JDK1.8 之前)
永久代在堆内存之中保存,不會被回收,如使用 intern()方法产生的对象不会被回收,也就是说大量的字符串对象产生,会导致永久代内存溢出。因此,保存数据量多大,会抛出 OOM 错误
使用动态加载类的方式处理时就有可能产生“java.lang.OutOfMemoryError:PermGen space”错误信息。
设置永久代内存参数
4.4、元空间
元空間类似于永久代,只不过数据保存在物理内存中,受到物理内存大小的限制。
元空间与永久代最大的区别在于:永久代保存使用的是 JVM 的堆内存,而元空间使用的是本机物理内存,所以元空间的大小受到本机物理内存的限制。
元空间内存参数
问题:
1、什么会时候发生 GC?如何处理?
2、Java 项目运行缓慢,如何解決?
3、当一個对象足夠大时,如何保存?
4、StackOverflowError 和 OutOfMemoryError 什麽时候出现?
5、gc()方法执行的时候是何种 GC 处理?
评论