写点什么

JVM 浅析(一)

作者:andy
  • 2022-10-28
    北京
  • 本文字数:4119 字

    阅读完需:约 14 分钟

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 信息如下:


Heap PSYoungGen      total 2560K, used 907K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)  eden space 2048K, 44% used [0x00000000ffd00000,0x00000000ffde2ff8,0x00000000fff00000)  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000) Metaspace       used 2624K, capacity 4486K, committed 4864K, reserved 1056768K  class space    used 283K, capacity 386K, committed 512K, reserved 1048576K
复制代码


注意:新年代的内存大小占 3/8,老年代的内存大小占 5/8。存活区 from(s0)和存货区 to(s1)内存大小相同。

为了查看 Full GC 起作用,可以编写以下代码,观察打印信息。

代码:


package org.fuys.ownutil.instance;import java.util.Random;public class RuntimeInstance {	public static void main(String[] args) {		String str = "www.chinal.com";		while(true){			str += str + new Random().nextInt(99999);			str.intern();		}	}}
复制代码


打印信息:

[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 处理?


用户头像

andy

关注

还未添加个人签名 2019-11-21 加入

还未添加个人简介

评论

发布
暂无评论
JVM浅析(一)_andy_InfoQ写作社区