JVM内存结构
JVM的内存结构主要包括两大部分:一是线程之间共享的方法区(永久代)和堆,二是线程私有的虚拟机栈、本地方法栈以及程序计数器。
- 方法区:存储被JVM加载的类信息、常量、静态变量以及编译后的代码,它还包括一个运行时的常量池,保存了Class文件中的类版本、接口、方法、字段等符号引用信息。另外也存储了声明为final的常量、基本数据类型值、字符串等,如程序中定义的String变量的值会存储在常量池中进行复用。
- 堆:堆是JVM中最大的一块内存,存放了Java程序运行时几乎所有的对象实例和数组。堆也是垃圾收集器管理的主要区域(故也被称为:GC堆)。按照垃圾分代收集器的设计,堆可以划分为:新生代(Eden、S0、S1)和老年代(Tentired)。关于垃圾回收的过程,我们在下面的“JVM垃圾回收”的章节再展开来说。
- 虚拟机栈:是存放线程执行时的局部变量等信息,与线程的生命周期相同。虚拟机栈由多个栈帧组成,线程中的每个Java方法被调用时,便会创建一个栈帧(存放局部变量表、操作数栈、动态链接、方法出口信息),方法执行完后出栈。所有栈帧都出栈后,线程也就完成了使命。
- 本地方法栈:类似于虚拟机栈,只不过是线程调用本地方法Native时,发挥存储的作用。
- 程序计数器:空间很小,主要记录线程当前执行的字节码的行号,可以保证进行线程上下文切换后,恢复线程运行的现场。
JDK1.8前后,JVM内存区域的划分有了一些变化,如下图:

从上图中可以看到。JDK1.8的最大变化是使用元空间替掉了方法区(永久代),并且元空间不在JVM中,而是被划到了直接内存中。这样做使方法区不再受制于JVM限制的固定内存大小,避免方法区容易OOM的问题。另外,原先方法区的运行时常量池也被移到堆空间中。
JVM启动加载
1. JVM的启动
- 配置JVM装载环境。主要进行JVM.dll文件的查找和装载。
- 解析虚拟机参数。解析JavaVMOption,以便按照预设的参数启动JVM。
- 设置线程栈大小。解析ThreadStackSize。
- 执行Java main方法。通过ContinueInNewThread方法创建新线程,并执行JavaMain函数。
2. 对象的创建过程

- 类加载检查。遇到一条new指令,先根据指令参数到常量池中定位这个类的符号引用,若其未被加载、解析、初始化过,则执行类加载过程。
- 分配内存。在Java堆中为新生对象分配内存空间。内存分配方式:指针碰撞(内存无碎片);空闲列表(内存不规整)
- 初始化零值。将对象内存空间中对象体的位置初始化为零值。
- 设置对象头。设置对象头信息,包括类的元信息Klassword、标记字Markword(对象的hash值、GC分代年龄、锁状态等信息)。
- 执行init方法。虚拟机层面的创建完成后,再按照编程人员的意愿完成初始化,为对象的成员赋值。
3. 类的加载过程

- 加载。获取类定义的二进制字节流;转换成方法区的运行时数据结构;在内存中生成Class对象,作为方法区的访问入口。
- 验证。文件格式、元数据、字节码、符号引用的验证。
- 准备。正式为类变量分配内存并设置类变量初始值。
- 解析。虚拟机将常量池内的符号引用(目标的描述)替换为直接引用(指向目标的指针或句柄)。
- 初始化。执行类的构造方法,为对象的成员赋值。
-- 类加载器。JVM内置了三个类加载器,分别是BootstrapClassLoader(启动类加载器)、ExtensionClassLoader(扩展类加载器) 、AppClassLoader(应用程序类加载器) 。
BootstrapClassLoader负责加载JAVA_HOME目录下的lib中的jar包和相关类;ExtensionClassLoader负责加载JAVA_HOME目录下lib/ext中的扩展jar包;而AppClassLoader是面向用户的加载器,主要加载当前classpath下的所有jar包和类。根据需要,用户也可以通过继承classload类来自定义类加载器。
-- 双亲委派机制。类在加载前会判断是否曾被加载过,若加载过直接返回,否则才进行加载。加载时,会把加载的请求委派给当前类的父类,然后逐级向上委派,直至将请求委派到启动类加载器BootstrapClassLoader。只有当父类加载器无法加载时,才会自己进行加载。
JVM垃圾回收
1. 何为垃圾?(如何判断对象/常量/类已经失效/废弃?)
判断对象失效一般有两种办法:
- 引用计数法。采用引用计数器,即每当对象被引用,计数器加1;引用失效,计数器减1。计数器为0的对象即视为不再使用,可以被回收。该方法实现简单效率高,但若是对象间相互引用(对象a的引用指向对象b,同时对象b的引用指向对象a)时,该方法会失效。
- 可达性分析法。以一系列“GC Roots”对象(包括:虚拟机栈中引用的对象;方法区中静态属性引用的对象;方法区中常量引用的对象;本地方法栈中(Native方法)引用的对象)作为根节点,向下搜索引用路径,可以被搜索到处在引用链中的对象为有效对象,不再相连的对象即视为已失效。
判断常量废弃的依据:常量池中的常量不再被任务对象引用。
判断类废弃的依据:类的所有实例均已回收;类不再被任何地方,包括反射方式进行引用;类的加载器已被回收。
失效的对象不是“非死不可”:因为对象的回收并不是一次性完成的,被标记失效的对象会被至于待回收队列F-Queue中,然后判断对象在执行重写的finalize()方法中是否重新建立了与其他对象的引用,是则将该对象移出F-Queue队列;否则才会被二次标记,然后回收。
2. 垃圾收集算法
标记-清除算法。标记所有可回收对象,直接进行空间清理回收。算法的缺点效率较低下,且容易产生大量不连续的内存碎片。这会导致JVM在分配较大内存块给大对象时,无法获得连续的内存空间。
复制算法。将可用内存分成两等分,每次使用其中一块。当前块内存用完后,会将其中仍存活的对象复制到另一块空闲的内存块中,然后把当前内存块全部清理掉。此时进行指针交换,即当前块成为空闲内存块,而获得存活对象的内存块成为当前块。该算法的使效率明显提升,但同时也降低了内存的使用率,因为每次只能使用可用内存中的一半。对象存活率高时,复制成本将很大。
标记-整理算法。该方法是标记清除算法的一种改良,在将可回收对象标记后,不再选择直接清理空间,而是将各个存活对象向一端移动,最后清理掉边界以外的空间。该方法适用于对象存活率较高的老年代中。
分代收集算法。分代收集算法并不是一种新的收集算法思路,而是根据内存块中对象存活周期的不同场景将内存分成不同的区域,然后不同区域分别用不同的更适用的收集算法。如新生代中对象“朝生夕死”,存活率低,采用复制算法,复制成本低,清理效率高;而老年代中对象的存活率较高,适用标记-清除或标记-整理算法。
-- 补充:分代收集算法的过程是这样的:首先按分代收集算法的思路,堆内存被划分为新生代(eden、S0、S1)和老年代(Tentired)两部分的。一般情况下,新对象会被分配到新生代的eden中(超大的对象会直接分配到老年代),当eden的空间不足时,新生代会发生GC,即采用复制算法进行垃圾回收,这个过程被称为Minor GC(也被成为Young GC)。eden和S0中存活的对象会被复制到S1区域,然后清空eden和S0,再将S0和S1进行交换,使S0成为下一次新生代GC接受存活对象的容器S1。存活下来的对象年龄将+1,当年龄达到15岁时(Markword中用了4位记录对象年龄),对象会晋升到老年代。当S1的空间被填满后,会将所有对象转移到老年代。之所以使用两个Survivor区域(S0和S1),是为了保证Survivor区域也可以进行垃圾回收。当老年代的空间不足时,会触发Full GC(也被成为Major GC),如空间仍然不足,便会报OOM的异常。Full GC的速度往往会比Minor GC慢10倍,非常影响程序性能,所以进行Java程序调优时,一定要避免程序频繁的Full GC。
3. G1垃圾收集器
G1垃圾收集器是JDK1.9的默认垃圾收集器,采用的是分代收集算法。同时对分代算法进行了优化和改进,支持并行和并发,GC动作的停顿时间明显缩短。是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。
JVM调优
1. JVM常用参数
| 参数 | 描述 |
|---|---|
| -Xms | JVM启动时堆内存的初始化大小 |
| -Xmx | 堆内存的最大值 |
| -Xmn | 年轻代的空间大小 |
| -Xss | 线程的堆栈大小 |
| -XX:+PrintGCDetails | 打印GC详情 |
| -XX:+UseG1GC | 应用使用G1垃圾回收器 |
| -XX: NewSize | 年轻代大小 |
| -XX: PermSize | 持久代大小 |
| -XX: SurvivorRatio | Eden区与Survivor区的大小比值,一般设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 |
2. JDK命令行工具
- jps (JVM Process Status): 查看当前运行的所有 Java 进程及相关信息;
- jstat( JVM Statistics Monitoring Tool): 查看虚拟机各方面的运行数据,如
jstat -gcutil PID可以查看指定java进程的GC状况; - jinfo (Configuration Info for Java) : 显示虚拟机配置信息;
- jmap (Memory Map for Java) :查看当前堆的使用情况,如
jmap -heap PID; - jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
- jstack (Stack Trace for Java): 查看JVM当前的线程方法堆栈的集合,如
jstack PID。
3. JDK调优工具
- Visual VM。 通过Visual VM的图形用户界面,可以方便、快捷地查看多个Java应用程序的相关信息。
- Arthas。Alibaba开源的Java诊断工具,通过JDK提供的Instrument(基于Java Virtual Machine Tool Interface)和asm库(操作字节码),在低侵入的情况下,可以便捷的实现Java应用的在线状态查看和故障诊断等功能。官网入口
参考资料:(文章仅做交流学习,侵权即删!!)