以下内容基于Java 8分析和理解。
Java Platform Standard Edition 8 Documentation 官网文档地址:
https://docs.oracle.com/javase/8/
下图是Java
概念图的描述
一. 编译
JVM运行的是.class
文件,我们编写的是.java
文件,所以我们需要将.java
文件编译成.class
文件,然后.class
文件被加载到JVM中运行。
假如我们现有一个Person.java
类,如下:
public class Person {
public static final String TAG = "Person";
public int id = 100;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
通过javac
命令编译成Person.class
文件,使用二进制编辑器打开如下:
cafe babe 0000 0034 001d 0a00 0500 1709
0004 0018 0900 0400 1907 001a 0700 1b01
0003 5441 4701 0012 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 0100 0d43 6f6e
7374 616e 7456 616c 7565 0800 1c01 0002
6964 0100 0149 0100 046e 616d 6501 0006
3c69 6e69 743e 0100 0328 2956 0100 0443
6f64 6501 000f 4c69 6e65 4e75 6d62 6572
5461 626c 6501 0007 6765 744e 616d 6501
0014 2829 4c6a 6176 612f 6c61 6e67 2f53
7472 696e 673b 0100 0773 6574 4e61 6d65
0100 1528 4c6a 6176 612f 6c61 6e67 2f53
7472 696e 673b 2956 0100 0a53 6f75 7263
6546 696c 6501 000b 5065 7273 6f6e 2e6a
6176 610c 000d 000e 0c00 0a00 0b0c 000c
0007 0100 1263 6f6d 2f65 7878 2f65 7878
2f50 6572 736f 6e01 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 0650 6572
736f 6e00 2100 0400 0500 0000 0300 1900
0600 0700 0100 0800 0000 0200 0900 0100
0a00 0b00 0000 0200 0c00 0700 0000 0300
0100 0d00 0e00 0100 0f00 0000 2700 0200
0100 0000 0b2a b700 012a 1064 b500 02b1
0000 0001 0010 0000 000a 0002 0000 0008
0004 000a 0001 0011 0012 0001 000f 0000
001d 0001 0001 0000 0005 2ab4 0003 b000
0000 0100 1000 0000 0600 0100 0000 0e00
0100 1300 1400 0100 0f00 0000 2200 0200
0200 0000 062a 2bb5 0003 b100 0000 0100
1000 0000 0a00 0200 0000 1200 0500 1300
0100 1500 0000 0200 16
编译器的编译原理大致如下:
- 对源文件进行词法和语法分析。
- 形成语法树。
- 通过字节码生成器生成
Person.class
字节码文件。
我们看不懂这些字节码文件,但是JVM是可以看懂的,那JVM是怎样“看”的呢?
通过官网文档Java虚拟机规范:
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
找到第四章节,Chapter 4. The class File Format(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html),来了解一个.class
文件是怎样被定义的。
A class file consists of a single ClassFile structure:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
第一行的U4是什么意思?可以简单理解成:每2位16进制表示U1,图解如下:
U4
那么U4表示的是:cafe babe
,代表的即是magic
,那magic
是什么意思呢?
通过文档可知:
The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE.
magic
是用来对.class
文件标识的,换言之就是只要是.class
文件,它的开头一定是cafe babe
。
再举栗分析一下minor_version 和 major_version:
minor_version
major_version
The values of the minor_version and major_version items are the minor and major version numbers of this class file
minor_version 和 major_version,表示.class
文件的次要版本和主要版本。
其他结构按照同样的方式进行分析,这里不再一一分析了。
这部分就是.java
文件到.class
文件的一个过程。
接下来我们考虑下,当有了.class
文件后,是怎样交给JVM
运行的呢?是把整个.class
文件直接加载到JVM
中吗?显然不是的,接下来我们分析下.class
文件加载到JVM
中。
显然它是需要一种机制来加载进JVM
中的,并不是随便想怎样加载就怎样加载。
这个机制是什么呢?答案就是:类加载机制
二. 类加载机制
它的作用就是将我们的.class
文件交给JVM
。
过程是怎样的?
通过官网文档第5章节可以了解到:
Chapter 5. Loading, Linking, and Initializing
The Java Virtual Machine dynamically
loads
,links
andinitializes
classes and interfaces. Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation. Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed. Initialization of a class or interface consists of executing the class or interface initialization method.
意思是类加载分为5个步骤:
- 装载
- 链接
- 初始化
- 绑定本地方法的实现
- 退出JVM虚拟机
接下来详细了解下这5个步骤:
- 装载(Loading)
通过官网了解到,JVM提供了2中类型的类加载器:-
Bootstrap ClassLoader
, 由JVM内置提供加载器的类型 -
User-defined ClassLoader
,由用户自定义加载器的类型,每个用户自定义的类加载器都需要继承抽象类ClassLoader
,例如可以用来加载网络下载的,动态生成的或者从加密文件中解密的。
-
通过不同类型的类加载器来加载不同区域的类。
JVM内置提供加载器的类型,主要有以下3个类加载器来实现的:
-
Bootstrap ClassLoader
,用于加载$JAVA_HOME中的 jre/lib/rt.jar中的所有class,或者Xbootclassoath选项指定的jar包下的class。 -
Extension ClassLoader
,加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。 -
App ClassLoader
,加载ClassPath中指定的jar包及Djava.class.path所指定目录下的class和jar包。
用户自定义加载器的类型:
-
Custom ClassLoader
,继承抽象类ClassLoader
实现自定义加载器。
装载过程:
1. 查找.class
的全路径。
2. 将类信息加载进JVM中。
3. 将.class
对应的类加载进JVM中。
这里需要说明一点,第3步只能装载一次,如果存在多个,则会造成运行时混乱。
如何保证.class
对应的类只装载一次?这里就要说下类加载机制中的委托机制。
我们从源码的角度来看下:
protected Class<?> loadClass(String class_name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(class_name)) {
Class c = this.findLoadedClass(class_name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (this.parent != null) {
c = this.parent.loadClass(class_name, false);
} else {
c = this.findBootstrapClassOrNull(class_name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(class_name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
this.resolveClass(c);
}
return c;
}
}
- 判断是否已经加载过,如果已经加载过直接返回,如果没有加载过,进入第2步。
- 判断是否有父类,如果有,则委托父类去加载,如果没有父类,则表示该类已经是最顶层的类加载器
Bootstrap ClassLoader
。 -
如果经过上述2步还没有加载到,需要自己加载。
流程图大致如下:
类加载器委托机制
找到对应的Class
之后,要怎样装载呢?
- 将类文件静态存储结构以流的形式加载到JVM中的方法区中,这里存储的是类结构的信息,比如,创建时间,作者等信息。
- 将类文件对应的对象加载到JVM中的堆(Heap)区中。
接下来进行链接
部分:
- 验证
主要是验证确保类或者接口二进制在结构上的正确性。 - 准备
为类或者接口创建静态字段,并设置字段的默认值。
举栗子:代码中有一个静态变量:static int KEY = 20;
到这一步只会给KEY赋值默认值,即KEY=0 - 解析
将符号引用解析为动态确定值(直接引用)的过程。
这一步会给第2步的KEY赋值确定的值,即KEY=20。
链接完成之后进入到初始化
步骤:
初始化:
执行类或接口的初始化方法,静态变量赋值。
经过装载
,链接
,初始化
上述步骤之后,一个class
文件就被加载到JVM中。
在之后就来到了JVM运行时数据区了。
三.JVM运行时数据区
记录的是代码在执行时的状态。
有几个问题想一下:
类信息应该加载到哪里?常量应该加载到哪里?方法应该加载到哪里?等等。
带着上面的问题,我们想一下,如果要分类加载到JVM不同区域中,那么首先我们要对JVM区域进行划分,这里我们直接说结果,官网文档,Chapter 2. The Structure of the Java Virtual Machine#2.5. Run-Time Data Areas,这一节可以看到:
- The pc Register(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1)
程序计数器,用于记录每个线程当前正在执行的程序的位置。
- Java Virtual Machine Stacks(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.2)
Java 虚拟机栈,用于处理方法,执行方法入栈,执行结束出栈。线程私有,线程安全,会出现StackOverflowError异常,当没有足够的Memory创建Java虚拟机栈时也会出现OutOfMemoryError异常。
- Heap(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.3)
堆,用于存放所有的class对象以及数组。所有线程共享,非线程安全,会出现OutOfMemoryError异常。
- Method Area(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
方法区,用于存放类的结构信息,常量,静态变量,构造函数,特殊初始化方法以及即时编译后的代码等。所有线程共享,非线程安全,会出现OutOfMemoryError异常。
- Run-Time Constant Pool(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.5)
常量池,由方法区创建的,用于存放常量,准确应该和方法区放到一起。
- Native Method Stacks(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.6)
本地方法栈,用于存放Java
和Native
交互的方法,当大小固定时会出现StackOverflowError,当可以动态扩展时,会出现OutOfMemoryError。
接下来详细聊一下JVM。
- Java虚拟机栈 -- 栈帧
何为栈帧?每一个线程都有一个私有的Java虚拟机栈,当执行到某一个方法时,就将一个栈帧压入Java虚拟机栈,而这个栈帧就代表一个方法的执行。
每一个栈帧由五部分组成:
- 局部变量表
局部变量表中维护的是一个数组,这个数组的长度在编译期就确定了。其中有基本类型的变量,返回值类型的变量以及引用对象的变量。
其中引用对象的变量指向的是堆内存中的一个内存地址。
单个局部变量可以存储:boolean, byte, char, short, int, float, reference, or returnAddress
一对局部变量可以存储:long or double
当然也包含this
。
- 操作数栈
一个后进先出(LIFO)堆栈,最大栈深也是在编译器确定的。对每一个指令进行入栈和出栈操作。
- 动态链接
在类加载的时候会将一些元信息进行解析,一些能够确定的类型,在类加载的过程中就确定了,不确定的类型就是需要在运行时才可以确定类型的,就是在此时进行一个动态链接,来确定具体的类型。
- 方法调用正常完成
栈帧正常执行结束。
- 方法调用异常完成
栈帧异常执行结束。
接下来我们结合字节码指令来看下,栈帧是怎样工作的。
Java 代码:
public int plus(int op1, int op2) {
op1 = 3;
int result = op1 + op2;
return result;
}
我们放一张Java虚拟机栈中的该方法的栈帧图,如下:
虚拟机栈-栈帧
字节码反编译后指令:
public int plus(int, int);
Code:
0: iconst_3
1: istore_1
2: iload_1
3: iload_2
4: iadd
5: istore_3
6: iload_3
7: ireturn
我们“翻译”下这段字节码:
iconst_3
表示将int类型的常量3压入操作数栈
istore_1
对操作数栈进行弹出,将弹出的值赋值给局部变量表索引为1的局部变量。
iload_1
将局部变量表中索引为1的变量的值压入操作数栈。
iload_2
将局部变量表中索引为2的变量的值压入操作数栈。
iadd
将操作数栈中的值弹出来进行相加后,得到的结果再入栈。
istore_3
对操作数栈弹出操作,然后将弹出的值赋值给局部变量表中索引为3的变量。
iload_3
将局部变量表中索引为3的变量的值压入操作数栈。
ireturn
将操作数栈的值弹出并返回给方法。
这里我们想一个问题,我们有一个方法里面会声明一个对象,用于参与计算,那么这个对象是怎样创建的呢?
很显然对象都是在堆中创建的,那么我们的栈帧中的局部变量中是如果使用这个对象的呢?就是在局部变量表中有一个引用指向了堆。大致如下图:
布局变量表-堆
这里就会有另一个问题,那么堆是怎样知道自己要创建什么类型的对象呢?
答案就是堆中对象实际上是指向了方法区中的对象所对应的类信息,一个Java对象的内存布局大致如下图:
Java对象内存布局
那如果一个类中的静态变量指向了一个对象呢 ?这种情况是怎么样的?
public class Person{
public static Object tag = new Object();
}
这种情况下,即是方法区也指向了堆。总结以上的2个问题,得出如下结论:
- 虚拟机栈可以指向堆,即局部变量表中的对象;
- 方法区可以指向堆,静态对象;
- 堆可以指向方法区(通过class pointer 指向java对象所对应的类信息内存地址),创建对象需要知道对象的具体类型,而这些对象的类信息是存储在方法区中的。
总结如下:
虚拟机栈总结
以上我们理解并分析了Java运行时的数据区,那么当处于非运行时的一个状态,或者说内存分布,或者说物理上的内存分布的一个模型,那么接下来我们就来理解和分析下Java的内存模型,即JMM。
四. JMM
那么在运行时数据区中的方法区和堆,在实际中的一个物理落地应该是怎样的呢?
为什么这里只分析方法区和堆的一个物理分布的落地,因为运行时数据区中的程序计数器,java虚拟机栈以及本地方法栈都是在创建一个线程时才有的一个状态,当程序执行结束,这个线程也就结束了。而方法区和堆是在JVM创建的时候就需要存在的,它俩跟随的是JVM的进程,生命周期更久。
那这个JMM的是怎样划分的呢?我们可以通过Java自带的工具可以清楚的看到它的一个内存分布的状态。
启动Java Visual VM :
jvisualvm
然后通过安装Visual GC
插件来观察它的一个内存模型。
JMM分布如下图:
JMM
我们通过上图来分析,为什么这样设计内存模型。
- JMM 组成:
- Metaspace
- Old
- Eden
- S0
- S1
方法区和堆我们来找下对应关系:
1. 方法区 --> Metaspace
2. 堆 --> Old,Eden,S0,S1
我们从方法区和堆来进行分析和理解JMM。
如下图:
如上图,我们的对象都存放在堆中,如果触发GC,就需要对整个堆内存进行扫描,然后就行垃圾回收。假如我们的堆内存是2G,那这种情况下,显然是不合理的。
需要将堆进行区域划分,这样我们将活跃的对象丢到一个区域中,不活跃的丢到另一个区,如下:
old-young
那么问题来了,我们应该有一个什么规则来判断一个对象应该在
Old
区还是在Young
区呢?就是给每一个对象加上年龄的标识,如果年龄大于15,则丢到
Old
老年区,否则丢到Young
新生区。
后面我们会知道,这个年龄其实就是这个对象被GC 回收的次数(回收一次加1),前面我们了解到了每个对象都有一个对象头信息,这个
年龄
就存放在头信息中的->Mark Word
中的分代年龄信息中。
其实还有一种特殊的情况,就是一开始创建对象的时候,这个对象需要的内存就非常大,超过了Yong
区的大小,这个时候这个新创建的对象就要丢到Old
区,如果Old
区也放不下它,只能报OutOfMemoryError
了。
这样就看似解决了问题,但是如果遇到这种问题我们应该怎么处理?
假如有一个对象大小为3个单元,但是Young
区的总大小是能够放得下的,可是连续的空间放不下这个对象,所以会造成创建对象失败,需要进行垃圾回收,垃圾回收就要启动垃圾回收线程,这个时候我们的业务代码的线程就要被迫停掉,这不是我们想要看到的,所以我们一直提倡减少垃圾回收的频率。
如下图:
针对以上空间不连续(碎片)情况需要对Young
区进一步来优化,如下:
我们都只到大部分对象的生命周期是很短的,大部分都是“朝生夕死”的,基于这样一个前提,我们来分析这样划分的好处。
当发生垃圾回收的时候,在 Eden
区少量没有被回收掉的对象,会丢到Survivor
区,这样下来,基本上保证了Eden
区的连续性。但是垃圾回收的过程是对整个Young
区进行回收的,所以刚刚在Eden
区存在的空间不连续(碎片)问题,在Survivor
区同样也存在,问题又来了,那怎样解决这个问题呢?
同样我们可以对Survivor
区在进行划分,如下图:
我们来分析下这样划分的好处。
当触发垃圾回收,我们将Eden
区少量没有被回收的对象全部丢到Survivor0
区,这个时候Eden
区和Survivor1
区是空间连续的。当再次触发垃圾回收,我们将Eden
区少量没有被回收的对象和Survivor0
区少量没有被回收的对象全部放到Survivor1
区中,这样就保证了Eden
区和Survivor0
区是连续可用的空间,再次发生垃圾回收的时候也是同样的操作,这样始终都保持Survivor0
区和Survivor1
区一定有一个是连续可用的空的空间,这样就解决了空间不连续(碎片)的问题。
通常Eden:Survivor0:Survivor1 = 8:1:1,这里分给了Eden更多,是因为大多数对象都是”朝生夕死”的,这样分配效率更高。
Survivor0(S0),Survivor1(S1)
那如果S0或S1的空间放不下没有被回收的对象呢?对,找Old
老年代借点空间,比如:S区需要12M的空间,但是目前只有10M,差了2M,这个时候找老年代借2M空间,这就是所谓的“担保机制”。如果S区的对象的年龄大于15了,就需要移到Old
老年代区了。
我们画一个流程图来加深下印象。
创建对象
五、垃圾回收
上一个章节已经说到了对象的创建,内存模型,对垃圾回收没这么详细说,接下来我们就详细说下垃圾回收。
-
什么样的对象才是垃圾?
- 引用计数
如果一个对象被引用的计数为0,则判定为垃圾对象,因为没有其他地方在引用它了,但会存在循环引用的情况。 - 可达性分析
首先要确定GCRoot对象,然后由GCRoot出发,是否能到达某一个对象(是否有引用),如果不到达,则为垃圾对象。
GCRoot对象特征: 生命周期要长,而且要有存在的意义。
可以作为GCRoot的对象:- 虚拟机栈中的本地变量
- 静态变量
- 常量
- 本地方法栈中的变量
- 类加载器
- 线程(Thread)
- 引用计数
-
该如何进行回收?
垃圾回收算法:-
标记回收算法
内存空间不连续,碎片化较严重,效率较低。
-
复制算法
使空间连续,解决碎片化的问题。弊端就是浪费空间,需要把空间一分为二,保证其中有一块空间是空的。
-
标记整理算法
不用分割空间,直接在原有的基础上进行标记,回收对象之后,将所有存活的对象进行重新整理到一起,达到空间连续的目的。
-
-
垃圾收集器
需要根据不同的分代来采用不同来及回收器。比如说,新生代使用什么垃圾回收器,老年代使用什么垃圾回收器?
新生代,使用复制回收算法,因为新生代的对象大部分都会被回收掉,存活时间很多,只有少部分对象存活,复制的成本不高。
老年代,老年代中的对象都是经过了15次回收之后还存在的对象,生命力顽强,即使再回收几次也可能还是一样的结果,所以这里并不适合使用复制回收算法,标记算法更合适,不管是标记清除还是标记整理都可以。
所以针对不同的算法去实现不同的垃圾回收器即可。
复制回收算法实现的垃圾回收器:Serial,ParNew,ParallelScavenge(关注的是吞吐量)等。
标记算法实现的垃圾回收器:CMS(Concurrent Mark Sweep,关注的是停顿时间),Serial Old,Parallel Old(关注的也是吞吐量)等。 -
优势和劣势
各种垃圾回收器的优劣势。
Serial:单线程执行,复制回收算法的实现,需要暂停业务线程。
ParNew:多线程执行,复制回收算法的实现,需要暂停业务线程。
ParallelScavenge:多线程执行,复制回收算法的实现,需要暂停业务线程,更加关注吞吐量(吞吐量=业务线程执行的时间/(业务线程执行的时间+垃圾回收的时间))。
Serial Old:和Serial特性一样,只不过是标记-清除算法的实现。
ParallelOld: 和ParallelOScavenge的特性基本一致,只不过是标记-清除算法的实现,更加关注吞吐量。
CMS:并行之心的垃圾回收器,它是标记-清除算法的实现,更加关注的是暂停时间,属于并发垃圾收集器,它的线程是和业务线程一起运行的。
工作模型如下:
CMS工作模型
G1(Garbage-First):它会尝试最短的暂停时间以及更高的吞吐量,这个时间是可配置的,也是标记-清除算法的实现,JDK1.9之后默认的垃圾回收器为G1.
工作模型如下:
对比下CMS会发现多了一步筛选回收,为什么不对CMS进行优化下呢?原因是因为CMS的堆内存布局不满足G1的需要,所以G1对堆内存重新进行了布局。
G1加入了区域的概念(Region),对堆内存进行了重新的布局,在逻辑上还是存在Young,Old, Eden区,但是实际物理上来说已经不是隔离开的了。
-
Region
堆内存被分成了很多块大小相同的Region区域,每一块的Region区域都是连续的虚拟内存。
Region
G1通过执行并发的全局标记来确认整个堆内存中的存活对象,标记阶段结束之后,G1就知道哪些区域大部分是空的,然后优先回收那些产生垃圾的Region区域,这也就是为什么叫垃圾优先了,从名字上也可以看出。G1会将1个或多个Region区域复制到单独的一个Region区域,并且在这个过程中会压缩和释放内存,进一步来减少碎片。执行垃圾回收的过程是利用多处理器进行并行回收,以此来减少垃圾回收带来的暂停时间。
当如果出现以下3个方面时,应该更倾向于使用G1垃圾回收器。
- Java的堆内存被实时数据占用超过50%;
- 对象分配率或提升率差异很大;
- 当应用程序的垃圾回收暂停时间大于0.5到1秒;
所以综合来说,G1是为更短的GC暂停时间而生的。
这里可以顺便说下JVM调优维度:
- GC收集器的停顿时间和吞吐量
两个指标:- 停顿时间:垃圾回收器进行垃圾回收暂停的时间,时间越短越好。
- 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收暂停时间),吞吐量越大越好。
二者需要动态调整,达到一个平衡点。
- 内存使用率
垃圾回收器分类:
- 停顿时间短的垃圾收集器:CMS(20ms),G1(可以设置暂停时间,并不是严格上的15ms),适用于Web应用,属于并发类的收集器。
- 吞吐量更好的垃圾收集器:Parallel Scanvent,Parallel Old,适用于后端跑任务,和用户很少交互或者没有交互的,属于并行类的收集器。
- 串行收集器:Serial,Serial Old,内存占用比较小,适用于嵌入式开发,属于串行类的收集器。
垃圾收集器的选择:
查看当前进程使用的垃圾收集器的类型:
终端执行以下命令,查看当前运行的进程:
jps -l
根据进程ID来查看当前进程的GC类型, 以G1垃圾收集器为例:
jinfo -flag UseG1GC <pid>
如果是-号则不是,+号代表正在使用还有一种方式就是打印JVM的启动参数:
jcmd <pid> VM.flags
最后放一张垃圾回收器的分类图:
GC类型图
- 查看垃圾回收器日志文件
JVM官网参数地址:点击查看
附上一些常用的JVM参数:
- 标准参数,这些参数通常是很稳定的,不会随着JDK的变化而变化
java -version //查看当前JDK的版本
java -help //查看帮助
等。
- -X 参数,分标准参数,会随着JDK的版本的变动而变动。
java -Xint -version //改变JVM虚拟鸡的模式 -Xint 解释执行 -Xcomp 编译执行 -Xmixed 混合执行
- -XX 参数
1. Boolean 类型
-XX:[+/-]name //这里的+/-表示启用或者停用
比如设置垃圾收集器为:G1
-XX:+UseG1GC
2. 非Boolean类型
-XX:name=value //name属性名,value属性值
比如设置堆内存最大空间:
-XX:MaxHeapSize=100M
- 其他参数
-Xms100M //初始化的堆内存大小
-Xmx100M //最大堆内存大小
-Xss100K //初始化栈帧的深度
常用参数如下:
-
启动某一进程时打印启动参数:
java -XX:+PrintFlagsFinal -version
-
查看当前Java进程
jps
-
查看或者实时修改JVM参数
jinfo -flag <参数名> <PID>
查看所有参数
jinfo -flags <PID>
例如:查看是否使用了G1垃圾收集器
jinfo -flag UseG1GC 1135
实时修改JVM参数值,条件就是该参数是可以修改的,也就是【manageable】
jinfo -flag key=value <PID>
例如修改 -
查看JVM中某指标体状态和信息(jstat)
查看JVM class 装载的情况
jstat -class <PID> 1000 10,意思是查看JVM类装载情况 每隔1000ms输出一次,共输出10次。
查看GC状态
jstat -gc <PID> 1000 10
查看线程堆栈信息,方便排查线程相关的情况,例如:死锁
jstack <PID>
查看堆内存的相关信息,方便排查分析OOM
jmap 生成堆内存的快照
jmap -heap <PID>
dump 文件
jmap -dump:format=b,file=heap.hprof <PID>
设置当出现OOM时自动dump堆内存信息
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof -
*.hprof文件分析工具
JDK 自带工具(bin/目录下)
- jconsole Java监视和管理控制台
启动命令
jconsole
- jvisualvm JVM虚拟机可视化工具
启动命令
jvisualvm
- JMC(Java Mission Control),使用 Java Management Extensions (JMX) 代理连接到 JVM
- Arthas
好了,由于个人能力有限,暂时先分析到这里,下面我们贴图总结下:
总览图 JMM JVM全貌 新对象分配对象流程图
有些图片来源网络,侵删!!!
网友评论