Java虚拟机学习,持续更新中~

说明

本教程测试源码可在github获得>_>!,JVM。
项目中包名’pers.xipiker.jvm1’和标题中或者内容部分有(#jvm1)对应,依次类推。
本教程是比较基础的,深入还在学习中,此内容持续更新中~
建议买本书看,先在哔哩哔哩看基础视频,或者总结一下其他优秀的博客进行学习,然后在买一本对应的书在继续学习~

jdk、jre和jvm之间的关系
JDK(Java Development Kit)

JDK是Java开发工具包,是Sun Microsystems针对Java开发员的产品。
JDK中包含JRE,在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
JDK是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment)、一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。
1、SE(J2SE),standard edition,标准版,是我们通常用的一个版本,从JDK 5.0开始,改名为Java SE。
2、EE(J2EE),enterprise edition,企业版,使用这种JDK开发J2EE应用程序,从JDK 5.0开始,改名为Java EE。
3、ME(J2ME),micro edition,主要用于移动设备、嵌入式设备上的java应用程序,从JDK 5.0开始,改名为Java ME。
金字塔结构 JDK=JRE+JVM+其它 运行Java程序一般都要求用户的电脑安装JRE环境(Java Runtime Environment);没有jre,java程序无法运行;而没有java程序,jre就没有用武之地。

Java Runtime Environment(JRE)

是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。
JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。
与大家熟知的JDK不同,JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器),只是针对于使用Java程序的用户。

JVM(java virtual machine)

就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。
也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。
JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Jvm初体验-内存溢出问题的分析与解决(#jvm1)

我们模拟程序内存溢出,通过如下方法,为了方便测试我们将jvm参数调成[-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m],启动内存溢出快照和最大最小内存。

Demo.java

public class Demo {
}

Main.java

public class Main {public static void main(String[] args) {List<Demo> demoList = new ArrayList<>();while(true){demoList.add(new Demo());}}
}

运行后报错,内存溢出,具体信息如下

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid46421.hprof ...
Heap dump file created [27771879 bytes in 0.118 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:3210)at java.util.Arrays.copyOf(Arrays.java:3181)at java.util.ArrayList.grow(ArrayList.java:265)at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)at java.util.ArrayList.add(ArrayList.java:462)at pers.xipiker.jvm1.Main.main(Main.java:10)

随后会生成*.hprof文件,我们用idea打开并进行分析排查,主要是看Shallow size和Retained size大小来逐步进行排查最终出问题的地方。

Jvm可视化监控工具(jconsole)(#jvm2)

通过命令行输入jconsole打开jvm可视化监控工具。
通过命令行输入jps查看当前系统java运行的进程。
可以运行如下程序,在去jconsole进行监控

public class Jconsole {public static void main(String[] args) {try {//这里为了方便测试延迟1000毫秒Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}fill(1000);}private static void fill(int n) {List<Jconsole> jconsoleList = new ArrayList<>();for(int i = 0; i < n; n++){try {//这里为了方便测试延迟1000毫秒Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}jconsoleList.add(new Jconsole());}}
}

运行结果如下图所示
在这里插入图片描述
在这里插入图片描述

Java虚拟机
Sun classic vm

世界上第一款商用的java虚拟机。
只能使用纯解释器的方式执行java代码。

Exact vm

Exact Memory Management准确试内存管理。
编译器和解释器混合工作以及两级即使编译器。
只在Solaris平台发布。
英雄气短。

HotSpot vm
KVM

kilobyte简单、轻量、高度可移植。
在手机平台运行。

BEA JRockit

世界上最快的java虚拟机。
专注服务端应用。
优势:

  • 垃圾收集器
  • MissionControl服务套件
    BEA JRockit Mission Control用来诊断泄露并指出根本原因。该工具开销非常小,因此使用它来寻找生产环境中的系统的内存泄露。
    BEA JRockit Mission Control(简称BJMC)于2005年12月面世,并从JRockit R26.0.0版本开始捆绑了这个工具套件,目前最新的版本是2.0.X。它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。它包括三个独立的应用程序:内存泄露监测器(Memory Leak Detector)、Jvm运行时分析器(Runtime Analyzer)和管理控制台(Management Console)。
IBM J9(IBM Technology for java virtual machine (IT4j))
Dalvik(Dex dalvik Executalbe)
Microsoft JVM
Azul VM Liquid VM(高性能的java虚拟机)
TaobaoVM
Java虚拟机内存管理

运行时数据区主要分为两个区域,线程共享区和线程独占区。

线程共享区:

  • 方法区(存储运行时常量池,已被虚拟机加载的类信息(类的版本信息、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码、运行时常量池等数据)。
    1、存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据。
    2、方法区和永久区,基本上是一致的的只是通过永久区合理的实现了方法区。
    3、垃圾回收在方法区的行为。
    4、异常的定义,OutOfMemoryError。
    5、运行时常量池属于方法区的一部分。(#jvm4)
    运行时常量池简单说明,可以参考如下java代码:
public class Test {public static void main(String[] args) {String s1 = "abc";String s2 = "abc";//这里为什么s1和s2进行比对会返回true,因为s1和s2的内容会存入运行时常量池,s1进入运行时常量池后,s2进入时通过类似于StringTable:HashSet进行比对。System.out.println(s1 == s2);String s3 = new String("abc");//这里为什么s1和s3进行比对会返回false,因为每个对象声明都会存到Java堆|堆内存,s1进入的是运行时常量池,所以s1!=s3。System.out.println(s1 == s3);//这里为什么s1和s3进行比对会返回true,intern方法native本地方法实现,类似于把s3牵引到运行时常量池,所以s1==s3。System.out.println(s1 == s3.intern());}
}
  • Java堆|堆内存(存储对象实例)。
    1、存放对象实例。
    2、垃圾收集器管理的主要区域(1和3主要的流程是为2座服务的)。
    3、新生代、老年代、Eden空间。

线程独占区:

  • 虚拟机栈(存放方法运行时所需的数据,成为栈帧)。
    1、虚拟机栈描述的是Java方法执行的动态内存模型。
    2、栈帧(每个方法执行,都会创建一个栈帧,伴随着方法从创建到执行完成。用与存储局部变量表、操作数栈、动态链接、方法出口等)。
    3、局部变量表(存放编译期可知的各种基本数据类型,引用类型,returnAddress类型。局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小)。
    4、大小(StackOverflowError、OutOfMemory)。
    以下为模拟栈溢出的异常操作,解决方法时有进有出(#jvm3)
public class StackError {public void test(){System.out.printf("start...");test();}public static void main(String[] args) {new StackError().test();}
}

控制台报错信息

*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
Exception in thread "main" java.lang.StackOverflowErrorat java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)at java.io.PrintStream.write(PrintStream.java:526)at java.io.PrintStream.print(PrintStream.java:669)at java.io.PrintStream.append(PrintStream.java:1065)at java.io.PrintStream.append(PrintStream.java:57)at java.util.Formatter$FixedString.print(Formatter.java:2595)at java.util.Formatter.format(Formatter.java:2508)at java.io.PrintStream.format(PrintStream.java:970)at java.io.PrintStream.printf(PrintStream.java:871)at pers.xipiker.jvm3.StackError.test(StackError.java:5)at pers.xipiker.jvm3.StackError.test(StackError.java:6)at pers.xipiker.jvm3.StackError.test(StackError.java:6)...
  • 本地方法栈(为JVM所调用到的native即本地方法服务)。
    1、虚拟机栈为虚拟机执行java方法服务。
    2、本地方法栈为虚拟机执行native方法服务。
  • 程序计数器(记录当前线程所执行到的字节码的行号)。
    1、程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
    2、程序计数器属于线程独占区。
    3、如果线程执行的是java方法,这个计数器记录的正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个计数器的值为undefined。
    4、此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Jvm直接内存(堆外内存)
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制。
配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常。
直接内存(堆外内存)与堆内存比较:
1、直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
2、直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。

对象
对象的创建

对象的创建:
1、给对象分配内存(指针碰撞、空闲列表)。
2、线程安全性问题(线程同步、本地线程分配缓存)。
3、初始化对象。
4、执行构造方法。
jvm创建对象执行流程(new类名->根据new的参数在常量池中定位一个类的符号引用->如果没有找到这个符号引用,说明类没有被加载,则进行类的加载、解析和初始化->虚拟机为对象分配内存(位于堆中)->将分配的内存初始化为零(不包括对象头)->调用对象的方法)。
1和2两个步骤程序运行看不出来,3和4可以通过程序代码进行演示(#jvm5)
User.java

public class User {private String name;private Integer age;private Date birth;private Boolean flag;//会先执行构造方法public User(){System.out.println("start ...");}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}public Date getBirth() {return birth;}public void setBirth(Date birth) {this.birth = birth;}public Boolean getFlag() {return flag;}public void setFlag(Boolean flag) {this.flag = flag;}//然后执行toString()@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +", birth=" + birth +", flag=" + flag +'}';}
}

Main.java

public class Main {public static void main(String[] args) {User user = new User();System.out.println(user.toString());}
}
对象的结构

对象的结构:
1、Header(对象头)

  • 自身运行时数据(Mark Word,包括哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程的ID、偏向时间戳)。
    2、InstanceData
    3、Padding
对象的访问定位

对象访问定位:
1、使用句柄
2、直接指针

垃圾回收

垃圾回收:
如何判定对象是垃圾对象(引用计数法、可达性分析法)。
1、引用计数算法,在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失败的时候,计数器的值就会-1。缺点因为如果两个对象互相引用,那么计数器的值就为2了,回收的时候-1,垃圾数据依然存在。可以看如下演示(#jvm6)

//在运行前需要在Jvm配置中加入-verbose:gc -XX:+PrintGCDetails,来显示垃圾回收日志
public class Main {private Object instance;public static void main(String[] args) {Main m1 = new Main();Main m2 = new Main();//对象引用m1.instance = m2;m2.instance = m1;m1 = null;m2 = null;//垃圾回收System.gc();}
}

控制台日志信息

[GC (System.gc()) [PSYoungGen: 2621K->528K(76288K)] 2621K->536K(251392K), 0.0010285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 528K->0K(76288K)] [ParOldGen: 8K->366K(175104K)] 536K->366K(251392K), [Metaspace: 2920K->2920K(1056768K)], 0.0041708 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
HeapPSYoungGen      total 76288K, used 1966K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)eden space 65536K, 3% used [0x000000076ab00000,0x000000076aceb9e0,0x000000076eb00000)from space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)to   space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)ParOldGen       total 175104K, used 366K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)object space 175104K, 0% used [0x00000006c0000000,0x00000006c005ba40,0x00000006cab00000)Metaspace       used 2926K, capacity 4496K, committed 4864K, reserved 1056768Kclass space    used 319K, capacity 388K, committed 512K, reserved 1048576K

2、可达性分析算法(虚拟机栈、方法区的类属性所引用的对象、方法区中常量所引用的对象、本地方法栈中引用的对象)。
如何回收(回收策略:标记-清除法、复制算法、标记-整理算法、分代收集算法;垃圾回收器:Serial、Parnew、Cms、G1)。
何时回收

垃圾回收器
Serial收集器
Parnew收集器
Parallel Scavenge收集器

复制算法(新生代收集器)。
多线程收集器。
达到可控制的吞吐量。
-XX:MaxGCPauseMillis,垃圾收集器最大停顿时间
-XX:GCTimeRa,吞吐量(CPU用于运行用户代码的时间与CPU消耗的总时间的比值,公式:吞吐量=(执行用户代码时间)/(执行用户代码的时间+垃圾回收所占用的时间))大小。

CMS收集器(Concurrent Mark Sweep)

工作过程(初始标记、并发标记、重新标记、并发清理)。
优点(并发收集、低停顿)。
缺点(占用大量的CPU资源、无法处理浮动垃圾、出现Concurrent Mode Failure、空间碎片)。

G1收集器

优势(并行与并发、分代收集、空间整合、可预测的停顿)。
步骤(初始标记、并发标记、最终标记、筛选回收)。
与cms比较。

内存分配
内存分配策略

优先分配到Eden,大对象直接分配到老年代、长期存活的对象分配到老年代、空间分配担保、动态对象年龄判断。

大对象直接进入老年代

-XX:PretenureSizeThreshold

长期存活的对象分配到老年代

-XX:MaxTenuringThreshold

空间分配担保

-XX:+HandlePromotionFailure

逃逸分析与栈上分配

逃逸分析:分析对象的作用域

虚拟机工具
jps(Java process status)

jps -m(运行时传入主类的参数)l(类的全名或者是jar包的名称)v(虚拟机参数)

jstat

类装置、内存、垃圾收集、jit编译信息,收集。
jstat -gcutil [PID]。
元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

jinfo

实时查看和调整虚拟机各项参数

jmap

jmap -dump:format=b,file=[生成快照的路径] [PID]//打印快照

jhat(Jvm heap analysis tool)

快照分析命令[jhat [快照的路径]]

jstack

用来生成线程快照,分析线程长时间停顿的原因。
jstack -l [PID],打印线程
可以通过java程序进行打印线程信息,获取方法Thread.getAllStackTraces()(#jvm7)

public class Main {public static void main(String[] args) {//打印线程信息Map<Thread, StackTraceElement[]> m = Thread.getAllStackTraces();for(Map.Entry<Thread, StackTraceElement[]> en : m.entrySet()){Thread t = en.getKey();StackTraceElement[] v = en.getValue();System.out.println("Thread name is:"+t.getName());for(StackTraceElement s : v){System.err.println("\t"+s.toString());}}}
}
jconsole内存监控

前面有简单使用,不做演示。

jconsole线程监控

参考如下java程序(#jvm8)

public class Main {public static void main(String[] args) {//线程第一次断掉Scanner sc = new Scanner(System.in);sc.next();new Thread(new Runnable() {@Overridepublic void run() {while (true){//线程不停的运行}}},"while true").start();//输入第二次前,断了sc.next();//输入后,线程进入wait,一直等待中testWait(new Object());}private static void testWait(Object object){new Thread(new Runnable() {@Overridepublic void run() {synchronized (object){try {object.wait();} catch (InterruptedException e) {e.printStackTrace();}}}},"wait").start();}
}

采用jconsole进行监控
程序运行时有一个线程Main如下图

在这里插入图片描述

在程序命令窗口进行第一次输入出现while true线程,线程状态一直为run,如下图

在这里插入图片描述

在程序命令窗口进行第二次输入出现wait线程,线程状态一直为等待,如下图

在这里插入图片描述

jconsole监控线程死锁

死锁原理:比如有两个线程A和B,A、B同时加上线程锁,此时A线程调用B线程,因为B线程加了锁,所以A线程进行等待,然后B线程需要调用A线程,因为A线程在等待,B线程也需要等待,A与B线程都在等待,所以会出现死锁这个情况,死锁出现的情况比较特殊,如果A、B互相调用比较快时则不会出现,所以如下演示程序加了线程执行时间,方便测试死锁出现的情况(#jvm9)

DeadLock.java

public class DeadLock implements Runnable {private Object object1;private Object object2;public DeadLock(Object object1, Object object2) {this.object1 = object1;this.object2 = object2;}@Overridepublic void run() {synchronized (object1){//实验线程间互相调用过快问题,仅测试使用try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object2){System.out.println("hello world!");}}}
}

Main.java

public class Main {public static void main(String[] args) {Object object1 = new Object();Object object2 = new Object();new Thread(new DeadLock(object1, object2)).start();new Thread(new DeadLock(object2, object1)).start();}
}

开启jconsole查看线程情况

在这里插入图片描述

VisualVM

工具很强大~

性能调优
性能调优-案例1

问题:经常有用户反映长时间出现卡顿。
处理思路:优化sql(如果是功能方面的卡顿可以使用sql优化)、监控CPU(好像没有一点关系)、监控内存(Full GC,堆内存比较大,垃圾回收处理时间比较长)。
解决方案:部署多个web容器,每个web容器的堆内存指定4G。

参考资料

jdk、jre和jvm三者之间的关系
深入理解Java虚拟机(JVM性能调优+内存模型+虚拟机原理)
JVM直接内存


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部