这两天看了一些有关JVM的东西,在这里以自己的理解做一个简单的记录。
jvm的主要构成有:
1、类加载器
2、运行时数据区(内存结构)
3、执行引擎
这里主要对1、2以及jvm垃圾回收进行一些简单的说明。
====================================
运行时数据区(内存结构):
======= 线程之间共享 =======
堆:存储单位,虚拟机启动时自动创建,存放对象的实例,几乎所有的对象都存放在堆上。是垃圾回收的主要区域。主要分为新生代和老年代。
新生代:
Eden: (伊甸园)new Obejct会在这个区域,如果这个对象比较大则会从这里进入到老年代(分配担保机制,后面会有提到)。如果Eden放满了则进行垃圾回收(GC-->YGC执行时间特别短),回收那个没有被引用的对象,如果对象被引用了则将对象放到From区域,如果From也放满了也会产生YGC,没有被回收的对象会进入到To区域,然后将To区域变成From区域,原来的From区域会变成To区域。【GC的复制算法】
From:(幸存者s0区域)
To:(幸存者s1区域)
老年代:在新生代存活的时间比较长的对象会被转移到这里。new Object这个对象比较大也会进入到老年代(分配担保机制,如果一次申请对象在Eden发生溢出,则占用Eden的超过45%【应该是45%】则会从Eden转入到老年代)。这里存放存活时间长的,存储空间比较大的对象,如果老年代满了则产生垃圾回收(Full-GC也叫Major-GC),也会触发YGC。**JVM调优:减少Full-GC的执行次数,减少Full-GC执行的时间。**
Full-GC:串行,执行Full-GC的时候会停止所有的用户线程。等待回收完毕之后用户线程才开始执行。
方法区:存储类的所有字段和方法(静态变量,常量,类信息【构造方法,结构】,运行时常量池) 为了和Java的堆区分开来【堆外内存】
持久代【元空间】:对方法区的一种实现 jdk1.8
======= 线程私有 =======
// jvm自动管理,会被回收
栈:包含多个栈帧(一个栈帧对应一个方法),栈帧内包含局部变量,操作数栈,动态链接(运行的过程中可以加载类,将符号引用变成直接引用),方法出口(返回地址,调用者地址)
javac xxx.java // 编译生成.class文件
javap -c xxx.class // 对代码进行反汇编 -- jvm指令集
javap -v xxx.class // 和 -c 类似但是有一些附加的信息
本地方法栈:存储本地方法的栈, 用c/c++区实现的一些方法,本地磁盘库里面的一些方法,java去调度
程序计数器:jvm指令的编号,为了找到下一行的指令。
====================================
JVM性能调优监控工具:
jinfo -flags 进程号 // 查看项目对应的jvm参数(堆内存,垃圾回收机制等。),进程号可以通过jps查看
jstat -gc 进程号 // 查看堆中的内存区域的信息
jmap // 查看堆内存使用情况
jmap -histo 进程号 // 堆的对象统计 num序号,instances实例数量,bytes占用空间大小,Class Name类名
jmap -dump:format=b,file=xxx.hprof 进程号 // 对某一时刻的堆做一个快照
// 内存溢出时候,自动导出dump文件(项目内存很大的时候,可能导出失败)
// 以下是jvm参数 【通过 java -jar jvm参数 进行设置】
-XX:+HeapDumpOnOutOFMemoryError
-XX:HeapDumpPath="文件输出的路径" // 和jmap导出的一样
使用JDK自带的工具去分析dump文件,jdk/bin/jvisualvm.exe(这个软件)
jstack 进程号 // 程序当前信息
====================================
类加载机制:
类的生命周期:
加载-->连接【验证,准备,解析】-->初始化-->使用-->卸载
类加载机制是 加载,连接,初始化这三个阶段,经过这三个阶段后就被加载到内存中
加载:将class文件从磁盘读取到内存当中(包名+类名)
连接:
验证:验证字节码文件(class文件)的正确性、
准备:给类的静态变量分配内存,并且赋予默认值(系统默认的值,例如int是0)
解析:类装载器装入类所引用的其他所有类(静态连接)
初始化:为类的静态变量赋予真正的值(代码中定义的值),执行静态代码块
类加载器的种类:(从上到下)
启动类加载器(bootstrap ClassLoader C语言编写,无法打印,使用jps可以查看):负责加载jre的核心类库
扩展类加载器(extension ClassLoader):负责加载jre扩展目录ext中jar包
系统类加载器(application ClassLoader):负责加载ClassPath路径下的包 -- 自定义类一般用这个
用户自定义加载器(user ClassLoader):负责加载用户自定义路径下的包
类加载机制:
全盘负责委托机制:由调用者的类加载器来加载。假设A使用的是类加载器*@,A调用了B,没有为B指定类加载器,那么B也使用类加载器*@
双亲委派机制(父类委派):当一个类加载器受到了一个类加载的请求,自己不会执行类的加载,将类的加载委托给父类加载器(逐层向上委托,直到application ClassLoader),然后逐层传递给下一层的类加载器,直到这个类可以被加载器加载。 防止用户自定义的类干扰(安全,防止核心类库被篡改)、避免类的重复加载(父类加载过这个类后,子类不需要重新加载),假设自定义了java.lang.String 和系统中的 java.lang.String就发生了冲突,但是由于是双亲委派机制,所以自定义的java.lang.String无法运行
打破双亲委派机制(tomcat类加载机制)
====================================
GC算法和收集器:
// 判断废弃对象
引用计数器法:给对象一个计数器,有地方引用这个对象,计数器加1,引用失效就减1,当计数器为0,进行GC的时候就清除这个对象。 -- 问题:循环引用无法被回收
可达性分析法:GCroots作为根,搜索整个树,在这个树中的所有节点都不会GC回收。
可以当做GCroots节点的有:类加载器(ClassLoader)、线程(Thread)、虚拟机栈局部变量表、static成员、常量引用、本地方法栈等。
**当一个对象被回收之前会调用finalize方法,可以重写这个方法,完成对象被回收前的自救。这个方法只会执行一次,也就说明一个对象可以被自救一次。
// 判断常量是废弃常量
和引用计数法类似,没有任何对象引用该常量的话,则这个就是废弃常量。
// 如何判断一个类是无用的类,满足三个条件:
1、该类的所有实例都被回收
2、该类的类加载器被回收
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法:
标记清除算法:标记需要被回收的对象,然后清除。 标记和清除两个过程的效率都不高,标记清除之后会产生大量的不连续的碎片,如果存储较大的对象则会出现一次GC。
复制算法:将内存分为两块【使用内存,保留内存】,一块存放对象,对象会被标记,当这一块使用完毕之后,就会将存活的对象复制到另外一个区域内(复制之后的存储地址是连续的),然后将这个区域清理掉。内存的利用率不高。Eden的From和To使用这个算法。
标记整理:标记完进行整理,让存活的对象的地址是连续的。代价比较大。停顿时间不可避免。
分代收集算法:根据每个代的特点,使用不同的垃圾回收算法。
垃圾收集器:(不同的收集器使用的GC算法不一样)
Serial收集器:串行收集器,单线程收集器,执行GC的时候会将应用程序给停止掉,因此STW(Stop the world)停顿时间会比较长 **应用程序-->GC线程-->应用线程**。 **这个垃圾收集器在新生代使用的是复制算法,在老年代使用的标记-整理算法**。 优点:没有线程交互的开销,简单高效。 缺点:STW时间比较长。
ParNew收集器:是Serial的多线程版本,**应用程序-->多个GC线程并发-->应用程序**。 **新生代使用复制算法,老年代使用标记整理算法**。没有完全实现并行。在执行GC的时候会停止用户线程,但是多个GC线程是并发的(同时执行)。
Parallel Scavenge收集器:**jdk8默认收集器**,类似于ParNew。**新生代使用复制算法,老年代使用标记整理算法**完全实现了**并行**。用户线程和GC线程交替执行。
SerialOld:Serial的老年代版本,单线程收集器。
Parallel Old:Parallel Scavenge的老年代版本,使用多线程和标记整理算法。
**CMS收集器:以获取最短回收停顿时间为目标的收集器,真正的**并发**。四个阶段:
1、初始标记阶段:暂停其他所有线程,记录下所有与GCroot相连的对象,速度非常快【只做标记,不做清除】
2、并发标记阶段:同时开启GC线程和用户线程,这里用户线程还是会产生一些垃圾,GC线程会对用户线程在这里产生的垃圾进行标记。【只做标记,不做清除】
3、重新标记阶段:对所有的垃圾进行再次的标记,确保万无一失。比初始标记的时间长,比并发标记的时间短。(多线程)【只做标记,不做清除】
4、并发清除:开启用户线程,同时GC线程开始对标记的区域做清扫。(这里用户线程会产生垃圾【浮动垃圾】,无法及时处理)
优点:并发,低停顿
缺点:对CPU资源敏感【要执行用户线程和GC线程,对CPU要求高】、无法处理浮动垃圾【运行过程中产生的垃圾】、回收算法采用“标记-清楚”结束后到导致大量的碎片,会导致垃圾收集的次数增多。
**G1收集器:针对服务器的垃圾处理器,多处理器大内存,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能的特征。**将内存划分成Region(一块块的区域),区域分为Eden、Survivor、Old、Humongous**。不再有分配担保机制了,而是使用Humongous【比较大的对象存储在这里】。 在空白的区域创建对象,这个区域就变成了Eden,不是直接将区域进行划分。Eden和Survivor和之前的类似,Old存储一些存活的比较久的对象。 **G1从整体看是标记-整理法**。 除非项目出现致命问题【例如内存溢出】,否则一般情况先不会出现Full-GC,在G1收集器里面的GC是 Mix-GC。
与CMS相比有一个比较大的优势就是,**可预测的停顿**,G1除了追求低停顿以外,还能建立可以预测的停顿时间模型,**用户可以指定垃圾回收的停顿时间**。
步骤如下:(和CMS类似)
1、初始标记
2、并发标记
3、最终标记(重新标记)
4、筛选回收:根据用户指定的回收停顿时间,优先选择回收价值最大的Region(例如占内存比较多)
缺点:Mix-GC执行的次数比较多
如何选择垃圾收集器:
1、优先调整堆的大小,让服务器自己来选择
2、如果内存小于100M,优先选择串行收集器
3、如果是单核,没有停顿时间的要求,可以选择串行或者由JVM自己选择
4、如果允许停顿时间超过1秒,选择并行或者让JVM自己选择
5、如果相应时间最重要,并且不能超过一秒,选择并发收集器(推荐使用G1,性能高)
====================================
JVM调优:
// 两个指标,主要是为了减少Full-GC次数
1、停顿时间:垃圾收集器回收垃圾时将用户线程暂停的时间。 -XX:MaxGCPauseMillis
2、吞吐量:垃圾收集时间和总时间的占比:1/(1+n),吞吐量为1-[1/(1+n)]。 -XX:GCTimeRatio=n
//调优步骤:
1、打印GC日志:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:path // path是日志保存路径
例如: java -jar -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/srv/gc.log mypro.jar
2、分析日志得到的关键性指标。(网上有图形化的工具,将GC日志导入,可以得到图形化,例如GCeasy)
3、分析GC的原因,调优JVM(图形,配合GC源日志)
**// 如果硬件无法达到,可能会起到相反的效果,所以调优是经验之谈**
JVM常用参数:(列举了几个)
增大元空间大小:-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64m
增大年轻代动态扩容量(默认20%),可以减少GC:-XX:YoungGenerationSizeIncrement=40
设置收集器:
-XX:+UseConcMarkSweepGC (CMS收集器)
-XX:+UserG1GC (G1收集器)
大概就看了这么多,之后有用到的会再进行补充,有哪里理解错误的欢迎大家指出。