IMUL(JVM深入(一))

精英怪
广告

JVM的位置

IMUL(JVM深入(一))

JVM是java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM的位置如下图所示。 JVM运行在操作系统之上,对于不同的操作系统需要运行不同的JVM。因此:Java程序是跨平台的,但JVM不跨平台。

JVM的体系架构图

JVM的体系架构如下图所示。重点关注堆、栈和方法区。 所谓的JVM调优就是在调堆,栈、本地方法栈和程序计时器一定不会产生垃圾。

类加载器ClassLoader

首先来看看一个类加载到JVM的基本结构,如下图所示。

类的加载、连接和初始化(了解)

加载:查找并记载类的二进制数据。

连接分为三个阶段:

验证:保证被加载的类的正确性。准备:给类静态变量分配内存空间,赋值一个默认的初始值。解析:把类中的符号引用转换为直接引用。

注:在把java编译为class文件时,虚拟机并不知道所引用的地址,而是使用助记符来进行标记,这就是符号引用,类加载器需要把助记符转换为真正的直接引用,找到对应的直接地址。

初始化:给类的静态变量赋值正确的值。

public class Test{ public static int a = 1; }

如上述代码所示,类加载的过程大致如下:

1、加载:编译文件为 .class 文件,通过类加载,加载到JVM。

2、连接: (1) 保证Class类文件没有问题;(2) 给int类型分配内存空间,a = 0;(3) 符号引用转换为直接引用。

3、初始化:经过这个阶段的解析,把1赋值给变量 a。

static加载分析

先来看一段代码:

package com.wunian.classloader; /** * static加载分析 * * JVM参数 * -XX:+TraceClassLoading 用于追踪类的加载信息并打印出来 * rt.jar 出厂自带,最高级别的类加载器要加载的 * 分析项目启动为什么这么慢,快速定位自己的类有没有被加载 */ public class ClassLoaderDemo2 { ​ public static void main(String[] args) { ​ System.out.println(MyChild1.str2); } } ​ class MyParent1{ ​ private static String str="Hello World!"; ​ static{ System.out.println("MyParent1 static"); } } ​ class MyChild1 extends MyParent1{ ​ public static String str2="Hello World,My Child"; ​ static{ System.out.println("MyChild1 static"); } }

最终运行结果为:

MyParent1 static MyChild1 static Hello World,My Child

可以在启动参数VM options中添加-XX:+TraceClassLoading参数追踪类的加载信息并打印出来,这将有助于理解类的加载顺序。 由于MyChild1继承了MyParent1,因此在MyChild1调用str2的时候,会进行初始化,初始化之前必须先初始化其父类MyParent1,初始化时首先会加载static代码块,因此我们会看到父类的static代码块中的输出语句先执行,父类初始化完毕后再初始化子类,因此MyChild1类的static代码块接着被加载,最后才加载static成员变量。 因此可以得到结论:

初始化一个类时,首先会初始化其父类。类初始化时会先加载static代码块,然后加载static变量。

final加载分析

先来看两段代码:

package com.wunian.classloader; /** * final加载分析 */ public class ClassLoaderDemo3 { ​ public static void main(String[] args) { System.out.println(MyParent2.str); } } ​ class MyParent2{ //final常量在编译阶段时候放入常量池 //这里将常量放入了ClassLoaderDemo3的常量池中,之后ClassLoaderDemo3与MyParent2就没关系了 public static final String str="hello world"; ​ static { System.out.println("MyParent2 static"); } }

package com.wunian.classloader; ​ import java.util.UUID; ​ /** * final加载分析 * * 当一个常量的值并非编译期间可以确定的,那么这个值不会放入方法调用类的常量池中 * 程序运行期间的时候,会主动使用常量所在的类 */ public class ClassLoaderDemo4 { public static void main(String[] args) { System.out.println(MyParent4.str); } } ​ ​ class MyParent4{ ​ public static final String str= UUID.randomUUID().toString(); ​ static { System.out.println("MyParent4 static"); } }

最终运行结果分别为:

hello world

MyParent4 static 628c844a-e186-4287-8ac8-3809ec660f0b

为什么看起来相似的两段代码其运行结果却完全不同呢? 这是由常量的加载机制决定的:

当一个常量的的值在编译期间可以确定,就会在编译阶段放入方法调用类的常量池中,之后方法调用类和常量所在的类就没有关系了。因此第一段代码不会加载MyParent2中的静态代码块,也就不会输出“MyParent2 static”了。反之,当一个常量的值并非编译期间可以确定的,那么这个值不会放入方法调用类的常量池中, 程序运行期间的时候,会主动使用常量所在的类。显然第二段代码中的str的值调用了UUID类的randomUUID方法,一定会去初始化UUID类,因此不能在编译期确定str的值,因此会初始化MyParent4类,导致static代码块会被加载,所以先输出了“MyParent4 static”。

ClassLoader分类

Java虚拟机自带三种加载器:

BootStrap,根加载器,加载系统的包,如JDK核心库中的类rt.jar包。Ext,扩展类加载器,加载一些扩展jar包中的类。Sys/App,系统(应用类)加载器,加载开发人员自己编写的类。

只需要继承ClassLoader这个抽象类,就能自定义自己的类加载器,一般很少使用。

双亲委派机制

双亲委派模型是JVM中类的加载机制,这个模型要求除了Bootstrap加载器外,其余的类加载器都要有自己的父加载器。子加载器通过组合来复用父加载器的代码,而不是使用继承。在某个类加载器加载class文件时,它首先委托父加载器去加载这个类,依次传递到顶层类加载器(Bootstrap)。如果顶层加载不了(它的搜索范围中找不到此类),子加载器才会尝试加载这个类。 双亲委派机制可以保护Java的核心类不会被自己定义的类所替代,一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推。 示例代码如下:

package com.wunian.classloader; ​ public class ClassLoaderDemo5 { ​ public static void main(String[] args) { ​ Object o=new Object();//jdk自带的 ClassLoaderDemo5 demo5=new ClassLoaderDemo5();//实例化一个自己定义的对象 ​ //null在这里并不代表没有,只是java触及不到 System.out.println(o.getClass().getClassLoader());//null System.out.println(demo5.getClass().getClassLoader());//AppClassLoader System.out.println(demo5.getClass().getClassLoader().getParent());//ExtClassLoader System.out.println(demo5.getClass().getClassLoader().getParent().getParent());//null ​ //思考:为什么自己定义的java.lang.String没有生效? //jvm中有机制可以保护自己的安全 //双亲委派制度: 一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推 // 双亲委派机制 可以保护java的核心类不会被自己定义的类所替代 // AppClassLoader 03 // ExtClassLoader 02 // BootStrap (最顶层) 01 java.lang.String rt.jar } }

Native方法

我们都知道,Java是无法开启线程的,必须调用C语言的库来开启线程,因为Java无法操作硬件。 native,只要带了此关键字,说明Java的作用范围达不到,只能去调用底层C语言的库。 Robot类提供了许多鼠标键盘操作的方法,这些方法都使用了native关键字,可以通过这个类实现一个简单的自动化脚本。

为什么会有Native这个东西?

1995年,java 必须可以去调用 c、c++的库,所以说Java就在内存中专门开辟了一块区域标记为 native 方法。

程序计数器

每个线程都有一个程序计数器,是线程私有的。 程序计数器就是一块十分小的内存空间,几乎可以忽略不计。

作用:可以看做当前字节码执行的行号指示器。 分支、循环、跳转、异常处理都需要依赖于程序计数器来完成。

如上图所示,左侧是代码,右侧是对应的Java字节码,Code列就是程序计时器,冒号右边是底层的程序指令:

bipush: 将 int、float、String、常量值推送至栈顶。

istore:将一个数值从操作数栈存储到局部变量表。

iadd:相加。

imul:相乘。

方法区渊源

方法区(Method Area )是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享。 方法区主要用来存储类信息,常量,字符串、静态变量、符号引用、方法代码等。 JDK1.7之前

永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等,这些东西都会放到永久代中。 永久代大小空间是有限的,如果满了会报出异常: OutOfMemoryError:PermGen

JDK1.8之后

彻底将永久代移除HotSpot JVM ,虚拟机加载类信息,常量,字符串、静态变量等放到了堆中或者元空间(Metaspcace)。 元空间就是方法区在HotSpot JVM的实现。

元空间和永久代,都是对JVM规范中方法区的实现。

元空间和永久代最大的区别:元空间并不在Java虚拟机中,使用的是本地内存

设置元空间大小的JVM参数:-XX:MetaspaceSize10m

栈和队列

栈和队列都是基本的数据结构。

栈:后进先出(LIFO-last in first out),最后插入的元素最先出来。

队列:先进先出(FIFO-first in first out),最先插入的元素最先出来。

它们的结构如下图所示。

程序运行的过程其实就是压栈的过程,栈空了,线程也就结束了。如下图所示。

Stack栈是什么

栈就是管理程序运行的,栈用来存储一些基本类型的值,局部变量,对象的引用,方法等。

栈的优势:存储速度比堆快,仅次于寄存器,栈的数据不可以共享。

当我们使用new关键字创建对象时,实际上是在堆中创建出了一个实例对象,然后在栈中创建该对象的引用,将引用指向堆中的实例对象。如下图所示。

模拟栈溢出异常,代码如下:

package com.wunian.stack; ​ /** * StackOverflow 栈溢出 */ public class StackOverflowDemo { ​ public static void main(String[] args) { a(); } ​ //main a a a a a... 栈满 //java.lang.StackOverflowError private static void a() { a(); } }

因此,栈里面是一定不会存在垃圾回收问题的,一旦线程结束,该栈也就结束了,栈的生命周期和线程是一致的。

栈的原理

Java栈的组成元素是栈帧。 每一次函数的调用,都会在调用栈上维护一个独立的栈帧(Stack Frame)。栈帧的结构如下图所示。

栈、堆、和方法区的交互如下图所示。

注:这里的栈指的是JVM版本为HotSpot中的栈。

常见的几种JVM

HotSpot(SUN公司)JRockit(BEA公司)J9VM(IBM公司)

一个JVM实例中只存在一个堆。堆的内存大小是可以调节的。 堆可以存放类、方法、常量和保存了类型引用的真实信息。 堆在逻辑上分为三个部分:

新生区(Young),分为Eden区、Survivor区(s0、s1)养老区(Old Tenure)永久区(Perm)

在JDK1.8以后,永久区被元空间替代,在物理上只有新生区和养老区,元空间在本地内存中,不在JVM中。 GC垃圾回收主要是在新生区和养老区。 GC又分为普通GCFull GC,如果堆满了,就会爆出OutOfMemory异常。

新生区

新生区就是一个类诞生、成长、消亡的地方。 新生区又分为伊甸(Eden)区、幸存者(Survivor)区。幸存者区又分为from区和to区。所有的类在伊甸区被new出来,慢慢的当伊甸区满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入幸存者区,清理了15次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收。这时候就会将这个对象送入养老区,运行了几个月之后,养老区满了,就会触发一次 Full GC。 假设一个项目运行1年后,整个堆空间彻彻底底的满了,突然有一天系统 爆出OOM异常,这时就需要排除OOM问题或者重启系统。

Sun HotSpot 虚拟机中,堆中的内存管理采用的是分代管理机制,即不同区域使用不同的算法。 在伊甸区99%的对象都是临时对象。

养老区

在新生区经过15次GC都幸存下来的对象进入养老区,养老区满了之后,会触发Full GC。这个15次是默认值,可以修改。

永久区

永久区存放的是一些JDK自身携带的类、接口的元数据。这里的对象几乎不会被垃圾回收。 如果系统爆出OutOfMemoryError:PermGen,说明永久代不够用了,可能原因是加载了大量的第三方包。

永久代在不同JDK版本中有差异:

JDK1.6之前,有永久代,常量池在方法区。

JDK1.7,有永久代,但是开始尝试去永久代,常量池在堆中。

JDK1.8 之后,没有永久代,取而代之的是元空间;常量池在元空间中。

注:方法区和堆一样,是线程共享区域,是JVM规范中的一个逻辑的部分,它还有一个别名:非堆

初识堆内存调优

Java环境:HotSpot、JDK1.8。

测试一(元空间是否在JVM中)

调优参数:

-XX:+PrintGCDetails:输出详细的垃圾回收信息。

-Xmx: 最大分配内存,默认为物理内存的1/4。

-Xms: 初始分配的内存大小,默认为物理内存的1/64。

测试代码如下:

package com.wunian.oom; ​ /** * 默认情况 * maxMemory:1801.0MB 虚拟机试图使用的最大内存量,一般是物理内存的1/4 * totalMemory:123.0MB 虚拟机试图默认的内存总量,一般是物理内存的1/64 * * 我们可以自定义堆内存的总量 * -XX:+PrintGCDetails 输出详细的垃圾回收信息 * -Xmx:最大分配内存 1/4 * -Xms:初识分配的内存大小 1/64 * * 调优参数:-Xmx1024m -Xms1024m -XX:+PrintGCDetails */ public class HeapParamDemo { ​ public static void main(String[] args) { //获取堆内存的初识大小和最大大小 long maxMemory=Runtime.getRuntime().maxMemory(); long totalMemory=Runtime.getRuntime().totalMemory(); System.out.println("maxMemory="+maxMemory+"字节、"+(maxMemory/1024/(double)1024)+"MB"); System.out.println("totalMemory="+totalMemory+"字节、"+(totalMemory/1024/(double)1024)+"MB"); } }

控制台日志如图所示。

通过计算得知,虚拟机试图使用的最大内存量=新生区的总内存+养老区的总内存,说明元空间不在JVM中。

测试二(模拟OOM)

调优参数为:-Xms8m -Xmx8m -XX:+PrintGCDetails

测试代码如下:

package com.wunian.oom; ​ import java.util.Random; ​ /** * * * 调优参数:-Xms8m -Xmx8m -XX:+PrintGCDetails * * 分析GC日志: * [PSYoungGen: 1534K->504K(2048K)] 1534K->656K(7680K), 0.0472431 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] * 1.GC类型 GC:普通GC FullGC:重GC * 2.1534K 执行GC之前的大小 * 3.504K 执行GC之后的大小 * 4.(2048K) youngGen的total大小 * 5.0.0472431 secs] 清理的时间 * 6.user 总计GC所占用CPU的时间 sys OS调用等待的时间 real 应用暂停的时间 * * GC :串行执行 STW(Stop The World) 并行执行 G1 */ public class OOMDemo { ​ public static void main(String[] args) { //java.lang.OutOfMemoryError: Java heap space String str="OOM Exception"; while (true){ str+=str+new Random().nextInt(999999999) +new Random().nextInt(999999999); } } }

运行程序,爆出java.lang.OutOfMemoryError: Java heap space异常,原因是字符串无限进行自增,最后导致新生区满了。

Dump内存快照

在Java程序运行的时候,如果想测试运行的情况怎么办? 我们可以使用一些工具来查看:

jconsole(JDK自带)IDEA的debug模式Eclipse的MAT插件IDEA的JProfiler插件

JProfiler插件

JProfiler是一款性能瓶颈分析插件。

安装步骤:

1、IDEA安装 JProfiler 插件。Settings中搜索Plugins,再搜索JProfiler,下载安装插件并重启IDEA。

2、windows上安装 JProfiler,注意安装目录路径中不能含有中文和空格。

3、注册码激活。

4、IDEA绑定JProfiler,Settings中搜索JProfilerJProfiler executable项选择JProfiler安装目录下的bin目录中的jprofiler.exe文件。

快速体验JProfiler插件

先来模拟一个OOM异常,调优参数为:-Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError,代码如下:

package com.wunian.oom; ​ import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; ​ /** * Dump内存快照 * * jconsole、JProfile测试 * * -Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError //当JVM发生OOM时,自动生成DUMP文件 */ public class DumpDemo { ​ byte[] bytes=new byte[1*1024*1024]; ​ public static void main(String[] args) throws InterruptedException { /*System.out.println("start"); TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);*/ //DumpDemo dumpDemo=new DumpDemo(); ​ List<DumpDemo> list=new ArrayList<>(); int count=0; try { while(true){ list.add(new DumpDemo()); count++; } } catch (Throwable e) {//使用Throwable或Error System.out.println("count="+count); e.printStackTrace(); } } }

运行程序,我们可以发现在项目目录下自动生成了DUMP文件,我们可以使用JProfiler来打开,然后就可以对DUMP文件进行分析了。或者也直接点击IDEA右上角的JProfiler图标,一段时间后JProfiler会自动打开并监控到当前程序的运行情况。 如果觉得JProfiler操作太复杂,也可以直接去JDK的bin目录下打开jconsole.exe,连接到当前程序监听端口,也可以对当前程序的运行情况进行监控。

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
评论列表 (暂无评论,193人围观)

还没有评论,来说两句吧...