jvm原理解析——不疯魔不成活

简介: 摘要: 作为一名java开发人员,如果有人问你java是什么?java是如何运行的?你该如何回答,事实上java是有Java语言、class文件、jvm、Java API共同组成。

摘要: 作为一名java开发人员,如果有人问你java是什么?java是如何运行的?你该如何回答,事实上java是有Java语言、class文件、jvm、Java API共同组成。

java程序运行

*.java文件-->编译器-->*.class文件-->线程启动(main)-->jvm-->操作系统-->硬件

通过上面的流程我们可以看出java程序的执行顺序,那么jvm到底是什么,class文件到底是如何在jvm中运行就显得很重要了。

jvm原理

什么是jvm

openjdk源码地址http://hg.openjdk.java.net/jdk9

JVM是一个计算机模型,JVM对Java可执行代码,即字节码(Bytecode)的格式给出了明确的规格。这一规格包括操作码和操作数的语法(也就是cpu指令集)和数值、标识符的数值表示方式、以及Java类文件中的Java对象、常量缓冲池在JVM的存储映象。

JVM的组成

JVM指令系统、JVM寄存器、JVM 栈结构、JVM 碎片回收堆、JVM 存储区

JVM指令

Java指令也是由操作码和操作数两部分组成,与RISC CPU采用的编码方式是一致的,也就是精简指令集,目前UNIX、Linux、MacOS系统使用RISC,我们目前知道的x86架构CPU使用的CISC编码,也就是复杂指令集。

JVM寄存器

1.pc程序计数器

2.optop操作数栈顶指针

3.frame当前执行环境指针

4.vars指向当前执行环境中第一个局部变量的指针

jvm的装载

windows操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境。

1.创建JVM装载环境和配置

2.装载JVM.dll(C:Program FilesJavajre1.8.0_151inserver linux在jre/lib/server下)

3.初始化JVM.dll并挂接到JNIENV(JNI调用接口)实例

4.调用JNIEnv实例装载并处理class类。

JVM虚拟机相当于x86计算机系统,Java解释器相当于x86CPU

JVM运行数据

JVM定义了若干个程序执行期间使用的数据区域。这个区域里的一些数据在JVM启动的时候创建,在JVM退出的时候销毁。而其他的数据依赖于每一个线程,在线程创建时创建,在线程退出时销毁。分别有程序计数器,堆,栈,方法区,运行时常量池

程序计数器:每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。

常量缓冲池和方法区:常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储Java方法的字节码。对于这两种存储区域具体实现方式在JVM规格中没有明确规定。这使得Java应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。

栈:Java栈是JVM存储信息的主要方法。当JVM得到一个Java字节码应用程序后,便为该代码中一个类的每一个方法创建一个栈框架,以保存该方法的状态信息。每个栈框架包括以下三类信息:

局部变量:对应vars寄存器指向该变量表中的第一个局部变量,用于存储一个类的方法中所用到的局部变量。

执行环境:对应frame寄存器的当前执行环境指针,用于保存解释器对Java字节码进行解释过程中所需的信息。它们是:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。执行环境是一个执行一个方法的控制中心。例如:如果解释器要执行iadd(整数加法),首先要从frame寄存器中找到当前执行环境,而后便从执行环境中找到操作数栈,从栈顶弹出两个整数进行加法运算,最后将结果压入栈顶。

操作数栈:对应optop寄存器的操作数栈顶指针,操作数栈用于存储运算所需操作数及运算的结果。

堆:JVM中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 主要的回收区,一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。

垃圾回收机制(GC)只发生在线程共享区,也就是堆和方法区,栈不需要回收,线程销毁则栈也销毁, 也就是上图的heap space与method area会发生gc。

通过上图可以发现heap space被分为两部分:

Young Generation:又分为Eden space所有的类都是在Eden space被new出来的。From区(Survivor 0 space)和To区(Survivor 1 space)。当Eden space空间用完时,程序又需要创建对象,JVM的垃圾回收器将对Eden space进行垃圾回收(Minor GC),将Eden space中的剩余对象移动到From区。若From区也满了,再对该区进行垃圾回收,然后移动到To区。那如果To区也满了呢,再移动到Old区。

Old Generation:若该区也满了,那么这个时候将产生Major GC(FullGCC),进行Tenured区的内存清理。若该区执行Full GC 之后发现依然无法进行对象的保存,产生异常java.lang.OutOfMemoryError: Java heap space。

Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

Permanent Generation:是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。产生异常java.lang.OutOfMemoryError: PermGen space jdk1.8之后已经不会再报报这个错误了。因为类信息的卸载几乎很少发生,这样会影响GC的效率。于是PermGen便被拆分出去了。

程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。

大量动态反射生成的类不断被加载,最终导致Perm区被占满。

jvm的算法

由于算法篇幅太长具体算法可自行查阅资料,主要介绍gc算法发生在什么区。

分代搜集算法:是由复制算法、标记/整理、标记/清除算法共同组成

复制算法发生在Young Generation

标记/整理和标记/清除算法发生在Old Generation和Permanent Generation

java验证jvm

栈中一般存放的都是对象的指针和基本类型,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

栈数据可以共享

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { int a=0; int b=0; System.out.print(a==b); }}

"C:Program FilesJavajdk1.8.0_151injava"trueProcess finished with exit code 0

编译器先处理int a = 0;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为0的地址,没找到,就开辟一个存放0这个字面值的地址,然后将a指向0的地址。接着处理int b = 0;在创建完b的引用变量后,由于在栈中已经有0这个字面值,便将b直接指向0的地址。这样,就出现了a与b同时均指向0的情况

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { int a=0; int b=0; a=1; System.out.print("a="+a); System.out.print("b="+b); System.out.print(a==b); }}

"C:Program FilesJavajdk1.8.0_151injava" a=1b=0falseProcess finished with exit code 0

再令a=1;那么,b不会等于1,还是等于0。在编译器内部,遇到a=1;时,它就会重新搜索栈中是否有1的字面值,如果没有,重新开辟地址存放1的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

String str = "abc"的工作原理

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; System.out.println(str1==str2); }}

"C:Program FilesJavajdk1.8.0_151injava"trueProcess finished with exit code 0

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; System.out.println(str1 + "," + str2); System.out.println(str1==str2); }}

"C:Program FilesJavajdk1.8.0_151injava" bcd,abcfalseProcess finished with exit code 0

赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; String str3 = str1; System.out.println(str3); String str4 = "bcd"; System.out.println(str1 == str4); }}

"C:Program FilesJavajdk1.8.0_151injava"bcdtrueProcess finished with exit code 0

str3这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用 str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。

堆验证

String类

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = new String("abc"); String str2 = "abc";System.out.println(str1==str2);}}

"C:Program FilesJavajdk1.8.0_151injava"falseProcess finished with exit code 0

以上代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

使用String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是 享元模式的思想。

由于String类的性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。

执行时间上寄存器 < 堆栈 < 堆

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String s1 = "ja"; String s2 = "va"; String s3 = "java"; String s4 = s1 + s2; System.out.println(s3 == s4); System.out.println(s3.equals(s4)); }}

"C:Program FilesJavajdk1.8.0_151injava"falsetrueProcess finished with exit code 0

是不是很矛盾啊!是不是又懵逼了?

打印false的原因是,java 重载了“+”,查看java字节码可以发现“+”其实是调用了StringBuilder 所以使用了“+”其实是生成了一个新的对象。所以(s3 == s4)打印false

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args){ long maxMemory = Runtime.getRuntime().maxMemory();//返回Java虚拟机试图使用的最大内存量。 Long totalMemory = Runtime. getRuntime().totalMemory();//返回jvm实例占用的内存。 System.out.println("MAX_MEMORY ="+maxMemory +"(字节)、"+(maxMemory/(double)1024/1024) + "MB"); System.out.println("TOTAL_ MEMORY = "+totalMemory +"(字节)"+(totalMemory/(double)1024/1024) + "MB"); }}

"C:Program FilesJavajdk1.8.0_151injava" -XX:+PrintGCDetailsMAX_MEMORY =1868038144(字节)、1781.5MBTOTAL_ MEMORY = 126877696(字节)121.0MBHeapPSYoungGen total 37888K, used 3932K [0x00000000d6400000, 0x00000000d8e00000, 0x0000000100000000) eden space 32768K, 12% used [0x00000000d6400000,0x00000000d67d7320,0x00000000d8400000) from space 5120K, 0% used [0x00000000d8900000,0x00000000d8900000,0x00000000d8e00000) to space 5120K, 0% used [0x00000000d8400000,0x00000000d8400000,0x00000000d8900000)ParOldGen total 86016K, used 0K [0x0000000082c00000, 0x0000000088000000, 0x00000000d6400000) object space 86016K, 0% used [0x0000000082c00000,0x0000000082c00000,0x0000000088000000)Metaspace used 3325K, capacity 4494K, committed 4864K, reserved 1056768K class space used 363K, capacity 386K, committed 512K, reserved 1048576KProcess finished with exit code 0

将jvm堆初始值改小,触发gc回收

import java.util.Random;/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args){ long maxMemory = Runtime.getRuntime().maxMemory();//返回jvm试图使用的最大内存量。 Long totalMemory = Runtime. getRuntime().totalMemory();//返回jvm实例的内存大小。 System.out.println("MAX_MEMORY ="+maxMemory +"(字节)、"+(maxMemory/(double)1024/1024) + "MB"); System.out.println("TOTAL_ MEMORY = "+totalMemory +"(字节)"+(totalMemory/(double)1024/1024) + "MB"); String str = "www.baidu.com"; while(true){ str += str + new Random().nextInt(88888888) + new Random().nextInt(99999999); } }}

"C:Program FilesJavajdk1.8.0_151injava" -XX:+PrintGCDetailsMAX_MEMORY =1868038144(字节)、1781.5MBTOTAL_ MEMORY = 126877696(字节)121.0MB[GC (Allocation Failure) [PSYoungGen: 32247K->2729K(37888K)] 32247K->10124K(123904K), 0.0045031 secs] [Times: user=0.01 sys=0.03, real=0.00 secs][GC (Allocation Failure) [PSYoungGen: 32912K->4469K(70656K)] 40307K->26638K(156672K), 0.0121112 secs] [Times: user=0.00 sys=0.00, real=0.01 secs][GC (Allocation Failure) [PSYoungGen: 66160K->776K(70656K)] 88329K->59879K(156672K), 0.0141096 secs] [Times: user=0.03 sys=0.02, real=0.01 secs][Full GC (Ergonomics) [PSYoungGen: 776K->0K(70656K)] [ParOldGen: 59103K->37630K(116224K)] 59879K->37630K(186880K), [Metaspace: 3408K->3408K(1056768K)], 0.0143902 secs] [Times: user=0.03 sys=0.00, real=0.01 secs][Full GC (Ergonomics) [PSYoungGen: 60370K->0K(70656K)] [ParOldGen: 96726K->74565K(172032K)] 157096K->74565K(242688K), [Metaspace: 3409K->3409K(1056768K)], 0.0598124 secs] [Times: user=0.08 sys=0.00, real=0.06 secs][GC (Allocation Failure) [PSYoungGen: 60382K->32K(95744K)] 1257771K->1226968K(1463808K), 0.0227293 secs] [Times: user=0.06 sys=0.01, real=0.02 secs][GC (Allocation Failure) [PSYoungGen: 32K->32K(131584K)] 1226968K->1226968K(1499648K), 0.0037586 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][Full GC (Allocation Failure) [PSYoungGen: 32K->0K(131584K)] [ParOldGen: 1226936K->355271K(483840K)] 1226968K->355271K(615424K), [Metaspace: 3409K->3409K(1056768K)], 0.1616835 secs] [Times: user=0.19 sys=0.09, real=0.16 secs][GC (Allocation Failure) [PSYoungGen: 2499K->32K(158208K)] 1303306K->1300838K(1526272K), 0.0037952 secs] [Times: user=0.06 sys=0.00, real=0.00 secs][GC (Allocation Failure) [PSYoungGen: 32K->32K(158208K)] 1300838K->1300838K(1526272K), 0.0036491 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][Full GC (Allocation Failure) [PSYoungGen: 32K->0K(158208K)] [ParOldGen: 1300806K->473463K(622080K)] 1300838K->473463K(780288K), [Metaspace: 3409K->3409K(1056768K)], 0.1641897 secs] [Times: user=0.30 sys=0.06, real=0.16 secs][GC (Allocation Failure) [PSYoungGen: 0K->0K(250880K)] 946230K->946230K(1618944K), 0.0027229 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][GC (Allocation Failure) [PSYoungGen: 0K->0K(258560K)] 946230K->946230K(1626624K), 0.0027747 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][Full GC (Allocation Failure) [PSYoungGen: 0K->0K(258560K)] [ParOldGen: 946230K->709846K(879104K)] 946230K->709846K(1137664K), [Metaspace: 3409K->3409K(1056768K)], 0.1013768 secs] [Times: user=0.28 sys=0.02, real=0.10 secs][GC (Allocation Failure) [PSYoungGen: 0K->0K(353280K)] 709846K->709846K(1721344K), 0.0049384 secs] [Times: user=0.00 sys=0.00, real=0.01 secs][Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space[PSYoungGen: 0K->0K(353280K)] [ParOldGen: 709846K->709816K(900608K)] 709846K->709816K(1253888K), [Metaspace: 3409K->3409K(1056768K)], 0.1792920 secs] [Times: user=0.39 sys=0.00, real=0.18 secs]Heapat java.util.Arrays.copyOf(Arrays.java:3332) PSYoungGen total 353280K, used 14028K [0x00000000d6400000, 0x00000000ec700000, 0x0000000100000000)at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) eden space 352768K, 3% used [0x00000000d6400000,0x00000000d71b3070,0x00000000ebc80000)at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674) from space 512K, 0% used [0x00000000ec680000,0x00000000ec680000,0x00000000ec700000)at java.lang.StringBuilder.append(StringBuilder.java:208) to space 4608K, 0% used [0x00000000ebe00000,0x00000000ebe00000,0x00000000ec280000)at JvmTest.main(JvmTest.java:15) ParOldGen total 1368064K, used 709816K [0x0000000082c00000, 0x00000000d6400000, 0x00000000d6400000)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) object space 1368064K, 51% used [0x0000000082c00000,0x00000000ae12e1e8,0x00000000d6400000) Metaspace used 3440K, capacity 4494K, committed 4864K, reserved 1056768Kat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) class space used 377K, capacity 386K, committed 512K, reserved 1048576Kat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)Process finished with exit code 1

欢迎工作一到五年的Java程序员朋友们加入Java架构开发:744677563

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导


相关文章
|
3天前
|
自然语言处理 前端开发 Java
深入浅出JVM(六)之前端编译过程与语法糖原理
深入浅出JVM(六)之前端编译过程与语法糖原理
|
3天前
|
XML JavaScript 数据格式
Beautiful Soup 库的工作原理基于解析器和 DOM(文档对象模型)树的概念
Beautiful Soup 使用解析器(如 html.parser, lxml, html5lib)解析HTML/XML文档,构建DOM树。它提供方法查询和操作DOM,如find(), find_all()查找元素,get_text(), get()提取信息。还能修改DOM,添加、修改或删除元素,并通过prettify()输出格式化字符串。它是处理网页数据的利器,尤其在处理不规则结构时。
7 2
|
4天前
|
Arthas Prometheus 监控
JVM工作原理与实战(四十四):JVM常见题目
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JVM常见题目等内容。
14 1
|
4天前
|
存储 监控 算法
JVM工作原理与实战(四十三):JVM常见题目
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JVM常见题目等内容。
|
4天前
|
存储 监控 Oracle
JVM工作原理与实战(四十二):JVM常见题目
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JVM常见题目等内容。
14 2
|
4天前
|
存储 监控 算法
JVM工作原理与实战(四十一):ShenandoahGC原理
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了ShenandoahGC、ShenandoahGC 1.0版本、ShenandoahGC 2.0版本、ShenandoahGC执行流程等内容。
|
4天前
|
存储 监控 安全
JVM工作原理与实战(四十):ZGC原理
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了ZGC、ZGC核心技术、ZGC的内存划分、ZGC的执行流程、分代ZGC的设计等内容。
16 1
|
4天前
|
监控 算法 安全
JVM工作原理与实战(三十九):G1垃圾回收器原理
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了G1垃圾回收器执行流程、年轻代回收原理、卡表(Card Table)、记忆集的生成流程、年轻代回收的详细步骤、混合回收的步骤、初始标记、并发标记、SATB、转移等内容。
13 0
|
4天前
|
监控 安全 Java
JVM工作原理与实战(三十八):JIT即时编译器原理
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JIT即时编译器、HotSpot中的JIT编译器、JIT优化技术、JIT优化建议等内容。
10 0
|
4天前
|
监控 Java Linux
JVM工作原理与实战(三十七):Shenandoah GC和ZGC
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了垃圾回收器的技术演进、Shenandoah GC、ZGC等内容。
14 0

推荐镜像

更多