java文件加载流程图
java文件 - javac命令编译 - 编译为class文件 - classLoader加载class文件 - class load到内存
- 字节码解释器/JIT即时编译器 - 执行迎请 - OS - 硬件
跨平台 - linux - windows - mac上都能执行
虽然它叫jvm,但jvm与java无关,它是一种规范,能在jvm上运行的语言目前已有100多种,
只要你符合class文件规范,能够编译为class文件,那么你就可以在jvm上执行。
相当于虚构出来的一台计算机,有自己的操作系统
有自己的字节码指令集(汇编语言)、有自己的内存管理:栈、堆、方法区等。
常见的JVM实现
JVM就像是一个接口,有很多实现类一样。通过jvm的规范,来给他具体的实现。
为什么会有这么多实现?因为这些大公司肯定是要有自己的虚拟机的,没必要都依赖于oracle,因为他们也不比oracle公司小。
HotSpot - oracle官方,也就是我们用的
Jrockit - 曾经号称世界上最快的,被oracle收购了,已和HotSpot合并了。
J9-IBM - IBM公司
MicrosoftVM - 微软
TaoBaoVM - 淘宝HotSpot深度定制版
LiquidVM - 直接针对硬件的虚拟机,下面没有操作系统,直接是硬件
azul zing - 最新垃圾回收的业界标杆(收费的商业版本),速度快。HotSpot的zgc就是参考了它。
还有很多。。。。。。
Java收费,我该怎么办?
其实不是java要收费,java是一门语言怎么可能收费呢?实际上是oracle自己开发的jvm实现要收费了。
就像上面介绍的,jvm实现那么多,oracle收费了,我们用其他的就行了,哈哈。
JDK - JRE - JVM
JDK(Java Development Kit) = jre + development kit
JRE(Java Runtime Environment) = jvm + core lib
JVM(java virtual machine)
jvm:它只是来执行的,你把class文件给他,他给你执行。
jre:它是java运行环境。java的一些核心类库得有(java.util、Object类等等等等)
jre是面向Java程序的使用者,而不是开发者。它不包含开发工具(编译器、调试器等)。
jdk:它包含了jre,包含了jvm,针对Java开发的产品,它是Java开发工具包,
它提供了Java的开发环境(提供了编译器javac等工具,用于将java文件编译为class文件)和
运行环境(提供了JVM和Runtime辅助包,用于解析class文件使其得到运行)。
Class文件加载过程
loading:加载。将class文件,二进制的,加载到内存
verification:校验。检测class文件符不符合class文件标准。
preparation:准备。将class文件的静态变量赋默认值。默认值的概念就比如int、double等他们的默认值是0。
然后引用类型不赋值的时候,那就是空值,默认值就是null。
resolution:解析。它会将类、方法、属性等符号引用解析为直接引用。class文件常量池里用到的各种符号
引用解析为指针、偏移量等内存地址的直接引用。什么是符号引用呢?就是指向一个类,开始的时候并不是直接
指向内存的,是先指向一个类名,比如指向一个字符串java.lang.xxx,然后这个解析的过程就是讲这个指向
字符串,解析为指向到内存中的java.lang.xxx。
initializing:初始化。静态变量赋值,赋初始值,调用类初始化代码。这里就是将上面那块赋默认值,这里
就是真正的赋值了。比如int i = 10;这里就是赋10了,不是上面的0了。
针对以上的过程,模拟一道题来看它的过程:
public class TestLoad {
public static void main(String [] args){
System.out.println(T.count);
}
}
class T {
public static T t = new T();
public static int count = 2;
privaate T(){ count++; }
}
通过代码来看count会输出几:答案是2。t在new的时候,先赋默认值为null,count先赋默认值为0。
执行new T()的时候,执行了构造方法,将count++,此时count为1,这时对count进行赋值操作,
将count的值赋为2。那么将count放上面,new T()放下面。
class T {
public static int count = 2;
public static T t = new T();
privaate T(){ count++; }
}
此时,count会输出3.先对count赋默认值0,t赋默认值null,然后对count进行赋值为2,再然后new T(),
执行构造方法,将count++,这时count输出3。
当然这只是一道面试题,如果真正写程序,赋值用static静态代码块赋值就好了。
类加载器 - ClassLoader
每个class文件都是被classloader加载到内存的。它是一个抽象类。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
。。。。。。省略
}
Launcher类,它里面有好几个内部类,下面是其中的2个
static class AppClassLoader extends URLClassLoader
static class ExtClassLoader extends URLClassLoader
System.out.println(String.class.getClassLoader()); //输出null,c++实现,java没有与其对应的
System.out.println(Server.class.getClassLoader()); //输出sun.misc.Launcher$AppClassLoader@18b4aac2
加载到内存之后,它是创建了2块内容,一块是class文件的二进制内容,一块是生成了一个class对象。
这个class对象,指向了那块二进制的内容。
如上图,有4个类加载器。实际上还有很多。最上面是顶层,最下面是下层。他们之间是一层层的父子关系,
但注意不是继承。和java里的继承不是一个概念。
String的类加载器输出null,说明是在顶层的加载器。C++实现的类加载器。bootstrap
Server的类加载器输出的是代表使用了App加载器,在上图都有文字说明。
为什么说是父子关系,但不是继承。比如:
AppClassLoader和ExtensionClassLoader是父子关系。
那么ExtensionClassLoader类中有一个AppClassLoader类型的变量,就是这种关系。
System.out.println(Server.class.getClassLoader().getParent());
输出:sun.misc.Launcher$ExtClassLoader@5b37e0d2
可以看到Server本来的类加载器是App,getParent以后,输出Ext了。
双亲委派
大概就是上图里由下至上来看,每个加载器,相当于有一块缓存(也就是一个容器,集合),缓存里都是他们各自要加载的class,
比如加载一个class,先从自定义类加载器的缓存里去找,看有没有这个class,如果没有找到这个class,
会委派给它的父亲(父加载器),也就是App类加载器去寻找,如果App类加载器的缓存里也没有这个class,那么会
继续向上,App的父亲(父加载器)Ext去寻找,直到找到Bootstrap,如果Bootstrap最后也没有找到,那么会一级一级
的向下传递,Bootstrap会告诉Ext,我这里没有这个class,并且这个class不应该由我来加载,同样的,
Ext会告诉App,App会告诉自定义类加载器,最后再由自定义类加载器去加载这个class。
为什么叫双亲?因为它有一个从子到父的过程,也有一个从父到子的过程。它和现实生活中的双亲并以一样的。
为什么要搞这个双亲委派?直接加载不就好了?
主要是为了安全。次要是防止重复加载。
jvm如何认定两个对象同属于一个类型,必须同时满足下面两个条件:
1、都是用同名的类完成实例化的。
2、两个实例各自对应的同名的类的加载器必须是同一个。比如两个相同名字的类,
一个是用系统加载器加载的,一个扩展类加载器加载的,两个类生成的对象将被jvm认定为不同类型的对象。
所以,为了系统类的安全,类似“java.lang.Object”这种核心类,jvm需要保证他们生成的对象都会被认定
为同一种类型。即“通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,
保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的”。
那么能不能不使用这种双亲委派的模式?不行,源码给写死了。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//这个getClassLoadingLock方法,它里面有一个map,就是去寻找class在不在map里的意思
synchronized (getClassLoadingLock(name)) {
//首先,检查你是不是已经加载进来了
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {//如果没有加载进来
long t0 = System.nanoTime();
try {
//private final ClassLoader parent;
//这个parent,就是它的父加载器,开始委派的过程了。看到parent都是final的了,肯定改不了了
if (parent != null) {
//这里就是一样的了,也是loadClass方法
c = parent.loadClass(name, false);
} else {
//如果parent为null,就再次去查找自己是否被加载过了
//这个findBootstrapClassOrNull方法,实际上就是调用了findLoadedClass(上面的)方法
//只不过里面加了一个对name的非空判断。底层调用了c++的方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果找不到,那就要自己去加载了
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//这个方法里面什么都没有,直接throw了一个ClassNotFoundException
//这里就体现了一个设计模式了,模板方法
//看之前的双亲委派,寻找,这一系列的方法都写好了,
//那么实现自定义类加载器,只需要实现findClass方法就可以了
//这里调用findClass是肯定可以找到的,因为层层调用,调父类加载器的findClass,
//然后再调用父类加载器的findClass,所以找不到,这里就抛出异常了。
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;//这里将class返回
}
}
那么可以打破双亲委派的规则吗?当然是可以的了
热部署、tomcat的jsp文件实时更新,这都是打破了双亲委派的规则。
Java内存模型 - JMM
缓存行对齐
读取缓存以cache line为基本单位,目前是64bytes,为一行
也就是读取缓存的时候,不是只读取某个缓存,而是一批,一行,这个也被成为伪共享。
就是位于同一缓存行的两个不同的数据,被两个不同的cpu锁定,产生互相影响的伪共享问题。
JVM运行时数据区
每一个线程都有自己的PC(程序计数器)、VMS(jvm stack)、NMS(c++管理的栈)。
他们共享的是堆和方法区(Method Area,1.8之前叫Perm Space,1.8之后叫Meta Space)。
为什么每个线程都要有自己的PC?因为涉及到线程切换问题。cpu执行某个线程的某个位置切换到另一个线程中
去执行,再切换回来时,执行到哪里了,就要根据PC来找到。所以每个线程都要有自己的PC。
PC - 程序计数器 - Program Counter
程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
> 它存放的是指令位置
> 虚拟机的运行,类似于这样的循环:
> while( not end ) {
> 取PC中的位置,找到对应位置的指令;
>
> 执行该指令;
>
> PC ++;
> }
JVM Stack - 栈
每个线程对应一个栈。一个线程栈里包含一个一个的栈帧(Frame)。栈帧里包含主要的是上图的4个。
栈帧:
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,栈帧中存储了方法的局部变量表,
操作数栈,动态连接,和方法返回地址等信息。在程序编译时,栈帧中需要多大的局部变量表,多深的操作数栈
都已经完全确定了,并且写在方法表的 Code 属性中。
每个方法对应一个栈帧
1. Local Variable Table - 局部变量表(方法内部变量)
2. Operand Stack - 操作数栈
压栈和出栈
对于long的处理(store and load),多数虚拟机的实现都是原子的
jls 17.7,没必要加volatile
3. Dynamic Linking - 动态链接
动态链接是一个将符号引用解析为直接引用的过程。java虚拟机执行字节码时,遇到一个操作码,
操作码第一次使用一个指向另一类的符号引用,则虚拟机就必须解析这个符号引用。解析时需要执行三个基本的任务:
(1)查找被引用的类(有必要的话就装载它,一般采用延时装载)。
(2)将符号引用替换为直接引用,这样当再次遇到相同的引用时,可以使用这个直接引用,省去再次解析的步骤。
(3)当java虚拟机解析一个符号引用时,class文件检查器的第四趟扫描确保了这个引用时合法的。
4. return address - 返回值地址
a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方
Native Method Stack - 栈
这个栈等同于上面的栈。它是调用c++的native方法的栈。由c++控制。
Direct Memory - 直接内存
一般的话,内存都由jvm管理。为了增加IO效率,1.4之后增加了它。直接可以访问操作系统管理的内存。它不归
jvm管理,归操作系统管理。
> JVM可以直接访问的内核空间的内存 (OS 管理的内存)
> NIO,提高效率,实现zero copy
Method Area - 方法区
方法区(Method Area)是 Java虚拟机规范中定义的运行时数据区域之一,它与堆(heap)一样在线程之间共享。
方法区主要用来存储运行时常量池、静态变量、类信息、JIT编译后的代码等数据。
方法区是 JVM 规范所描述的抽象概念,在实际的 JVM 实现中,它不一定是由单一的特殊区域所实现,
不同的实现可以放在不同的地方。
1、类型信息:
类的完整名称
类的直接父类的完整名称
类的直接实现接口的有序列表
类型标志(类类型还是接口类型)
类的修饰符(public private defautl abstract final static)
2、类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(字符串、整数、浮点数)和对其他类型、字段、方法的
符号引用。
3、字段信息(该类声明的所有字段)
字段修饰符(public、peotect、private、default)
字段的类型
字段名称
4、方法信息
方法信息中包含类的所有方法。
方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表
5、类变量(静态变量)
6、指向类加载器的引用
7、指向Class实例的引用
8、方法表
9、运行时常量池(Runtime Constant Pool)
不同版本的实现
1. Perm Space (<1.8版本之前)
字符串常量位于PermSpace
FGC不会清理
大小启动的时候指定,不能变
2. Meta Space (>=1.8版本)
字符串常量位于堆
会触发FGC清理
不设定的话,最大就是物理内存
Runtime Constant Pool - 常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、
接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量
和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时
会抛出OutOfMemoryError异常。
GC - Garbage Collection
什么是垃圾?没有引用指向的任何对象,就是垃圾。
如何找到垃圾?一般有两种算法。
1、reference count - 引用计数
一个对象,有一个引用指向它,就记为1,有两个引用指向它,记为2。没有引用记为0。但它不能解决的一种
情况是循环引用。a引用b,b引用c,c引用a。
2、Root searchering - 根可达算法
哪些是根对象?比如程序启动,main方法会有一个线程,线程里有栈,栈里有栈帧。main方法里引用的对象,
就是根对象。一个class,class里的静态变量,指向的对象也是根对象。常量池,常量池里引用的对象也是
根对象。还有JNI,调用本地方法c++的方法,用到的对象,也是根对象。
从根上对象开始搜索,通过根对象来查找。一环连着一环的引用。假设有2个对象相互引用,但是通过根对象
找不到它,就像上图里的两个对象,那么它会被定为垃圾。
GC回收概念
凡是在年轻代回收的,叫MinorGC或YGC。
凡是在老年代或者整个区域回收的,叫MajorGC或FullGC。
常见垃圾回收器
- Serial
历史:JDK诞生,Serial追随,提高效率,诞生了PS,为了配合CMS,诞生了PN,CMS是1.4版本后期引入,
CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS的。
并发垃圾回收是因为无法忍受STW(stop the world)。
Serial:单线程清理。safe point(安全点),清理垃圾之前,所有线程执行到安全点停止,开始清理垃圾,
清理垃圾完成,所有线程继续执行。
- Serial Old
Serial Old:也是单线程的。
Serial Old 收集器是在 TenuredGeneration 老年代上实现收集的,Serial Old 收集器所使用的垃圾回收
算法是标记-压缩-清理算法。在回收阶段,将标记对象越过堆的空闲区移动到堆的另一端,所有被移动的对象的
引用也会被更新指向新的位置。
- Parallel Scavenge
多线程执行回收。如果jvm什么都不设置,默认使用的就是PS+PO
Parallel Old
多线程的。使用整理算法。
ParNew
与PS的区别只是做了一些增强。以便和CMS配合使用。
CMS - ConcurrentMarkSweep
垃圾回收和应用程序同时运行,降低STW的时间(200ms)
CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定
CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,
使用SerialOld进行老年代回收。
堆内逻辑分区(不适用不分代垃圾收集器)
部分垃圾回收器使用的模型,因为有的不是分代处理的。
除Epsilon、ZGC、Shenandoah之外的GC都是使用逻辑分代模型,G1是逻辑分代,物理不分代
除此之外不仅逻辑分代,而且物理分代。
逻辑分代是把堆分为新生代和老年代。1:3的关系。默认的。但可以通过参数改变。
新生代:刚刚new出来的对象。
老年代:垃圾回收很多次,都没回收掉。
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1。
Eden区是真正new出对象往里扔的那块区域。
为啥默认会是这个比例?因为年轻代中的对象基本都是朝生夕死的(80%以上)。存活下来的很少。
一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,
如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,
当它的年龄增加到一定程度时,就会被移动到年老代中。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们
的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会
被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。
这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次
GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,
直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
一个对象的这一辈子
我是一个普通的java对象,出生以后尝试栈上分配,如果分配不下,我会进入在Eden区,
在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。
有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,
自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,
居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,
人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
GC- Algorithms - 常见垃圾回收算法
- Mark - Sweep 标记清除算法
**
通过根对象查找,将有用的标记出来,没用的标记出来,最后将没用的清除。在存活对象比较多的情况下效率较高。
它要通过两遍扫描(有用的找一遍,没用的找一遍并清理),执行效率偏低,容易产生碎片(碎片就是回收后的空闲位置)。
- Copying - 拷贝
将内存一分为二,将有用的全都拷贝到第二部分,然后将第一部分全部清理掉。
适用于存活对象较少的情况,只扫描一次,效率提高,不会产生碎片。
但会造成空间浪费,移动复制对象,需要调整对象的引用。因此这种算法就很适合Eden区。
- Mark - Compact - 标记压缩
将有用的全都移动到最前面。不会产生碎片,方便对象分配,不会产生内存减半。
但也是会扫描两次,需要移动对象,执行效率偏低。
JVM调优
调优前的基础概念
1. 吞吐量:用户代码时间 /(用户代码执行时间 + 垃圾回收时间)
吞吐量是指对网络、设备、端口、虚电路或其他设施,单位时间内成功地传送数据的数量
(以比特、字节、分组等测量)。
2. 响应时间:STW越短,响应时间越好
所谓调优,首先确定,追求啥?吞吐量优先,还是响应时间优先?
还是在满足一定的响应时间的情况下,要求达到多大的吞吐量...
吞吐量优先的一般:(PS + PO)
响应时间:网站、GUI、API (1.8 G1)
什么是调优?
1. 根据需求进行JVM规划和预调优
2. 优化运行JVM运行环境(慢,卡顿)
3. 解决JVM运行过程中出现的各种问题(OOM)
调优,从规划开始
* 调优,从业务场景开始,没有业务场景的调优都是耍流氓
* 无监控(压力测试,能看到结果),不调优
* 步骤:
1. 熟悉业务场景,选定垃圾回收器(没有最好的垃圾回收器,只有最合适的垃圾回收器)
1. 响应时间、停顿时间 [CMS G1 ZGC] (需要给用户作响应)
2. 吞吐量 = 用户时间 /( 用户时间 + GC时间) [PS]
2. 选择回收器组合
3. 计算内存需求
4. 选定CPU(越高越好)
5. 设定年代大小、升级年龄
6. 设定日志参数
1. -Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
2. 或者每天产生一个日志文件
7. 观察日志情况
观察虚拟机配置
java -XX:+PrintCommandLineFlags -version
对象占用大小
1.对象头:markword 8字节
2.ClassPointer指针:-XX:+UseCompressedClassPointers 为4字节 不开启为8字节,默认开启
3. 实例数据
1. 引用类型:-XX:+UseCompressedOops 为4字节 不开启为8字节,默认开启
Oops Ordinary Object Pointers
4. Padding对齐,8的倍数
这俩参数控制对象指针和实例数据是否进行压缩
对象何时进入老年代
超过指定次数(YGC)
Parallel Scavenge收集器 15次
CMS收集器 6次
G1 15次
使用这个参数指定:XX:MaxTenuringThreshold
动态年龄
s1 > s2超过50%
把年龄最大的放入老年代
常见垃圾回收器组合参数设定:(1.8)
使用何种垃圾回收器
-XX:+UseSerialGC = Serial New (DefNew) + Serial Old
小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
-XX:+UseParNewGC = ParNew + SerialOld
这个组合已经很少用(在某些版本中已经废弃,很少用了)
-XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
-XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
-XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
这两个其实是一样的
-XX:+UseG1GC = G1
* Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
* java +XX:+PrintCommandLineFlags -version
* 通过GC的日志来分辨
* Linux下1.8版本默认的垃圾回收器到底是什么?
* 1.8.0_181 默认(看不出来)Copy MarkCompact
* 1.8.0_222 默认 PS + PO
HotSpot参数分类
JVM的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
标准参数: - 开头,所有的HotSpot都支持
非标准参数:-X 开头,特定版本HotSpot支持特定命令
不稳定参数:-XX 开头,下个版本可能取消
java -version
java -X
常见参数
1. 区分概念:内存泄漏memory leak,内存溢出out of memory
内存泄漏就是一直占用,不回收。
内存溢出就是内存撑不住了,撑爆了。
2. java -XX:+PrintCommandLineFlags HelloGC
3. java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
PrintGCDetails(GC详细信息) PrintGCTimeStamps(GC时间) PrintGCCauses(GC产生原因)
Xmn:新生代的大小
Xms:最小堆大小,这两个参数一般设成一样的。
Xmx:最大堆大小,这两个参数一般设成一样的。
PrintGC:打印GC回收的信息
4. java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags HelloGC
使用cms,查看GC
5. java -XX:+PrintFlagsInitial 默认参数值
6. java -XX:+PrintFlagsFinal 最终参数值
7. java -XX:+PrintFlagsFinal | grep xxx 找到对应的参数
8. java -XX:+PrintFlagsFinal -version |grep GC
GC日志 - PS+PO日志
每种垃圾回收器的日志格式是不同的!
Times:user用户态占用时间,sys内核态占用时间,real总共占用时间。
heap dump部分:
eden space 5632K, 94% used [0x00000000ff980000,0x00000000ffeb3e28,0x00000000fff00000)
后面的内存地址指的是, 起始地址, 使用空间结束地址, 整体空间结束地址
Metaspace:元数据区
class space:存储class数据的区域。
total = eden + 1个survivor
问题排查定位
top命令观察到问题:内存不断增长 CPU占用率居高不下
top -Hp 观察进程中的线程,哪个线程CPU和内存占比高
使用方法top -Hp PID(程序的PID),会输出占比详情,每个线程有一个PID。
jstack 定位线程状况,重点关注:WAITING BLOCKED
jstack能列出线程信息
waiting on <0x0000000088ca3310> (a java.lang.Object)
假如有一个进程中100个线程,很多线程都在waiting on <xx> ,一定要找到是哪个线程持有这把锁
怎么找?搜索jstack dump的信息,找<xx> ,看哪个线程持有这把锁RUNNABLE
为什么阿里规范里规定,线程的名称(尤其是线程池)都要写有意义的名称
怎么样自定义线程池里的线程名称?(自定义ThreadFactory)
所以使用线程池,一定要指定线程名,方便问题排查。
jconsole远程连接\jvisualvm远程连接,需要加入参数。启动JMX协议。但是一般不用。只用在测试
java -Djava.rmi.server.hostname=192.168.17.11
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=11111
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false XXX(可以加其他参数,和项目名称)
jmap命令
jmap - histo 4655(PID) | head -20 查找有多少对象产生
jmap生成文件
jmap -dump:format=b,file=xxx路径 pid
线上系统,内存特别大,jmap执行期间会对进程产生很大影响,甚至卡顿(电商不适合)
1:设定了参数HeapDump,OOM的时候会自动产生堆转储文件
2:很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
3:在线定位(一般小点儿公司用不到)
java -Xms20M -Xmx20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError xxxxxxxxx
栈溢出问题
-Xss设定太小