【Java jvm工作原理 全面详细】简单理解

一、JVM基础知识

(一)java跨平台解释

注意:我们提到java跨平台性,就会想到JVM,但能跨平台的是java程序,而不是JVM。JVM是用 C/C++ 开发的,是编译后的机器码,不同平台装有不同版本的JVM,我们编写的java代码,也叫源码,编译后生成 .class文件,也叫字节码文件。

java虚拟机就是负责将字节码文件翻译成特定的机器码然后运行。也就是说,只要在不同平台安装对应的JVM,我们编写的java程序没做任何改变,仅仅通过JVM’这一’中间层‘就可以跨平台做到“一次编译,到处运行”的目的。

(二)JVM介绍

JVM:java Vir Machine(java虚拟机)
JVM是java的核心和基础,在java编译器和os平台之间的虚拟的处理器。它是用软件方法实现的的抽象计算机基于下层的操作系统和硬件平台,可以在上面执行java程序。
jVM有自己完善的硬件架构。如:处理器,堆栈,寄存器,还具有相关的指令等。

(三)JDK、JRE、JVM三者关系

在这里插入图片描述
JDK是Java开发工具包,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。

JRE(java runtime environment):
JRE是Java的运行环境,包括JVM标准实现及Java核心类库。

JVM(java virtual machine):
JVM是java虚拟机,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。

(四)JVM的生命周期
启动和消亡:

JVM负责运行一个java程序。当启动一个java程序,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也随之消亡。

JVM运行起点:

java虚拟机实例通过某个初始类 main() 方法 来调用一个java程序。而这个main() 方法必须是静态的,共有的,返回值为void,并且接受一个字符串为参数的方法。任何拥有这样一个 main() 方法的类都可以作为java程序的运行起点。

JVM的两种线程:

守护线程和非守护线程。守护线程通常都是由JVM虚拟机自己使用的,比如执行垃圾的回收任务的线程。
只要还有非守护线程在运行,那么这个java程序也在继续运行,当该程序的所有非守护线程都终止时,虚拟机实例也将自动退出。假若安全管理器允许,程序本身也可以通过调用Runtime类或者System类exit() 方法来退出。

二、JVM的工作机制(类加载机制)

(一)类加载时机

虚拟机规范则是严格规定了有且只有5种情况必须对类进行初始化 (class文件加载到JVM中)

  • 创建对象实例:new对象的时候,会对类进行初始化,前提这个类没有被初始化
  • 调用类的静态方法
  • 调用类的静态属性或者为静态属性赋值
  • 通过class 文件反射对象
  • 初始化一个类的子类:使用子类时候先初始化父类

不会初始化的情况:

  • 同一个加载器只能初始化一个类一次
  • 在编译时候可以确定下来的静态变量(编译常量),如:fina修饰的静态变量
(二)类加载器

在这里插入图片描述
各个加载器的工作职责:
Bootstrap ClassLoader:

  • 负责加载JAVA_HOME中的jre/lib/.jar里面所有的calss,由c++ 实现的,不是ClassLoader的子类

Extension ClassLoader:

  • 负责加载java平台中扩展功能的一些jar包,包括JAVA_HOME 中的jre/lib/.jar 或者 Djava.exit.dirs 指定目录下的 jar

App ClassLoader:

  • 负责加载classpath指定的jar包及目录中的class

    (三)双亲委派模型

    工作过程:
    1、当前类加载器从自己已经加载的类中去查看是否有此类,如果已经加载了则返回原来已经加载的类。
    2、如果没有找到,就委托父类去加载,父类也会采取同样的策略,直到委托到启动类加载器为止,因为父类加载器为空了,就代表用启动类加载器作为父加载器去加载该类
    3、如果启动类加载器加载失败,就会使用扩展类加载器来加载,继续失败就会用AppClassLoader来加载,继续失败就会抛出异常:ClassNotFoundException
    使用双亲委派的好处:
    1、安全性:避免用户自己编写的类动态替换java的一些核心的类。如果不采用双亲委派模型加载类,那我们就可以随时的使用自定义的类来动态替换java核心APL中定义的类。例如:如果黑客把“代码病毒”植入到自定义的String类中,随后类加载器加载到JVM中,那么就会对JVM产生病毒攻击。
    双亲委派就可以避免这些情况发生,因为Sring类在启动时就被引导类加载器进行加载了
    2、避免重复加载类。因为JVM判定两个类是不是同一个类,不仅仅是根据类名,还需要判断加载该类的类加载器是不是同一个类加载器,相同的class文件会被不同的类加载器加载得到不同结果就是两个不同的类。

    (四)类加载详细过程

    在这里插入图片描述
    加载器加载到JVM中,又分为好几个步骤:
    加载,查找并加载类的二进制数据,在java堆中创建一个java.lang.Class的对象。连接分为以下三块内容:
    1、验证:文件格式,元数据,字节码,符号引用验证
    2、准备:为类的静态变量分配内存空间,并将其初始化为默认值
    3、解析:把类中的符号引用转换为直接引用,
    初始化,为类的静态变量赋予正确的初始值。

举例1:加载Test类

class Test{
       static  Object  instance = new Object();
       //加载类Test,验证完成,准备阶段只是分配内存: instance,并未赋值
       //在图中,初始化阶段才进行赋值 new Object()操作;
}
//调用
Test.instance

举例2:单例模式(安全)

class Single{
   private static Object instance;
   static{
        instance = new Object();
   }
   public static Single getInstance(){
           return instance;
   }
}
//调用
Single.getInstance();
  • 为什么上面这种单例模式安全呢?

回答:Single里面的instance变量是静态的,又有静态代码块;
在第一次调用Single.getInstance();时候,类Single进行加载,准备阶段就把instance内存分配出来,在初始化节点把instance实例化;在Single.getInstance()时候,实例就已经创建出来了。不会再次给我们创建,从而保证安全。

三、JVM的工作过程(内存模型)

在这里插入图片描述

  • 从这几个方面去分析:功能、是否线程共享、生命周期、抛出异常
(一)程序计数器
  • 程序计数器:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
  • 主要作用:
    1、字节码解释器通过改变程序计数器来依次读取指令,从而实现流程控制,如:顺序执行、选择、循环、异常处理等
    2、在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程运行到哪里了。
  • 注意:程序计数器是唯一一个不会抛出OutOfMemoryError(内存溢出)的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡
(二)虚拟机栈

在这里插入图片描述

  • 与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期和线程相同(实际上java虚拟机栈是由一个个栈帧组成,而每个栈帧都拥有:局部变量表,操作数栈,动态链接,方法出口信息)

  • 栈帧是什么:通俗来说,我们程序运行无非就是方法调用方法的过程,那每一个新的方法都会形成一个栈桢

  • 局部变量表:包含八大原始类型、对象的引用(地址)、returnAdress(返回地址信息)

  • 操作数栈:一般存放一些临时变量,运算等等!

  • 动态链接:比方说我们要加载一个视频,其实视频也是一些代码写成的,我们就通过动态链接方式来加载这些视频。其实我们系统程序下某些文件,以:.dll文件,这些就是加载动态链接的。

  • 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError

StackOverFlowError:
若java虚拟机栈的内存大小不允许动态扩展,当线程请求的深度超过java虚拟机栈的最大深度时,就会抛出StackOverFlowError异常

OutOfMemoryError:
若java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,就会抛出OutOfMemoryError异常

(三)本地方法栈

和虚拟机栈发挥作用很类似
区别:java虚拟机栈执行java方法(也就是字节码文件),而本地方法栈则为虚拟机使用到的Native方法服务
其余和java虚拟机栈类似。

(四)堆

在这里插入图片描述
java虚拟机所管理的内存最大的一块,java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是:存放实例对象
其次,java堆是垃圾回收器管理的主要区域,因此也被称作GC堆。
(上面区域的含义,作用等后面垃圾回收内容时候讲,到时候会更新)

(五)方法区

方法区和java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量,及时编译器编译后的代码等数据。

四、java命令

  • 作为一个合格的开发人员,不仅要能写好代码,还有一项很重要的技能就是排查问题。这里提到的排查问题不仅仅是在coding的过程中debug等,还包括的就是线上问题的排查。由于在生产环境中,
    一般没办法debug,所以我们需要借助一些常用命令来查看运行时的具体情况,这些运行时信息包括但不限于运行日志、异常堆栈、堆使用情况、GC情况、JVM参数情况、线程情况等。

那么我们来介绍常用的Java命令,这些命令都是被存放在JDK安装目录的bin目录中,下面来介绍一下相关命令以及具体使用方式

jps:显示所有java进程pid
jps(Java Virtual Machine Process Status
Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况
jps类似linux/unix平台上上的ps命令,但是jps只查找查找所有的Java应用程序,包括即使没有使用java执行体的那种(例如,定制的启动器)。另外,jps仅查找当前用户的Java进程,而不是当前系统中的所有进程。
在这里插入图片描述

  • 参数介绍
    -q 只显示pid,不显示class名称,jar文件名和传递给main 方法的参数
    -m 输出传递给main 方法的参数,在嵌入式jvm上可能是null
    -l 输出应用程序main class的完整package名 或者 应用程序的jar文件完整路径名
    -v 输出传递给JVM的参数

  • 常用指令
    jps:显示当前用户的所有java进程的PID
    jps -v 3331:显示虚拟机参数
    jps -m 3331:显示传递给main()函数的参数
    jps -l 3331:显示主类的全路径

jinfo:实时查看和调整虚拟机参数
配置信息包括JAVA系统参数和命令行参数,如果运行在64位虚拟机上运行,需要指定-J-d64参数,
如:jinfo -J-d64 -sysprops pid
由于打印jvm常用信息可以使用Jps命令,并且在后续的java版本中可能不再支持(注:jdk8中已经不支持该命令)
查看帮助:jinfo -help
在这里插入图片描述
• 常用指令
jinfo -flag CMSIniniatingOccupancyFration
1444:查询CMSIniniatingOccupancyFration参数值

jstat:监控虚拟机各种运行状态信息
jstat(JVM Statistics Monitoring Tool)是用于监控虚拟机各种运行状态信息的命令行工具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形的服务器上,它是运行期定位虚拟机性能问题的首选工具。
在这里插入图片描述
利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。可见,Jstat是轻量级的、专门针对JVM的工具,非常适用。

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

• 参数解释

Option —
选项,我们一般使用 -gcutil 查看gc情况
vmid — VM的进程号,即当前运行的java进程号
interval–
间隔时间,单位为秒或者毫秒
count —
打印次数,如果缺省则打印无数次
  • 参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次

假设需要每250毫秒查询一次进程5828垃圾收集状况,一共查询5次,那命令行如下

jstat -gc 5828 250 5

option:
选项option代表这用户希望查询的虚拟机信息,主要分为3类:类装载、垃圾收集和运行期编译状况,具体选项及作用如下:

–class 监视类装载、卸载数量、总空间及类装载所耗费的时间
–gc 监视Java堆状况,包括Eden区、2个Survivor区、老年代、永久代等的容量
–gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大和最小空间
–gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
–gccause 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
–gcnew 监视新生代GC的状况
–gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大和最小空间
–gcold 监视老年代GC的状况
–gcoldcapacity 监视内容与——gcold基本相同,输出主要关注使用到的最大和最小空间
–gcpermcapacity 输出永久代使用到的最大和最小空间
–compiler 输出JIT编译器编译过的方法、耗时等信息
–printcompilation 输出已经被JIT编译的方法
  • 常用指令
    jstat–class <pid> : 显示加载class的数量,及所占空间等信息
Loaded          装载的类的数量
Bytes             装载类所占用的字节数
Unloaded       卸载类的数量
Bytes            卸载类的字节数
Time             装载和卸载类所花费的时间

jstat -compiler <pid>:显示VM实时编译的数量等信息

Compiled       编译任务执行数量
Failed            编译任务执行失败数量
Invalid           编译任务执行失效数量
Time              编译任务消耗时间
FailedType   最后一个编译失败任务的类型
FailedMethod 最后一个编译失败任务所在的类及方法

jstat -gc <pid>:可以显示gc的信息,查看gc的次数,及时间

S0C               年轻代中第一个survivor(幸存区)的容量 (字节)
S1C               年轻代中第二个survivor(幸存区)的容量 (字节)
S0U        年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
S1U        年轻代中第二个survivor(幸存区)目前已使用空间 (字节) 
EC         年轻代中Eden(伊甸园)的容量 (字节)
EU         年轻代中Eden(伊甸园)目前已使用空间 (字节) 
OC         Old代的容量 (字节) 
OU         Old代目前已使用空间 (字节) 
PC          Perm(持久代)的容量 (字节)
PU          Perm(持久代)目前已使用空间 (字节)
YGC      从应用程序启动到采样时年轻代中gc次数
YGCT    从应用程序启动到采样时年轻代中gc所用时间(s)
FGC      从应用程序启动到采样时old代(全gc)gc次数
FGCT     从应用程序启动到采样时old代(全gc)gc所用时间(s) 
GCT       从应用程序启动到采样时gc用的总时间(s)

jstat -gccapacity <pid>:可以显示VM内存中三代(young,old,perm)对象的使用和占用大小

NGCMN       年轻代(young)中初始化(最小)的大小(字节)
NGCMX       年轻代(young)的最大容量 (字节)
NGC             年轻代(young)中当前的容量 (字节)
S0C                      年轻代中第一个survivor(幸存区)的容量 (字节)
S1C                     年轻代中第二个survivor(幸存区)的容量 (字节)
EC                 年轻代中Eden(伊甸园)的容量 (字节)
OGCMN       old代中初始化(最小)的大小 (字节)
OGCMX       old代的最大容量(字节)
OGC             old代当前新生成的容量 (字节)
OC                Old代的容量 (字节) 
PGCMN       perm代中初始化(最小)的大小 (字节)
PGCMX       perm代的最大容量 (字节)
PGC             perm代当前新生成的容量 (字节)
PC                Perm(持久代)的容量 (字节)
YGC             从应用程序启动到采样时年轻代中gc次数
FGC             从应用程序启动到采样时old代(全gc)gc次数

jstat -gcutil <pid>:统计gc信息

S0           年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
S1           年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
E            年轻代中Eden(伊甸园)已使用的占当前容量百分比
O            old代已使用的占当前容量百分比
P             perm代已使用的占当前容量百分比
YGC     从应用程序启动到采样时年轻代中gc次数
YGCT   从应用程序启动到采样时年轻代中gc所用时间(s) 
FGC       从应用程序启动到采样时old代(全gc)gc次数
FGCT    从应用程序启动到采样时old代(全gc)gc所用时间(s) 
GCT       从应用程序启动到采样时gc用的总时间(s)
jstat -gcnew <pid>:年轻代对象的信息
S0C       年轻代中第一个survivor(幸存区)的容量 (字节) 
S1C       年轻代中第二个survivor(幸存区)的容量 (字节) 
S0U       年轻代中第一个survivor(幸存区)目前已使用空间 (字节) 
S1U       年轻代中第二个survivor(幸存区)目前已使用空间 (字节) 
TT         持有次数限制
MTT      最大持有次数限制
EC         年轻代中Eden(伊甸园)的容量 (字节) 
EU         年轻代中Eden(伊甸园)目前已使用空间 (字节) 
YGC     从应用程序启动到采样时年轻代中gc次数
YGCT   从应用程序启动到采样时年轻代中gc所用时间(s)

jstat -gcnewcapacity<pid>:年轻代对象的信息及其占用量。

NGCMN      年轻代(young)中初始化(最小)的大小(字节) 
NGCMX      年轻代(young)的最大容量 (字节) 
NGC            年轻代(young)中当前的容量 (字节) 
S0CMX       年轻代中第一个survivor(幸存区)的最大容量 (字节) 
S0C       
年轻代中第一个survivor(幸存区)的容量 (字节) 
S1CMX 年轻代中第二个survivor(幸存区)的最大容量 (字节) 
S1C       年轻代中第二个survivor(幸存区)的容量 (字节) 
ECMX  年轻代中Eden(伊甸园)的最大容量 (字节) 
EC         年轻代中Eden(伊甸园)的容量 (字节) 
YGC     从应用程序启动到采样时年轻代中gc次数
FGC      从应用程序启动到采样时old代(全gc)gc次数

jstat -gcold <pid>:old代对象的信息。

PC         Perm(持久代)的容量 (字节) 
PU         Perm(持久代)目前已使用空间 (字节) 
OC        Old代的容量 (字节) 
OU               Old代目前已使用空间 (字节) 
YGC     从应用程序启动到采样时年轻代中gc次数
FGC      从应用程序启动到采样时old代(全gc)gc次数
FGCT    从应用程序启动到采样时old代(全gc)gc所用时间(s) 
GCT      从应用程序启动到采样时gc用的总时间(s)

stat -gcoldcapacity <pid>:old代对象的信息及其占用量。

OGCMN      old代中初始化(最小)的大小 (字节) 
OGCMX      old代的最大容量(字节) 
OGC            old代当前新生成的容量 (字节) 
OC               Old代的容量 (字节) 
YGC            从应用程序启动到采样时年轻代中gc次数
FGC             从应用程序启动到采样时old代(全gc)gc次数
FGCT           从应用程序启动到采样时old代(全gc)gc所用时间(s) 
GCT             从应用程序启动到采样时gc用的总时间(s)

jstat –gcpermcapacity <pid>:perm对象的信息及其占用量。PGCMN perm代中初始化(最小)的大小 (字节)

PGCMX      perm代的最大容量 (字节)
PGC             perm代当前新生成的容量 (字节) 
PC                Perm(持久代)的容量 (字节) 
YGC            从应用程序启动到采样时年轻代中gc次数
FGC             从应用程序启动到采样时old代(全gc)gc次数
FGCT           从应用程序启动到采样时old代(全gc)gc所用时间(s) 
GCT             从应用程序启动到采样时gc用的总时间(s)

jstat -printcompilation <pid>:当前VM执行的信息。

Compiled      编译任务的数目
Size              方法生成的字节码的大小
Type             编译类型
Method         类名和方法名用来标识编译的方法。类名使用/做为一个命名空间分隔符。方法名是给定类中的方法。上述格式是由-XX:+PrintComplation选项进行设置的

jmap:生成堆转储快照

jmap是JDK自带的工具软件,主要用于打印指定Java过程(或核心文件,远程调试服务器)的共享对象内存映射或堆内存细节。可以使用jmap生成堆转储

在这里插入图片描述

  • 参数解释
    option参数详解:
<no option>  如果使用不带选项参数的jmap打印共享对象映射,将打印目标虚拟机中加载的每个共享对象的起始地址,映射大小以及共享对象文件的路径全称。
-dump:[live,]format=b,file=<filename>  以hprof二进制格式转储Java堆到指定filename的文件中。
   live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。想要浏览堆转储,你可以使用jhat (Java的堆分析工具)读取生成的文件。
-finalizerinfo 打印等待终结的对象信息。
-heap           打印一个堆的摘要信息,包括使用的GC算法,堆配置信息和代明智堆的使用情况。
-histo[:live]   打印堆的柱状图。其中包括每一个Java类,对象数量,内存大小(单位:字节),完全限定的类名。打印的虚拟机内部的类名称将带有一个'*'替换。如果指定了live子选项,则只计算活动的对象。
-permstat     打印的Java堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言,它的名称 活跃度,地址,父类加载器,它所加载的类的数量和大小都会被打印。此外包含的字符串数量和大小也会被打印。
-F               强制模式。如果指定的PID没有响应,请使用JMAP -dump或jmap -histo选项。此模式下,不支持live子选项。
-h                打印帮助信息。
-help            打印帮助信息。
-J<flag>       指定传递给运行jmap的JVM的参数。
  • 常用指令
    jmap-heap 31846 查看java堆(heap)使用情况
Attaching
to process ID 31846, please wait...
Debugger
attached successfully.
Server
compiler detected.
JVM
version is 24.71-b01
 
using
thread-local object allocation.
Parallel
GC with 4 thread(s)//GC 方式
 
Heap
Configuration: //堆内存初始化配置
MinHeapFreeRatio = 0 //对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
MaxHeapFreeRatio = 100 //对应jvm启动参数
-XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
MaxHeapSize  = 2082471936 (1986.0MB) //对应启动参数-XX:MaxHeapSize=设置JVM堆的最大大小
NewSize      = 1310720 (1.25MB)//对应jvm启动参数-XX:NewSize=设置JVM堆的‘新生代’的默认大小
MaxNewSize = 17592186044415 MB//对应jvm启动参数-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
OldSize  = 5439488 (5.1875MB)//对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的‘老生代’的大小
NewRatio  = 2 //对应jvm启动参数-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
SurvivorRatio = 8 //对应jvm启动参数-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
PermSize = 21757952 (20.75MB)  //对应jvm启动参数-XX:PermSize=<value>:设置JVM堆的‘永生代’的初始大小
MaxPermSize      = 85983232 (82.0MB)//对应jvm启动参数-XX:MaxPermSize=<value>:设置JVM堆的‘永生代’的最大大小
G1HeapRegionSize = 0 (0.0MB)
 
Heap
Usage://堆内存使用情况
PS
Young Generation
Eden
Space://Eden区内存分布
   capacity = 33030144 (31.5MB)//Eden区总容量
   used    
= 1524040 (1.4534378051757812MB) 
//Eden区已使用
   free    
= 31506104 (30.04656219482422MB) 
//Eden区剩余容量
   4.614088270399305% used //Eden区使用比率
From
Space:  //其中一个Survivor区的内存分布
   capacity = 5242880 (5.0MB)
   used    
= 0 (0.0MB)
   free    
= 5242880 (5.0MB)
   0.0% used
To
Space:  //另一个Survivor区的内存分布
   capacity = 5242880 (5.0MB)
   used    
= 0 (0.0MB)
   free    
= 5242880 (5.0MB)
   0.0% used
PS
Old Generation //当前的Old区内存分布
   capacity = 86507520 (82.5MB)
   used    
= 0 (0.0MB)
   free    
= 86507520 (82.5MB)
   0.0% used
PS
Perm Generation//当前的
“永生代” 内存分布
   capacity = 22020096 (21.0MB)
   used    
= 2496528 (2.3808746337890625MB)
   free    
= 19523568 (18.619125366210938MB)
   11.337498256138392% used
 
670
interned Strings occupying 43720 bytes.

jmap-histo 3331 查看堆内存(直方图)中的对象数量及大小
num
#instances #bytes
class name

编号     个数                字节     类名
----------------------------------------------
   1:             7        1322080 
[I
   2:         
5603         722368  <methodKlass>
   3:         
5603         641944  <constMethodKlass>
   4:        
34022         544352  java.lang.Integer
   5:          
371         437208  <constantPoolKlass>
   6:          
336         270624  <constantPoolCacheKlass>
   7:          
371         253816  <instanceKlassKlass>
 
jmap -histo:live这个命令执行,JVM会先触发gc,然后再统计信息。

jmap-dump:format=b,file=heapDump 6900 将要使用的内存的详细情况输出到文件

然后用jhat命令可以参见jhat -port 5000 heapDump在浏览器中访问:http://localhost:5000/查看详细信息

这个命令执行,JVM重新整堆堆的信息转储写入一个文件,堆如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证转储的信息是可靠的,所以会暂停应用。

* 总结
1.如果程序内存不足或经常GC,很有可能存在内存不足情况,这时候就要重新使用Java堆Dump查看对象的情况
2.要制作堆Dump可以直接使用jvm自带的jmap命令
3.可以先使用jmap
-heap命令查看堆的使用情况,看一下各个堆空间的占用情况。
4.使用jmap
-histo:[live]查看堆内存中的对象的情况。如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉。
5.可以使用jmap
-dump:format=b,file=<fileName>命令将堆信息保存到一个文件中,再借助与jhat命令查看详细内容
6.在内存出现泄露,溢出或者其它前提条件下,建议多转储先前的内存,把内存文件进行编号扩展,并进行后续的内存整理分析

jhat:Java堆分析工具

 jhat(Java堆分析工具),是一个用来分析java的堆情况的命令。使用jmap可以生成Java堆的Dump文件。生成转储文件之后就可以用jhat命令,将转储文件转成html的形式,然后通过http访问可以查看堆情况。
jhat命令解析会Java堆dump并启动一个web服务器,然后就可以在浏览器中查看堆的dump文件了。

查看帮助: jhat -help
在这里插入图片描述

  • 使用步骤
    1,查看该进程的ID
jps

使用jps命令查看JAVA进程ID
2,生成转储文件

jmap -dump:format=b,file=heapDump 62247

3,解析Java堆转储文件,并启动一个Web服务器

jhat heapDump

使用jhat命令,就启动了一个http服务,端口是7000
然后在访问 http://localhost:7000/
• 常用指令

jmap -dump:format=b,file=heapDump 3331 + jhat
heapDump:解析Java堆转储文件,并启动一个 web server

jstack:堆栈跟踪工具

jstack是java虚拟机自带的一种堆栈跟踪工具
jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

拓展知识
线程状态

  • 想要通过jstack命令来分析线程的情况的话,首先要知道线程都有哪些状态,下面这些状态是我们使用jstack命令查看线程堆栈信息时可能会看到的线程的几种状态:
NEW,未启动的。不会出现在Dump中。
RUNNABLE,在虚拟机内执行的。
BLOCKED,受阻塞并等待监视器锁。
WATING,无限期等待另一个线程执行特定操作。
TIMED_WATING,有时限的等待另一个线程的特定操作。
TERMINATED,已退出的。

调用修饰

表示线程在方法调用时,额外的重要的操作。线程Dump分析的重要信息。修饰上方的方法调用。
locked <地址> 目标:使用synchronized申请对象锁成功,监视器的拥有者。
waiting to lock <地址> 目标:使用synchronized申请对象锁未成功,在迚入区等待。
waiting on <地址> 目标:使用synchronized申请对象锁成功后,释放锁幵在等待区等待。
parking to wait for <地址> 目标
  • 常用指令
jstack 3331:查看线程情况
jstack -F 3331:正常输出不被响应时,使用该指令
jstack -l 3331:除堆栈外,显示关于锁的附件信息

五、JVM垃圾回收

(一)GC原理(垃圾回收)

Java GC 新生代,老年代: https://www.cnblogs.com/yydcdut/p/3959711.html(这链接我在网上看到的,写的很不错的博客,链接发给大家)

  • 回收内存中不再被使用的对象,GC用于回收的方法被称为:收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征分析后,按照新生代、旧生代的方式对对象进行收集,尽可能地缩短GC对应用造成的停顿

1、对新生代的对象收集称为:minor GC
2、对旧生代的对象回收称为:Full GC
3、程序中主动调用System.gc()强制执行的GC为Full GC

  • JVM对象引用分别采用四种类型:

1、强引用:默认情况下,对象采用的均为强引用(这个对象实例没有其它对象引用,GC才回收)
2、软引用:软引用是Java提供的一种比较是用于缓存场景的应用(只有内存不够时才会被GC) 3、弱引用:在GC一定会被GC回收
4、虚引用:虚引用只是用来得知对象是否被GC

(二)对象被标记为垃圾的方法
  • 我们前面知道,JVM内存结构主要包括五大区域:方法区,堆,栈,本地方法栈,程序计数器五块。
  • 程序计数器,栈,本地方法栈,堆和线程的生命周期是相对应的;随着线程的开始而开产生,当线程消亡,这几部分区域也会被回收掉,和我们对象是相关的。
  • Java堆区和方法区则不一样,因为这部分是线程共享的。这部分分配和回收时动态的,正是垃圾回收的关注部分
(1)引用计数(早期用这个方法)

在这里插入图片描述

  • 引用计数: 对堆中每个对象、实例都有一个引用计数,任何对象引用指向这个变量时,这个对象的计数器就会+1,反之-1;当count计数器减为0时,就会被回收。
  • 弊端: 无法检测循环引用
public class Demo{
  public static void main(String[] args){
    GCobject  o1 = new GCobject();//1
    GCobject  o2 = new GCobject();//1
    
    o1.instance = o2;//2
    o2.instance = o1;//2
    //下面我们想销毁o1和o2所指对象,虽然令其为空,但前面的操作让对象计数器为2,下面让计数器-1,计数器还剩1;那也就是说对象不会被垃圾回收
    o1 = null;//1
    o2 = null;//1
    }
}
class GCobject{
    public Object instance = null;
}

图示:
在这里插入图片描述

(2)可达性分析

在这里插入图片描述

  • 以一系列的GC Roots为起点开始向下搜索,搜索所走过的路径称为引用链,当对象与GC Roots不可达(一个对象与GC Roots没有任何的引用链相连)时,代表此对象是不可用的,应该被回收。

那也就是说:上面的循环引用,虽然你相互指向,但你如果没从GC Roor引过来,GC引来会回收对象

  • 在Java中,可以作为GC Roots对象包括以下几种:

1、虚拟机栈中引用的对象(栈帧的本地变量表) 2、方法区中静态属性引用对象 3、方法区中常量引用对象
4、本地方法栈中JNI(Native方法)引用的对象

(三)垃圾回收算法
(1)标记-清除算法

在这里插入图片描述

  • 这种算法分两分:标记、清除两个阶段, 标记阶段是从根集合(GC Root)开始扫描,每到达一个对象就会标记该对象为存活状态,清除阶段在扫描完成之后将没有标记的对象给清除掉
  • 缺点:
    1、这个算法有个缺陷就是会产生内存碎片,如上图B被清除掉后会留下一块内存区域,如果后面需要分配大的对象就会导致没有连续的内存可供使用。
    2、需要对堆空间进行操作,会引起程序卡顿(阻止程序继续运行阶段),效率很低
(2)标记整理算法

在这里插入图片描述

  • 标记整理就没有内存碎片的问题了,也是从根集合(GC Root)开始扫描进行标记然后清除无用的对象,清除完成后它会整理内存。
  • 缺点: 这样内存就是连续的了,但是产生的另外一个问题是:每次都得移动对象,因此成本很高。
(3)复制算法

在这里插入图片描述

  • 复制算法会将JVM推分成二等分,如果堆设置的是1g,那使用复制算法的时候堆就会有被划分为两块区域各512m。给对象分配内存的时候总是使用其中的一块来分配,分配满了以后,GC就会进行标记,然后将存活的对象移动到另外一块空白的区域,然后清除掉所有没有存活的对象,这样重复的处理,始终就会有一块空白的区域没有被合理的利用到。
  • 缺点 两块区域交替使用,最大问题就是会导致空间的浪费,现在堆内存的使用率只有50%。
(4)分代回收算法

在这里插入图片描述
新生代回收

  • JVM的堆分为新生代和老年代,两种类型有不同的特性,根据它们的特性来选择不同的回收算法,这种算法会将新生代划分为一块Eden和二个Survivor区。
  • 如上面的图有三块区域它们会按照8:1:1的比例进行分配,如1000m的堆Eden是800m,二个Survivor各占100m,那它们是如何运行的呢?
  • 新生代的GC:新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和其中一个Survivor,复制到另一个之间Survivor空间中,然后清理掉原来就是在Eden和其中一个Survivor中的对象。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代。

1、始终会有一块Survivor是空着的,内存使用率是90% 2、程序运行会在Eden和其中一块Survivor 1中分配内存
3、等到执行Minor gc,会将存活下来的对象移动到空着的Survivor 2中 4、然后在Eden和Survivor
2中继续分配内存,Survivor 1空着等着下次使用

这样就能使内存使用率达到90%,也不会产生内存碎片。
老年代回收

老年代对象即使进行了垃圾回收,对象的存活率也高,所以采用标记清除或标记整理算法都是不错的选择

  • JVM 会检查老年代连续空间是否大于新生代对象大小或者历次晋升的平均大小,如果条件成立,则进行:Minor GC,否则进行Full GC
(四)垃圾回收器

在这里插入图片描述
我们知道,jvm堆内存分为新生代和老生代,新生代采用复制算法,老生代采用标记-清除或者标记-整理算法来收集和清理垃圾,关于算法的具体实现便是接下来要讲解的垃圾回收器。
一、整堆收集器:G1

  • 新生代收集器只能收集新生代的垃圾,老生代收集器只能收集老生代的垃圾,而整堆收集器G1新老通吃,值得一提的是,G1收集器将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合,所以G1收集器的堆内存结构跟我们之前介绍的结构区别是很大的,它标记了每个Region所属的区域,然后再对其进行垃圾回收。
  • 如上图,连线表示可以搭配使用。你也许会好奇为什么cms不能和perallel scavenge搭配使用,cms为什么可以跟serial old搭配使用,parallel old为什么只能和parallel sacvenge搭配使用。首先回答第一个问题:parallel scavenge没有使用其他Gc通用的Gc框架,导致两者无法搭配,至于为什么没有使用同一个框架,这完全是人为原因,跟技术没有关系(也许未来可以实现两者共用吧)。第二个问题:二者其实并非搭配使用,cms收集器(并发收集器)采用的是标记-清除算法,与用户线程并发运行,所以会产生浮动垃圾(标记完垃圾之后产生的垃圾),当cms运行期间预留内存无法满足程序需要的时候,便会启动后备方案,使用serial old来进行收集(标记-整理),释放内存空间。第三个问题:parallel old跟parallel scavenge一样,为并行收集器,不能跟serial和parnew的原因同问题一。
      在这里有一个概念:并行收集器和并发收集器,可以把用户线程作为参照物,跟用户线程一起运行称之为并发,没有用户线程参与称之为并行。
    二、Serial收集器  
    Serial收集器是最原始的一款垃圾收集器,也称之为串行收集器,顾名思义,它是单线程运行的,而且不止如此,它在收集垃圾的时候,会暂停其他所有的工作线程,直到收集结束,被称之“stop the world”。想象一下,比如你在看电影,每看五分钟需要暂停几秒钟,这显然是令人难以接受的。serial收集器的运行流程如下:
    在这里插入图片描述
    三、ParNew收集器
     ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程收集垃圾之外,其他行为基本和Serial收集器的实现完全一样。ParNew收集器只有在多核CPU的环境下才能发挥出它的优势(多线程收集速度快,停顿时间缩短),如果是单核CPU它甚至不如Serial收集器的效果好(单核CPU的线程切换导致额外开销)。它的运行流程如下:
    在这里插入图片描述
    四、Parallel Scavenge收集器
  • Parallel Scavenge与ParNew类似,也是一款并行多线程收集器,相比于ParNew,它的目标则是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),如果虚拟机总共运行了100分钟,垃圾收集花了1分钟,那么吞吐量变为100-1/100=99%。
  • 停顿时间越短越适合与用户进行交互的程序,良好的响应速度可以提升用户体验,而高吞吐量则是可以高效利用cpu,主要用于在后台运算不需要进行用户交互的任务。
  • Parallel Scavenge提供了两个参数来控制吞吐量:-XX:MaxGCPauseMillis(控制停顿时间(jvm尽量不超过设置的时间),单位ms),-XX:GCTimeRatio(吞吐量大小,大于0小于100),但你千万不要以为把停顿时间的参数设小,吞吐量参数设大就可以让垃圾收集的速度变快,停顿时间的缩短是靠牺牲吞吐量和新生代空间来换取的:系统把新生代调小,比如由1000兆调节为700兆,收集700兆的空间速度必然比1000兆快,但是相应的收集频率会增高,原来10s收集一次,每次停顿100ms,现在需要5s收集一次,每次停顿70ms(相当于10s停顿140ms),停顿时间确实下降了,但是吞吐量也降了下来。
  • 所以,Parallel Scavenge也被称为“吞吐量优先收集器”,此收集器还有一个参数:-XX:+UseAdaptiveSizePolicy,打开这个参数之后,不需要我们再额外设置新生代的大小以及新生代eden和survivor的比例等参数了,jvm会根据当前系统的运行情况动态调整这些参数以提供最合适的停顿时间和吞吐量,这种调节方式称为GC自适应的调节策略,同时这也是Parallel Scavenge和ParNew的重要区别之一。
    五、Serial Old收集器
     Serial Old跟Serial一样是一个单线程收集器,使用“标记-整理”算法。他可以作为CMS收集器的后备军,运行流程同Serial收集器运行流程。
    六、Parallel Old收集器
    Parallel Old收集器跟Parallel Scavenge一样是一个并行多线程收集器,采用“标记-整理”算法。它与Parallel Scavenge搭配适用于增加吞吐量以及CPU资源敏感的场合。工作流程如下:
    在这里插入图片描述
    七、CMS收集器
    CMS收集器全称Concurrent Mark Sweep,主打并发收集,低停顿,适用于B/S系统的服务端,我们熟知的淘宝网站使用的便是CMS收集器,它的收集器线程可以跟用户线程一起工作,这也是与并行收集器所不同的地方,运行流程如下:
    在这里插入图片描述
      由上图可见,尽管CMS可以跟用户线程一起运行,但它同样也无法避免“stop the world”,之前的博客讲过可达性原则,即在初始标记和重新标记验证对象死活的时候也会引起工作线程的停顿,只是停顿的时间较短。CMS收集器是一款非常优秀的垃圾回收器,但它也存在以下缺点:
  • 对CPU资源敏感。事实上,面向并发设计的程序对CPU资源都较为敏感,在并发阶段,他虽然不会使用户线程停顿,但是也会因为占用了一部分CPU资源而使应用程序变慢,总吞吐量就会降低。
  • 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”导致另一次Full Gc(收集老生代成为Full Gc)。由于CMS在并发清理阶段用户线程依然运行着并不断产生垃圾,这部分垃圾出现在重新标记之后,所以在本次Gc中无法清理,这部分垃圾就称为浮动垃圾。CMS在垃圾收集的时候用户线程仍在运行,所以他不能向其他收集器一样等到老生代几乎填满再进行回收,需要预留一部分空间供并发时的程序使用,可以通过:-XX:CMSInitIatingOccupancyFaction的参数值来调节触发收集的百分比,一般不需要特意动它。如果预留空间无法满足程序运行的需要,那么就会出现Concurrent Mode Failure,这个时候就轮到Serial Old收集器登场了,jvm会临时使用Serial Old来重新对老年代进行垃圾收集,这同时也就意味着系统停顿时间变长,所以此参数设置过高容易引起大量Concurrent Mode Failure,反而降低性能!
  • 产生大量内存碎片。CMS利用的是标记-清除算法来进行垃圾收集(比标记-整理快),这必然会不可避免的产生内存碎片,内存碎片过多时,就算剩余空间很足,但是无法找到连续的内存空间去分配新来的大对象,就会不得不提前触发Full GC。我们可以通过开启XX:UseCMSCompactAtFullCollection参数来解决此问题(默认开启),这样CMS在顶不住要进行Full GC时会对内存碎片进行合并整理,但这也会使得停顿时间变长(内存整理无法并发执行)。通过XX:CMSFullGCsBeforeCompaction可以设置执行多少次不合并整理的Full Gc后,执行一次带合并整理的Full Gc,默认为0,即每次进入Full Gc时都会进行碎片整理。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值