1、JVM是什么

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。

一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。

  • Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

2、Java对象编译过程

源文件编译成字节码,主要分成两个部分:

1:常量池:所有的Token(类名、成员变量名等)、符号的引用(方法引用、成员变量应用等)

2:方法字节码:各个类中的各个方法的字节码

字节码由Java虚拟机解析运行分成两个部分:

1:类加载

2:类的执行

3、JVM类加载机制

3.1 什么是类加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

3.2 类加载的顺序

从类被加载到虚拟机内存中开始,到卸御出内存为止,它的整个生命周期分为7个阶段,加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。其中验证、准备、解析三个部分统称为连接。

3.2.1 加载

1、将class文件加载在内存中。

2、将静态数据结构(数据存在于class文件的结构)转化成方法区中运行时的数据结构(数据存在于JVM的数据结构)。

注意:方法区中如果出现OOM,那么多半是因为加载的依赖太多

3、在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口。

3.2.2 连接(验证、准备、解析)

1、验证:确保加载的类符合JVM规范与安全。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件

2、准备:为static变量在方法区中分配空间,设置变量的初始值。例如static int a=3,在此阶段会a被初始化为0;

注意:准备阶段,只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是在对象初始化的时候分配值的

3、解析:

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:简单的理解就是字符串,比如引用一个类,java.util.ArrayList 这就是一个符号引用

直接引用:指针或者地址偏移量。引用对象一定在内存(已经加载)。

3.2.3初始化

初始化是类加载的最后阶段,初始化阶段是执行类构造器<clinit>()方法。在类构造器方法中,它将 由编译器自动收集类中的所有类变量的赋值动作(准备阶段的a正式被赋值3)和静态变量与静态语句块static{}合并

初始化,为类的静态变量赋予正确的初始值

在Java中对类变量进行初始值设定有两种方式:

①声明类变量时指定初始值

②使用静态代码块为类变量指定初始值

3.2.4 使用

正常使用

3.2.5 卸载

GC把无用的对象从内存中卸载

3.3 类加载器

1)Bootstrap ClassLoader

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的 class,由 C++ 实现,不是 ClassLoader 子类。

2)Extension ClassLoader

负责加载Java平台中扩展功能的一些 jar 包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的 jar 包。

3)App ClassLoader

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

4)Custom ClassLoader

属于应用程序根据自身需要自定义的 ClassLoader,如 Tomcat、jboss 都会根据 J2EE 规范自行实现 ClassLoader。

3.4 类加载器加载顺序

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 Classloader 已加载就视为已加载此类,保证此类只所有 ClassLoader 加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

3.4.1 验证代码

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

在获取ExtClassLoader的父loader的时候出现了null,这是因为Bootstrap Loader(引导类加载器)是用C++语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

4、JVM运行时内存数据区

4.1 概念解读

4.1.1 方法区

方法区和java堆一样,是线程共享的区域;

方法区的作用的就是用来存储:已经被虚拟机加载的类信息、常量、静态变量等

而且方法区还有另一种叫法:【非堆】,也有人给方法区叫做永久代

当方法区存储信息过大时候,也就是无法满足内存分配的时候报错。

4.1.1.1 运行时常量池

运行时常量池是方法区中的一部分,主要是用来存放程序编译期生成的各种字面量和符号引用,也就是在类加载之后会进入方法区的运行时常量池中存放

4.1.2 Java堆

Java堆是Java虚拟机管理内存最大的一块区域;并且Java堆是被所有线程共享的一块内存区域(最大的区域);

对于堆内存唯一的目的就是用来存放对象实例的,而且几乎所有的对象实例都是堆内存中分配的内存(可能会有一些对象逃逸分析技术会导致对象实例不是在Java堆中进行内存分配的)

经常会听到一些程序说“调优”,其中调优的95%部分都是在跟Java堆有关系的;

因为Java堆是垃圾收集器管理的主要区域

4.1.3 虚拟机栈

程序员经常说“堆栈”,其中的栈就是虚拟机栈,更确切的说,大家谈的栈是虚拟机中的局部变量表部分;

虚拟机栈描述的是:Java方法执行的内存模型;(说白了就是:虚拟机栈就是用来存储:

局部变量、操作栈、动态链表、方法出口这些东西;这些东西有个特点:都是线程私有的,所以虚拟机栈是线程私有的

因为虚拟机栈是私有的,当线程调用某一个方法再到这个方法执行结束;其实就是对应着一个栈帧在虚拟机入栈到出栈的过程;

对于虚拟机栈可能出现的异常有两种

1:如果线程请求的栈深度大于虚拟机栈允许的最大深度,报错:StackOverflowError

(这种错误经常出现在递归操作中,无限制的反复调用方法,最终导致压栈深度超过虚拟机允许的最大深度,就会报错)

2:java的虚拟机栈可以进行动态扩展,但随着扩展会不断的申请内存,当无法申请足够内存的时候就会报错:OutOfMemoryError

4.1.4 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是**为虚拟机使用到的Native方法服务(比如C语言写的程序和C++写的程序)

4.1.5 直接内存(了解)

直接内存(Direct Memory)并不是运行时数据区中的部分;但是这块儿往往会被大多数程序员忽略,不小心也会导致OOM的错误;

4.1.5.1 原始的socket IO

这是因为在JDK1.4之前java操作数据过程中使用的IO操作是原始的socket IO

传统的IO,通过socket的方式发送给服务端,需要干些什么事情:

1、先把文件读到操作系统的缓存当中

2、再把操作系统的缓存中内容读到应用中

3、再从应用的缓存当中读到发送的socket缓存当中

4、在从socket缓存当中把数据发出去

总共做了4次的拷贝。

4.1.5.2 NIO

NIO比较传统IO的话,系统中的buffer不再需要拷贝给应用了

而是read buffer 直接拷贝给socket buffer

我们的应用只需要在两个buffer之间建立一个管道的

这样省略了两次的copy。速度就快了很多

NIO可以直接使用Native(本地方法栈)函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为堆外内存的引用进行操作;

所以有时候程序员在分配内存时候经常会忽略掉直接内存。导致各个区域的内存总和大于物理内存限制,然后OOM。

4.2 JVM线程安全

4.3 JVM内存溢出

4.3.1 Java堆溢出

/**
 * -Xms20m -Xmx20m
 * -XX:+PrintGCDetails
 */
public class PrintGC_demo {
    public static void main(String[] args) throws InterruptedException {
        List<OOM_demo> list = new ArrayList<OOM_demo>();
        while (true){
            list.add(new OOM_demo());
        }
    }
}

class OOM_demo{
    private static final int _1kb = 1024 ;
    byte[] data = new byte[_1kb];
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at cn.itcast.jvm.heap.OOM_demo.<init>(PrintGC_demo.java:21)
    at cn.itcast.jvm.heap.PrintGC_demo.main(PrintGC_demo.java:14)

4.3.2 虚拟机栈和本地方法栈溢出

Java虚拟机规范中描述了两种异常:

1:如果线程请求的栈深度大于虚拟机允许的最大深度,则报错:StackOverFlowError

2:如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError

4.3.2.1 栈帧溢出

单个线程的栈帧溢出

public class StackOverFlow {
    public static void main(String[] args) {
        new StackOverFlow().pushStack();
    }
    int index = 0;
    public void pushStack(){
        System.out.println("压栈第 :  "+index++);
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread().getState();
        pushStack();
    }
}
压栈第 :  1174
压栈第 :  1175
压栈第 :  1176
压栈第 :  1177
压栈第 :  1178
压栈第 :  1179
压栈第 :  1180
压栈第 :  1181Exception in thread "main" java.lang.StackOverflowError
    at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
    at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
    at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
    at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
    at 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.newLine(PrintStream.java:545)
    at java.io.PrintStream.println(PrintStream.java:807)
    at cn.itcast.jvm.oom.StackOverFlow.pushStack(StackOverFlow.java:14)
    at cn.itcast.jvm.oom.StackOverFlow.pushStack(StackOverFlow.java:20)

4.3.2.2 栈溢出

创建很多线程,让虚拟机栈内存溢出。

/**
 * -Xms40m -Xmx40m
 */
public class StackThread extends Thread {
    public static volatile int index = 0;
    public static void main(String[] args) {
        new StackThread().addThread();
    }
    public void dont_stop(){
        while (true){ }
    }
    public void addThread(){
        while (true){
            Thread thread = new Thread(new Runnable()
            {
                public void run()
                {
                    System.out.println(index++);
                    dont_stop();
                }
            });
            thread.start();
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Thread.java:717)
    at cn.itcast.jvm.oom.StackThread.addThread(StackThread.java:24)
    at cn.itcast.jvm.oom.StackThread.main(StackThread.java:9)

线程的创建由操作系统来调度(初始化、调度、销毁)。

4.3.3 方法区和运行时常量池溢出

在方法区中有个小块区域叫做“常量池”。

常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = “java”这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是JVM的一块特殊的内存空间。

4.3.3.1 常量池测试

 public static void main(String[] args) {
        String a = "123";
        String b = "123";
        // ==操作比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量在堆中存储的地址是否相同,即栈中的内容是否相同。
        System.out.println(a == b);

        String aa = new String("123");
        String bb = new String("123");
        // ==操作比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量在堆中存储的地址是否相同
        // 这里是两个对象,引用自然不一样,所以返回false
        System.out.println(aa == bb);

        String c = "c";
        String cc = new String("c");
        //==操作比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量在堆中存储的地址是否相同
        System.out.println(c == cc);

        String d = "d";
        //当调用intern()方法时,不管使用什么方式定义一个字符串,都会首先在常量池中查找是否有相应的字符串存在,
        //如果有,直接返回常量池中的引用,否则,在常量池中生成相应的字符串并返回引用
        String dd = new String("d").intern();
        System.out.println(d == dd);
    }

/**
 * -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class MethodOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i=0;
        String abc = "abc";
        while (true){
            //jvm 方法区,方法区中有个常量池,String a = "a"
            list.add(String.valueOf(i).intern());
            list.add((abc+i+abc).intern());
            i = i+1;
        }
    }
}
  • 在JDK7中使用以下命令

    -XX:PermSize=10M -XX:MaxPermSize=10M
  • 在JDK8中使用以下命令

    -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m

 

  • 注意:在JDK1.8以后,正式移除了永久代!,取而代之的是【元空间】

    元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存[所以元空间Metaspace仍然在非堆中]。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

      -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。   -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

      除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:   -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集   -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

    所以上面的VM Args的参数稍微修改:

    -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m

4.3.3.2 内存溢出

/**
 * -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class MethodOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i=0;
        while (true){
            list.add(String.valueOf(i).intern());
        }
    }
}

4.3.4 直接内存溢出

当程序中使用NIO存储数据,存储的数据容量超过了本地方法栈允许的容量的时候,就会报错: java.lang.OutOfMemoryError: Direct buffer memory

/**
 * -Xmx20m -XX:MaxDirectMemorySize=10m
 */
public class DirectMemOOM {
    private static final int _1m = 1024*1024;
    public static void main(String[] args) throws IllegalAccessException {
        ByteBuffer.allocateDirect(11*_1m);
    }
}

5、 JVM垃圾回收算法

垃圾回收,就是要将内存中不用的对象清理掉,腾出空间。

5.1 如何判断这个对象是存活的还是无用的

5.1.1 引用计数器

定义:给每个对象分配一个计算器,当有引用指向这个对象时,计数器加1,当指向该对象的引用失效时,计数器减一。最后如果该对象的计算器为0时,java垃圾回收器会认为该对象是可回收的。

很多人认为的是:给对象添加一个引用计数器,每当有一个地方引用它时候,计数器值就+1,当引用失效的时候计数器就-1;所以得出结论:当这个对象的计数器为0的时候,就会被GC掉。

package cn.itcast.jvm.gc;

/**
 * 相互引用
 */
public class ObjectGC {
    //写一些变量,占用一些内存,方便GC日志的查看
    private static final int _1M = 1024*1024 ;
    private byte[] byteSize = new byte[10*_1M] ;

    public void testGC(){
        A a = new A();
        B b = new B();
        a.b = b ;
        b.a = a ;
        //即使下面进行a = null和b = null,但是A类对象仍然被B类对象中的字段引用着,
        // 尽管现在A类和B类独享都已经访问不到了,但是引用计数却都不为0.
        a = null ;
        b = null ;
        System.gc();
    }

    public static void main(String[] args) {
        new ObjectGC().testGC();
    }
}

class A{
    public B b;
}

class B{
    public A a;
}

从运行结果中可以清楚看到GC日志中包含“11112K->0K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数器算法来判断对象是否存活的。

 

 

 

分类: JAVA

发表评论

电子邮件地址不会被公开。 必填项已用*标注