深入理解JVM

JVM的位置

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

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;清理完一次垃圾之后,会将活下来的对象,会放入幸存者区,清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了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,连接到当前程序监听端口,也可以对当前程序的运行情况进行监控。

JVM调优参数

输出详细的垃圾回收信息:-XX:+PrintGCDetails
追踪类的加载信息并打印出来:-XX:+TraceClassLoading
设置最大分配内存:-Xmx10m
设置初始分配的内存大小:-Xms10m
发生OOM时,自动生成DUMP文件:-XX:+HeapDumpOnOutOfMemoryError

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wunianisme

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值