前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、虚拟机由来和优化

1.1、为什么要使用Java虚拟机

1、Java虚拟机运行在实际的计算器系统的软件,但是它本身也作为是一个操作系统,也拥有想象中的硬件,比如,堆栈,等。 它自己崩溃了不会影响别的应用程序

2、 Java语言非常重要的一个特性就是与平台的无关性,而Java虚拟机就是实现这个特点的关键, 一般的 高级语言如果要在不同的平台上运行,至少需要编译成不同的 目标代码。而引入Java语言虚拟机后,Java语言在不同平台上 运行时不需要重新编译

1.2、虚拟机一直在做哪些优化

1、类加载

1.1、类加载说明

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。

1.1.1、类的生命周期:

包括,加载、验证、准备、解析、初始化、使用、卸载7个阶段,其中验证、准备、解析3个被称为连接

WX20180411-180608@2x

1.1.2、JVM中OOP-KLASS模型 来表示Java对象堆区生成

元数据—— instanceKlass 对象会存在元空间(方法区)

对象实例—— instanceOopDesc 会存在Java堆。Java虚拟机栈中会存有这个对象实例的引用

1.1.2.1、具体过程

1、Jvm在加载class时,如果这个类没有被加载过,JVM就会进行类的加载,并在JVM内部创建一个instanceKlass对象表示这个类的运行时元数据(相当于Java层的Class对象)包括常量池、字段、方法等,存放在方法区

2、在new一个对象时,jvm创建instanceOopDesc,来表示这个对象,存放在堆区,引用,存放在栈区;它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象;instanceOopDesc对应java中的对象实例

3、HotSpot并不把instanceKlass暴露给Java使用,而会另外创建对应的instanceOopDesc来表示java.lang.Class对象,并将后者称为前者的“Java镜像”,instanceKlass持有指向oop引用(_java_mirror便是该instanceKlass对Class对象的引用)

4、要注意,new操作返回的instanceOopDesc类型指针指向instanceKlass,而instanceKlass指向了对应的类型的Class实例的instanceOopDesc

1578562322110

1.1.2.2、堆中实例化对象的结构

hotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构:

1578561760566

1、对象头:HotSpot虚拟机的对象头包括两部分信息:

mark:用于存储对象自身的运行时数据,如hashcode, GC分代年龄,锁状态标志,线程ID,时间戳等;

klass(元数据指针):即对象指向它的类元数据(instanceKlass实例)的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

数组长度:(只有数组对象有,如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

2、实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

3、对齐填充:第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。不做解释

1.2、类加载的过程

1.2.1 、加载

第一个阶段加载,虚拟机规范中并没有强制约束,这个交给虚拟机自由把控类加载时机以及加载方法

加载是类加载的第一个阶段,这个阶段,虚拟机完成3件事情,

1.2.1.1、加载工作

1、通过一个类的全限定名来获取定义此类的二进制字节流,JAVA开发团队说的很模糊啊,充满智力的开发人员在这个基础上,从jar获取,从war获取,从动态代理proxy中获取等等

2、将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构

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

1.2.1.2、 加载的三种方式

1、命令行启动应用时候由JVM初始化加载

2、通过Class.forName()方法动态加载

3、通过ClassLoader.loadClass()方法动态加载

1.2.2、验证:确保class文件的字节流中包含的信息符合当前虚拟机的要求

1、文件格式验证:字节流是否符合Class文件格式的规范。只有通过了这个阶段的验证,字节流才会进入内存的方法区进行存储。所以后面的3个阶段全是基于方法区的数据结构进行,不直接操作字节流。

2、元数据验证:对字节码的描述的信息进行语义分析,保证其符合Java语言规范。

3、字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

4、符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个动作发生在连接的第三阶段——解析,符合引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

1.2.3、准备:正式为类变量分配内存并设置类变量初始值的阶段

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配,需要注意的是仅包括static修饰的变量

不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

1.2.3.1、静态变量
public static int value = 123  

那么变量在准备阶段过后的初始值为0,而不是123,因为这个时候还没有执行java方法,123的动作在初始化阶段才会执行。

1.2.3.2、final变量
public final static int value = 123  

 被final修饰,则在准备阶段就会将123赋值给它

1.2.4、解析

解析过程是将常量池内的符号引用替换成直接引用。针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行

符号引用 (字符串,能根据这个字符串定位到指定的数据,比如java/lang/StringBuilder)

符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能够无歧义的定位到目标即可.

例如, 在Java中, 一个Java类将会编译成一个class文件. 在编译时, Java类并不知道所引用的类的实际地址, 因此只能使用符号引用来代替. 比如org.simple.People类引用了org.simple.Language类, 在编译时People类并不知道Language类的实际内存地址, 因此只能使用符号org.simple.Language来表示Language类的地址.

@:类和接口的全限定名

@:字段名称和描述符

@:方法名称和描述符

直接引用 (内存地址)

1、直接指向目标的指针.(个人理解为: 指向方法区中类对象, 类变量和类方法的指针)

2、相对偏移量. (指向实例的变量, 方法的指针)

3、一个间接定位到对象的句柄.

1.2.5、初始化阶

在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块等。 初始化阶段,才真正开始执行类中定义的Java程序代码

1.2.5.1、初始化条件

1、创建类的实例,也就是new的方式

2、访问某个类或接口的静态变量、对该静态变量赋值、调用类的静态方法

3、反射(如Class.forName(“com.shengsiyuan.Test”))

4、初始化某个类的子类,则其父类也会被初始化

5、 Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

1.2.5.2、初始化步揍

1、假如这个类还没有被加载和连接,则程序先加载并连接该类,加载过就不会重新加载了

2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

3、假如类中有初始化语句,则系统依次执行这些初始化语句

(1)加载父类(以下序号相同,表明初始化是按代码从上到下的顺序来的)
  1.为父类的静态属性分配空间并赋于初值
  2.执行父类静态初始化块;

(2)加载子类
  3.为子类的静态属性分配空间并赋于初值
  4.执行子类的静态的内容;

(3)加载父类构造器
  5.初始化父类的非静态属性并赋于初值
  6.执行父类的非静态代码块;
  7.执行父类的构造方法;

(4)加载子类构造器
  8.初始化子类的非静态属性并赋于初值
  9.执行子类的非静态代码块;
  10.执行子类的构造方法.
1.2.5.3、结束声明周期

1、执行了System.exit()方法

2、程序正常执行结束

3、程序在执行过程中遇到了异常或错误而异常终止

4、由于操作系统出现错误而导致Java虚拟机进程终止

1.2.6、测试 static

静态域的初始化和静态代码块的执行只在类加载的时候执行且只执行一次,也就是说类加载过就不会重新加载了

1父类
public class SuperClass {
    static{
        System.out.println("super class init!");
    }
    public static int a = 1;
    public final static int b = 1;
}

2子类
public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }
}
3测试
public class Test {

    static{
        System.out.println("test class init!");
    }

    public static void main(String[] args){
        System.out.println(SubClass.a);
    }
}

控制台
test class init!
super class init!
1

解释很明显没有输出子类SubClass中的static代码块的信息对于静态字段只有直接定义这个类的字段的类才会被加载

当子类添加一个静态字段的时候

public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }

    public static int s = 2; //对于静态字段,只有指定定义这个字段的类才会初始化
}

public class Test {

    static{
        System.out.println("test class init!");
    }

    public static void main(String[] args){
        System.out.println(SubClass.s);
    }
}

控制台
test class init!
super class init!
SubClass init!
2

解释:当初始化类的时候如果他的父类还没有被初始化则需要先初始化它的父类和上面的其实也不冲突啦



public final static int s = 2; //变成final常亮的时则不会初始化任何,因为它放到了常亮池中,并不是在类中获取的,所以不需要初始化,控制台只会打印出2


package com.mousycoder.staticTest;

public class HelloB extends HelloA {
    public HelloB() {
        System.out.println("HelloB");
    }

    {
        System.out.println("I’m B class");
    }
    static {
        
        System.out.println("static B");
    }

    public static void main(String[] args) {
        new HelloB();
    }
}

class HelloA {
    public HelloA() {
        System.out.println("HelloA");
    }

    {
        System.out.println("I’m A class");
    }
    static {
        System.out.println("static A");
    }
}


static A
static B
Im A class
HelloA
Im B class
HelloB()

    
    
// 存在父子关系,又有静态代码块,先执行父类静态代码块,再执行子类静态代码块,故打印static A static B

// 存在父子关系,又有非静态代码块,先执行父类非静态代码块,父类构造器,再执行子类非静态代码块,子类构造器故打印I'm A class HelloA I'm B class HelloB

1.3、类加载器

1.3.1、什么是类加载机制

虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析,准备到初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

1.3.2、什么是类加载器classLoader及加载过程

官方:

虚拟机设计团队,把类加载阶段中的,通过一个类的全限名称来描述二进制字节流,这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块成为类加载器

字面理解:

Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader。所以classLoader的目的在于把class文件装入到jvm中。

1.3.4、不同的类加载器

从java虚拟机的角度看看,只存在两种不同的类加载器,一种是启动类加载器Bootstrap ClassLoader ,这个类加载器使用C++实现,是虚拟机自身的一部分 ,另一个种是由java语言实现,独立于虚拟机外部,并且全都继承自抽象类Java.lang.ClassLoader,Java语言系统自带有三个类加载器:

3个类加载器,执行顺序(先给结果)具体看sun.misc.Launcher,它是一个java虚拟机的入口应用。

1. Bootstrap CLassloder 
2. Extention ClassLoader 
3. Application  ClassLoader
4. 
1)启动类加载器(Bootstrap ClassLoader):最顶层的加载类

最顶层的加载类,主要加载核心类库,这个类加载器负责将存放在%JAVA_HOME%\lib目录中的,或者可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,因为他是虚拟机的一部分

@Test
public void bootClassPath(){
    System.out.println(System.getProperty("sun.boot.class.path"));

   //Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar:
//Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar:
//Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar:
// Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar:
// /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar:
//Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar:
///Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar:
// Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes
   
    //或者执行下面的代码
    URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
    for (int i = 0; i < urls.length; i++) {
        System.out.println(urls[i].toExternalForm());
    }
    
    
}
2)扩展类加载器(Extension ClassLoader):

这个加载器由sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

@Test
public void ExtClassLoaderClassPath(){
    System.out.println(System.getProperty("java.ext.dirs"));

    //  /Users/healerjean/Library/Java/Extensions:
    //  /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext:
    //  /Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extension:
    //  /usr/lib/java

}
3)应用程序类加载器(Application ClassLoader):

这个类加载器sun.misc.Launcher.AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器

如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader systemClassLoader = sun.misc.Launcher.getLauncher().getClassLoader();

//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(System.getProperty("java.class.path"));
   
    
    
D:\programFiles\IntelliJ IDEA 2018.3.5\lib\idea_rt.jar;D:\programFiles\IntelliJ IDEA 2018.3.5\plugins\junit\lib\junit-rt.jar;D:\programFiles\IntelliJ IDEA 2018.3.5\plugins\junit\lib\junit5-rt.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\charsets.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\access-bridge-64.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\cldrdata.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\dnsns.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\jaccess.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\localedata.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\nashorn.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\sunec.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\sunjce_provider.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\sunmscapi.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\sunpkcs11.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\ext\zipfs.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\jce.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\jsse.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\management-agent.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\resources.jar;D:\programFiles\java-1.8.0-openjdk\jre\lib\rt.jar;D:\study\HealerJean.github.io\_posts\8_Jvm\hlj-jvm\com-hlj-admin\target\test-classes;D:\study\HealerJean.github.io\_posts\8_Jvm\hlj-jvm\com-hlj-admin\target\classes;D:\study\HealerJean.github.io\_posts\8_Jvm\hlj-jvm\com-hlj-dao\target\classes;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.1.0.RELEASE\spring-boot-starter-web-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-starter\2.1.0.RELEASE\spring-boot-starter-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot\2.1.0.RELEASE\spring-boot-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.1.0.RELEASE\spring-boot-autoconfigure-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.1.0.RELEASE\spring-boot-starter-logging-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.11.1\log4j-to-slf4j-2.11.1.jar;C:\Users\HealerJean\.m2\repository\org\apache\logging\log4j\log4j-api\2.11.1\log4j-api-2.11.1.jar;C:\Users\HealerJean\.m2\repository\org\slf4j\jul-to-slf4j\1.7.25\jul-to-slf4j-1.7.25.jar;C:\Users\HealerJean\.m2\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;C:\Users\HealerJean\.m2\repository\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.1.0.RELEASE\spring-boot-starter-json-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.9.7\jackson-datatype-jdk8-2.9.7.jar;C:\Users\HealerJean\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.9.7\jackson-datatype-jsr310-2.9.7.jar;C:\Users\HealerJean\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.9.7\jackson-module-parameter-names-2.9.7.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.1.0.RELEASE\spring-boot-starter-tomcat-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.12\tomcat-embed-core-9.0.12.jar;C:\Users\HealerJean\.m2\repository\org\apache\tomcat\embed\tomcat-embed-el\9.0.12\tomcat-embed-el-9.0.12.jar;C:\Users\HealerJean\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.12\tomcat-embed-websocket-9.0.12.jar;C:\Users\HealerJean\.m2\repository\org\hibernate\validator\hibernate-validator\6.0.13.Final\hibernate-validator-6.0.13.Final.jar;C:\Users\HealerJean\.m2\repository\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;C:\Users\HealerJean\.m2\repository\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-web\5.1.2.RELEASE\spring-web-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-webmvc\5.1.2.RELEASE\spring-webmvc-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-starter-test\2.1.0.RELEASE\spring-boot-starter-test-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-test\2.1.0.RELEASE\spring-boot-test-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-test-autoconfigure\2.1.0.RELEASE\spring-boot-test-autoconfigure-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\com\jayway\jsonpath\json-path\2.4.0\json-path-2.4.0.jar;C:\Users\HealerJean\.m2\repository\net\minidev\json-smart\2.3\json-smart-2.3.jar;C:\Users\HealerJean\.m2\repository\net\minidev\accessors-smart\1.2\accessors-smart-1.2.jar;C:\Users\HealerJean\.m2\repository\org\ow2\asm\asm\5.0.4\asm-5.0.4.jar;C:\Users\HealerJean\.m2\repository\org\assertj\assertj-core\3.11.1\assertj-core-3.11.1.jar;C:\Users\HealerJean\.m2\repository\org\mockito\mockito-core\2.23.0\mockito-core-2.23.0.jar;C:\Users\HealerJean\.m2\repository\net\bytebuddy\byte-buddy\1.9.3\byte-buddy-1.9.3.jar;C:\Users\HealerJean\.m2\repository\net\bytebuddy\byte-buddy-agent\1.9.3\byte-buddy-agent-1.9.3.jar;C:\Users\HealerJean\.m2\repository\org\objenesis\objenesis\2.6\objenesis-2.6.jar;C:\Users\HealerJean\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;C:\Users\HealerJean\.m2\repository\org\hamcrest\hamcrest-library\1.3\hamcrest-library-1.3.jar;C:\Users\HealerJean\.m2\repository\org\skyscreamer\jsonassert\1.5.0\jsonassert-1.5.0.jar;C:\Users\HealerJean\.m2\repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-core\5.1.2.RELEASE\spring-core-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-jcl\5.1.2.RELEASE\spring-jcl-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-test\5.1.2.RELEASE\spring-test-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\xmlunit\xmlunit-core\2.6.2\xmlunit-core-2.6.2.jar;C:\Users\HealerJean\.m2\repository\org\springframework\boot\spring-boot-starter-aop\2.1.0.RELEASE\spring-boot-starter-aop-2.1.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-aop\5.1.2.RELEASE\spring-aop-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\aspectj\aspectjweaver\1.9.2\aspectjweaver-1.9.2.jar;C:\Users\HealerJean\.m2\repository\org\springframework\security\spring-security-core\4.2.3.RELEASE\spring-security-core-4.2.3.RELEASE.jar;C:\Users\HealerJean\.m2\repository\aopalliance\aopalliance\1.0\aopalliance-1.0.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-beans\5.1.2.RELEASE\spring-beans-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-context\5.1.2.RELEASE\spring-context-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\spring-expression\5.1.2.RELEASE\spring-expression-5.1.2.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\security\spring-security-web\4.2.3.RELEASE\spring-security-web-4.2.3.RELEASE.jar;C:\Users\HealerJean\.m2\repository\com\github\pukkaone\logback-gelf\1.1.9\logback-gelf-1.1.9.jar;C:\Users\HealerJean\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.9.7\jackson-databind-2.9.7.jar;C:\Users\HealerJean\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;C:\Users\HealerJean\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.9.7\jackson-core-2.9.7.jar;C:\Users\HealerJean\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;C:\Users\HealerJean\.m2\repository\com\rabbitmq\amqp-client\5.4.3\amqp-client-5.4.3.jar;C:\Users\HealerJean\.m2\repository\ch\qos\logback\logback-core\1.1.6\logback-core-1.1.6.jar;C:\Users\HealerJean\.m2\repository\ch\qos\logback\logback-classic\1.1.6\logback-classic-1.1.6.jar;C:\Users\HealerJean\.m2\repository\ch\qos\logback\logback-access\1.1.6\logback-access-1.1.6.jar;C:\Users\HealerJean\.m2\repository\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;C:\Users\HealerJean\.m2\repository\net\sf\json-lib\json-lib\2.4\json-lib-2.4-jdk15.jar;C:\Users\HealerJean\.m2\repository\junit\junit\4.12\junit-4.12.jar;C:\Users\HealerJean\.m2\repository\org\projectlombok\lombok\1.18.2\lombok-1.18.2.jar;C:\Users\HealerJean\.m2\repository\io\springfox\springfox-swagger2\2.7.0\springfox-swagger2-2.7.0.jar;C:\Users\HealerJean\.m2\repository\io\swagger\swagger-annotations\1.5.13\swagger-annotations-1.5.13.jar;C:\Users\HealerJean\.m2\repository\io\swagger\swagger-models\1.5.13\swagger-models-1.5.13.jar;C:\Users\HealerJean\.m2\repository\io\springfox\springfox-spi\2.7.0\springfox-spi-2.7.0.jar;C:\Users\HealerJean\.m2\repository\io\springfox\springfox-core\2.7.0\springfox-core-2.7.0.jar;C:\Users\HealerJean\.m2\repository\io\springfox\springfox-schema\2.7.0\springfox-schema-2.7.0.jar;C:\Users\HealerJean\.m2\repository\io\springfox\springfox-swagger-common\2.7.0\springfox-swagger-common-2.7.0.jar;C:\Users\HealerJean\.m2\repository\io\springfox\springfox-spring-web\2.7.0\springfox-spring-web-2.7.0.jar;C:\Users\HealerJean\.m2\repository\org\reflections\reflections\0.9.11\reflections-0.9.11.jar;C:\Users\HealerJean\.m2\repository\org\javassist\javassist\3.21.0-GA\javassist-3.21.0-GA.jar;C:\Users\HealerJean\.m2\repository\com\google\guava\guava\18.0\guava-18.0.jar;C:\Users\HealerJean\.m2\repository\com\fasterxml\classmate\1.4.0\classmate-1.4.0.jar;C:\Users\HealerJean\.m2\repository\org\springframework\plugin\spring-plugin-core\1.2.0.RELEASE\spring-plugin-core-1.2.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\springframework\plugin\spring-plugin-metadata\1.2.0.RELEASE\spring-plugin-metadata-1.2.0.RELEASE.jar;C:\Users\HealerJean\.m2\repository\org\mapstruct\mapstruct\1.1.0.Final\mapstruct-1.1.0.Final.jar;C:\Users\HealerJean\.m2\repository\io\springfox\springfox-swagger-ui\2.7.0\springfox-swagger-ui-2.7.0.jar;D:\programFiles\IntelliJ IDEA 2018.3.5\lib\idea_rt.jar
    
    

1.3.5、Launcher代码解释

Launcher 是java虚拟机的入口应用 ,代码有精简,具体自己看

1. Launcher初始化了ExtClassLoader和AppClassLoader。 
2. 
2. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}

    /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}

1.3.6、实例测试

public class Jvm02Test {
}

1.3.6.1、自己编写的是由AppClassLoader加载的。

解释:也就是说明Jvm02Test.class文件是由AppClassLoader加载的。

public class Jvm02ClassLoader {

    /**
     * 1、 也就是说明Jvm02Test.class文件是由AppClassLoader加载的。
     */
    @Test
    public  void testClassLoader (){

        ClassLoader classLoader = Jvm02Test.class.getClassLoader();

        System.out.println("ClassLoader is:" + classLoader.toString());
        // ClassLoader is:sun.misc.Launcher$AppClassLoader@18b4aac2
    }
    
}

1.3.6.2、系统自带的int String是通过Bootstrap ClassLoader加载呢

解释 : 提示的是空指针,意思是int.class这类基础类没有类加载器加载?

当然不是! ,int.class是由Bootstrap ClassLoader加载的。要想弄明白这些,我们首先得知道一个前提。

  每个类加载器都有一个父加载器,通过getParent方法
    @Test
    public void intClassLoader(){

      ClassLoader  classLoader = int.class.getClassLoader() ;
      System.out.println("int is:" + classLoader.toString());
      //空指针异常
      //java.lang.NullPointerException

      ClassLoader stringClassLoader = String.class.getClassLoader() ;
      System.out.println(stringClassLoader.toString());
//     java.lang.NullPointerException
    }
1.3.6.3、父加载器

解释 :这个说明,AppClassLoader的父加载器是ExtClassLoader。那么ExtClassLoader的父加载器又是谁呢?


@Test
public void classLoaderParent(){

    ClassLoader classLoader = Jvm02Test.class.getClassLoader();
    System.out.println("ClassLoader is:" + classLoader.toString());
    //  AppClassLoader
    //打印信息:" ClassLoader is:sun.misc.Launcher$AppClassLoader@18b4aac2

    System.out.println("ClassLoader\'s parent is:"+classLoader.getParent().toString());
    //ExtClassLoader@
    //打印信息:"   ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@531d72ca

}


1.3.6.4、ExtClassLoader的父加载器

解释:又是一个空指针异常,这表明ExtClassLoader也没有父加载器。那么,为什么标题又是每一个加载器都有一个父加载器呢?这不矛盾吗?为了解释这一点,我们还需要看下面的一个基础前提。

@Test
public  void testGrandClassLoader (){

ClassLoader classLoader = Jvm02Test.class.getClassLoader();
	System.out.println("ClassLoader is:" + classLoader.toString());
    // ClassLoader is:sun.misc.Launcher$AppClassLoader@18b4aac2
  
    System.out.println("ClassLoader\'s parent is:"+classLoader.getParent().toString());
    //ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@3941a79c
    
    System.out.println("ClassLoader\'s grand father is:"+classLoader.getParent().getParent().toString());
    //跑出了异常,空指针
    //Exception in thread "main" java.lang.NullPointerException

}
    
1.3.6.5、总结:父加载器不是父类

但上面一小节代码中,为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢?先从URLClassLoader说起,这个类又是什么?

launch中已经有了下面这两个内部类,可以看见ExtClassLoaderAppClassLoader同样继承自URLClassLoade

static class ExtClassLoader extends URLClassLoader {
    ……
}

static class AppClassLoader extends URLClassLoader {

    static {
        ClassLoader.registerAsParallelCapable();
    }

    public static ClassLoader getAppClassLoader(final ClassLoader extcl)
        throws IOException
    {
        final String s = System.getProperty("java.class.path");
        final File[] path = (s == null) ? new File[0] : getClassPath(s);

        // Note: on bugid 4256530
        // Prior implementations of this doPrivileged() block supplied
        // a rather restrictive ACC via a call to the private method
        // AppClassLoader.getContext(). This proved overly restrictive
        // when loading  classes. Specifically it prevent
        // accessClassInPackage.sun.* grants from being honored.
        //
        return AccessController.doPrivileged(
            new PrivilegedAction<AppClassLoader>() {
                public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
    }

    ……
}


WX20190208-161242@2x

URLClassLoader的源码中并没有找到getParent()方法。这个方法在ClassLoader.java中。

public class URLClassLoader extends SecureClassLoader implements Closeable {}


public class SecureClassLoader extends ClassLoader {}



public abstract class ClassLoader {
 
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
    // @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
 
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    ...
}
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
    if (parent == null)
        return null;
    return parent;
}
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    return scl;
}
 
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            //通过Launcher获取ClassLoader
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}
}

我们可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:

1、由外部类创建ClassLoader时直接指定一个ClassLoader为parent。

2、由getSystemClassLoader()方法生成,也就是说在sun.misc.Laucher通过getClassLoader()获取,直白的说,一个ClassLoader创建时如果没有指定parent(比如我们下面的自定义加载器,不包括ExtClassLoader),那么它的parent默认就是AppClassLoader。

public class Launcher {

    private ClassLoader loader;


    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
Launcher.ExtClassLoader var1;
var1 = Launcher.ExtClassLoader.getExtClassLoader(); 
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); //生成自己的父加载器

AppClassLoader的parent是ExtClassLoaderExtClassLoader的parent是null

ExtClassLoader的父加载器为null,但是Bootstrap CLassLoader却可以当成它的父加载器这又是为何呢?

@:Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。

@:虚拟机启动先加载Bootstrp loaderlathcher作为虚拟机启动的入口应用,JVM初始化sun.misc.Launcher,并在launcher中开始加载剩余两个,Bootstrp loader加载ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoaderAppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader

@: Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoaderBootStrapClassLoader 是一个纯的C++实现,没有对应的Java类。所以在Java中是取不到的。如果一个类的classloader是null。已经足可以证明他就是由BootStrapClassLoader 加载的

@:另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。

1.3.7、双亲委托(终于来到这里)

1.3.7.1、双亲委托原理

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

20170211135054825

1、一个类加载器查找class和resource时,是通过“委托模式”进行的,如果该类加载器是AppClassLoader,该类加载器查找资源时 ,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。 (也可能是自定义加载器,它的父加载器是AppClassLoader

2、递归,重复第1部的操作。

3、如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。

4、Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。

5、ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到则返回给委托的发起者(自定义类加载器),由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,抛出ClassNotFoundException异常。如果没有委托者,则同样抛出ClassNotFoundException异常

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检测是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父加载器不为空则调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则调用Bootstrap Classloader,前面说过ExtClassLoader的parent为null,所以它向上委托时,系统会为它指定Bootstrap ClassLoader。
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父加载器没有找到,则调用findclass
                    c = findClass(name);
 
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //调用resolveClass()
                resolveClass(c);
            }
            return c;
        }
    }

1.3.7.2、为什么要使用双亲委托这种模型呢

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

1.3.7.3、 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?

​ 必须满足两个条件 ,只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。

@:两个类名是否相同

@:是否由同一个类加载器实例加载的。

1.3.8、自定义ClassLoader

感谢博主 https://blog.csdn.net/andyzhaojianhui/article/details/72829386

不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?

1.3.8.1、自定义步骤

1、编写一个类继承自ClassLoader抽象类。

2、重写它的findClass()方法。

3、在findClass()方法中调用defineClass()

1.3.8.2、编辑一个测试文件

将它编译之后的class文件,放到一个任意的目录下面

package com.hlj.moudle.Jvm03类加载器;

public class Jvm02Test {

    public void say(){
        System.out.println("Say Hello");
    }
}


1.3.8.3、自定义ClassLoader示例之DiskClassLoader。

我们在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。

package com.hlj.moudle.Jvm03类加载器;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


/**
 * @Description
 * @Author HealerJean
 * @Date 2019/2/8  下午5:13.
 */
public class Jvm03DiskClassLoader extends ClassLoader{

        private String mLibPath;

        public Jvm03DiskClassLoader(String path) {
            // TODO Auto-generated constructor stub
            mLibPath = path;
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // TODO Auto-generated method stub

            String fileName = getFileName(name);

            File file = new File(mLibPath,fileName);

            try {
                FileInputStream is = new FileInputStream(file);

                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                int len = 0;
                try {
                    while ((len = is.read()) != -1) {
                        bos.write(len);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

                byte[] data = bos.toByteArray();
                is.close();
                bos.close();

                return defineClass(name,data,0,data.length);

            } catch (IOException e) {
                // TODO Auto-
    
1.3.8.4、测试
package com.hlj.moudle.Jvm03类加载器;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @Description
 * @Author HealerJean
 * @Date 2019/2/8  下午5:16.
 */
public class Jvm03ClassLoaderTest {

     public static void main(String[] args) throws Exception {

        //创建自定义classloader对象。
        Jvm03DiskClassLoader loader = new Jvm03DiskClassLoader("D:\\study\\HealerJean.github.io\\_posts\\8_Jvm\\hlj-jvm\\com-hlj-admin\\src\\main\\java");
        System.out.println(loader);
        //加载class文件
        Class c = loader.loadClass("com.hlj.moudle.Jvm03类加载器.Jvm02Test");
        System.out.println(loader.getParent());
        Object obj = c.newInstance();
        Method method = c.getDeclaredMethod("say", null);
        //通过反射调用Test类的say方法
        method.invoke(obj, null);

    }


}

1.3.9、ClosserLoader卸载Class

JVM中的Class只有满足三个条件,才能被GC回收,也就是该Class被卸载(unload):看上面

    public static void main(String[] args) throws Exception {

        //创建自定义classloader对象。
        Jvm03DiskClassLoader loader = new Jvm03DiskClassLoader("D:\\study\\HealerJean.github.io\\_posts\\8_Jvm\\hlj-jvm\\com-hlj-admin\\src\\main\\java");
        System.out.println(loader);
        //加载class文件
        Class c = loader.loadClass("com.hlj.moudle.Jvm03类加载器.Jvm02Test");
        System.out.println(loader.getParent());
        Object obj = c.newInstance();
        Method method = c.getDeclaredMethod("say", null);
        //通过反射调用Test类的say方法
        method.invoke(obj, null);

        //1、清除该类的实例
        obj = null;
        //2、清除该类的ClassLoader引用
        loader = null;
        //3、清除该class对象的引用
        c = null;
        // 执行一次gc垃圾回收
        System.gc();
        System.out.println("GC over");
    }

}

1.3.10、服务器启动共用一个AppClassLoader

返回结果只有一个Classloader,所以一般类很难被卸载,因此才到了方法区

@GetMapping("")
@ResponseBody
public ResponseBean get() {
    try {
        Set<ClassLoader> set = new HashSet<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                ClassLoader classLoader = Jvm02Test.class.getClassLoader();
                set.add(classLoader);
            }).start();
        }
        Thread.sleep(6000L);
        System.out.println("----------------------");
        System.out.println(set);
        return ResponseBean.buildSuccess(set);
    } catch (AppException e) {
        log.error(e.getMessage(), e);
        return ResponseBean.buildFailure(e.getCode(), e.getMessage());
    } catch (Exception e) {
        log.error(e.getMessage(), e);
        return ResponseBean.buildFailure(e.getMessage());
    }
}

1578644692110

1578644709948

2、JVM内存分配

2.1、理解

2.1.1、为什么要了解内存

由于虚拟机自动内存管理机制的帮助下,不需要为每一个new出来的对象,去写delete/free代码。不容易出现内存泄漏和内存溢出的问题, 但是如果一旦出现问题,那将是非常可怕的 ,如果不了解虚拟机是怎么使用内存的,那么排查错误将会成为一项非常艰难的工资。

2.1.2、内存分配误区

经常有人把java内存区分为堆内存和栈内存,这种分发是错误的,只能说明大多数程序员最密切的就是这两块,在多线程环境下,每个线程拥有一个栈和一个程序计数器。栈和程序计数器用来保存线程的执行历史和线程的执行状态,是线程私有的资源。其他的资源(比如堆、方法区)是由同一个进程内的多个线程共享。

2.2、内存分配

2.2.1、程序计数器(线程私有)

解释:较小的内存空间,可以当做当前线程所执行字节码的行号指示器,每个线程都有自己的行号指示器,互补干扰,因此这部分内存区域也叫作私有区域

2.2.2、Java虚拟机栈(线程私有)

解释:这里其实就是程序员通俗意义上的栈区,存放基本类型的变量和对象的引用(函数调用参数,函数返回值,函数返回地址,局部变量)

每次在方法执行的同时,都会创建一个栈帧,用来存储局部变量表,操作数栈(可以理解成数字),动态链接、方法出入口等信息。每一个方法执行的时候,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程

栈内存溢出

1、如果虚拟机请求栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError异常

2、如果虚拟机栈可以动态扩展(当前大部分虚拟机都可以)如果扩展时无法申请到足够的内存,将飘出OutOfMemoryError

2.2.3、本地方法栈 (线程私有)

本地方法栈和虚拟机栈区别是,虚拟机栈为执行的java方法服务,而本地方法栈为Native方法服务。它也会抛出StackOverflowErrorOutOfMemoryError异常

2.2.4、Java堆(线程共享)

解释:Java堆是被所有线程共享的区域,在虚拟机启动时候创建, 此区域的唯一目标就是存放对象实例和数组,Java堆是垃圾收集器管理的主要区域

2.2.5、方法区(非堆区)(线程共享)

解释:虽然Java虚拟机将方法区描述为作为堆区的一个逻辑部分,但是它有一个别名叫非堆区域

存储加载的类信息(类的信息包含类的版本、字段、方法、接口等描述信息。)、常量区、静态变量、JIT(即时编译器)处理后的数据等,

需要注意是常量池就在方法区中,也是我们这次需要关注的地方

2.2.6、运行时常量池

Java中的常量池,实际上分为两种形态:静态常量池运行时常量池。 我们常说的常量池,就是指方法区中的运行时常量池。

1)所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。

2)而运行时常量池运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用 ,这部分内容将在类加载后进入方法区的运行时常量池中存放。 ,将class文件中的常量池载入到内存中,并保存在方法区中

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

2.2.5.1、永久代和方法区的关系

《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。 那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区

因此,我们得到了结论,永久代是 HotSpot 的概念是一种实现,方法区是Java虚拟机规范中的定义,是一种规范,一个是标准,一个是实现。其他的虚拟机实现并没有永久代这一说法。

2.2.5.2、永久代大小参数配置

永久代是一段连续的内存空间,我们在 JVM 启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小,32 位机器默认的永久代的大小为 64M,64 位的机器则为 85M。

2.2.5.3、永久代垃圾回收

Java虚拟机规范中也说过不要求虚拟机在方法区实现垃圾收集, 永久代中的回收率则非常低。也就是说,回收方法区内的内存性价比很低,

永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,(永久代-XX:PermSize达到,这个值是动态变换的哦),会触发完全垃圾回收(Full GC)

由于我们可以通过‑XX:MaxPermSize设置永久代的大小,有一个明显的问题,一旦类的元数据超过了设定的大小(废弃的常亮容易被回收,方法中写了个String a = “abc”),程序就会耗尽内存,并出现内存溢出错误 (java.lang.OutOfMemoryError: PermGen space)。 一般情况下,我们为了防止内存空间变换,一般情况下我们会设置成一样的 ,所以这个时候就认为永久代没有垃圾回收 ,因为达到最大设定大小就会内存溢出**

回收内容

废弃常亮和无用的类,当然回收是可以,而不是一定能够回收,而不是和对象一样,不使用了就必然会回收

1、废弃的常亮

常量的回收与堆中对象的回收机制类似,当某个常量没有被任何对象引用的时候,这个常量就没有用了,就可以被回收。举例,当前系统中找不到任何String对象引用了常量池中的的某个字符串常量:abcd,那么abcd这个常量就会被回收。同理,常量池中其他类、方法、字段的符号引用也与此类似。

注意: JDK7开始字符串常量池在堆中分布,所以young gc过程会扫描该区域,因此如果字符串常量区非常庞大会导致young gc过程扫描的时间也会变长。但是,young gc阶段并不会对字符串常量区进行回收,具体回收阶段是在Full gc或者CMS gc阶段 (所以有的时候,如果老年代频繁GC,可能是字符串常亮过多且正在使用,无法回收。而老年代过小导致的,考虑提高老年代大小)

2、无用的类(必须满足下面三个条件才能被回收,一般很难保证)

  回收类的效率非常低,但在当前企业级应用大量使用反射、动态代理、CGLib等技术的前提下,类的回收也变得很重要。已加载类的回收条件非常苛刻,需要满足以下三个条件,才有可能被JVM回收:(可以参考上文类加载的卸载代码)

回收场景

在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能(对象设置为null,便于垃圾收集器回收),以保证永久代不会溢出。

  1. 该类所有的实例都已经被回收(不会再使用它的相关new 了),即Java堆中不存在该类的任何实例; 

  2. 加载该类的ClassLoader已经被回收,非常难保证,因为使用的系统的类加载器AppCloader,而且大家都是使用的同一个; 

  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

如何回收

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。

  • 是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制
  • 使用-verbose:class-XX:+TraceClassLoading-XX:+TraceClassUnLoading查看类的加载和卸载信息。
2.2.5.4、版本迭代
  • JDK1.7之前包括JDK1.7,可以使用如下-XX:PermSize: 方法区初始大小 -XX:MaxPermSize:方法区最大大小来调节方法区的大小,超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

  • JDK 1.7后,字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代
  • JDK1.8之后,永久代(PermGen)被完全的移除了,所以永久代的参数-XX:PermSize-XX:MaxPermSize也被移除了存储的类的元数据、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory),静态变量和常量池等并入堆中
2.2.5.4.1、为什么会从老年代变成元空间

JDK1.8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。

在原来的永久代划分中,永久代用来存放类的元数据信息、静态量以及常量池等。 现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了

  • 永久代大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,但是PermSize指定太小又很容易造成永久代内存溢出; 元空间的最大可分配空间就是系统可用内存空间,由于类的元数据分配在元空间中中。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值, 如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。。

  • 因为在 JDK7 之前的 HotSpot 虚拟机中,纳入字符串常量池的字符串被存储在永久代中,如果添加太多字符串常量到该区域,有些虚拟机不会做永久代的垃圾回收,而且如果参数设置不妥当,比如两个永久代参数设置一样,容易发生OOM),因此导致了一系列的性能问题和内存溢出错误 JDK1.8以后,运行时常亮、以及静态变量存在于堆中,可以被垃圾收集器进行回收了

  • HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。永久代会为GC带来不必要的复杂度,并且回收效率偏低

  • 原来的jar包及你自己项目的class存放的内存空间,这部分空间是固定的,启动参数里面-permSize确定,,且每个项目都会占用自己的permGen空间。改成metaSpaces各个项目会共享同样的class内存空间,比如两个项目都用了fast-json开源包,在mentaSpaces里面只存一份class,提高内存利用率,且更利于垃圾回收

2.2.5.4.2、元空间参数配置以及垃圾回收

对于64位JVM来说,元空间的默认初始大小是20.75MB,默认的元空间的最大值是无限。

元空间垃圾回收

只要类加载器存活,其加载的类的元数据也是存活的,就不会被回收掉,对于僵死的类及类加载器的垃圾回收将在元数据使用达到MetaspaceSize参数的设定值时进行,但是不会单独回收某个类,会把相关的空间整个回收掉

适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。

1.MetaspaceSize

初始空间大小,但不是初始容量,可以看做是扩容时触发FullGC的初始化阈值,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 这样就会导致我们使用命令jstat 看到的MU MC的大小和这里的会有非常巨大的差异。一般情况下,这个值大小根据不同的平台在12M到20M浮动。使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为21810376B(大约20.8M)。

  • 无论 -XX:MetaspaceSize 配置什么值,Metaspace的初始容量一定是 21807104 (约20.8m),如果没有配置-XX:MetaspaceSize,那么触发FGC的阈值是21807104(约20.8m);如果配置了-XX:MetaspaceSize,那么触发FGC的阈值就是配置的值;

  • Metaspace由于使用不断扩容到 -XX:MetaspaceSize 参数指定的量,就会发生FGC;且之后每次Metaspace扩容都会发生FGC
  • Metaspace容量范围为[20.8m, MaxMetaspaceSize);

2.MaxMetaspaceSize

限制Metaspace增长的上限,如果不设置的话会导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为17592186044415代表着无限大。

product(uintx, MaxNewSize, max_uintx,                                  \ 
        "Maximum new generation size (in bytes), max_uintx means set " \ 
        "ergonomically")  
    
在HotSpot VM里intx是跟平台字长一样宽的带符号整型uintx是其无符号版max_uintx是(uintx) -1也就是说在32位平台上是无符号的0xFFFFFFFF64位平台上则是0xFFFFFFFFFFFFFFFF

jmap -heap显示的部分参数是以MB为单位来显示的而MaxNewSize的单位是byte我跑例子的平台是64位的于是算一下 0xFFFFFFFFFFFFFFFF / 1024 / 1024 = 17592186044415 MB    
参数的说明告诉我们当MaxNewSize的值等于max_uintx时意思就是交由ergonomics来自动选择的最大大小并不是说最大大小真的有0xFFFFFFFFFFFFFFFF这么大

3.MinMetaspaceFreeRatio

当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。

4.MaxMetasaceFreeRatio

当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。

建议

1、MetaspaceSizeMaxMetaspaceSize 设置一样大;MaxMetaspaceSize如果设置太小,可能会导致频繁FullGC,甚至OOM。

2、具体设置多大,建议稳定运行一段时间后通过 jstat -gc pid 确认且这个值大一些,对于大部分项目256m即可。

测试 :同时设置 PermSize 和 MaxPermSize的大小。

1578043745362

1578043727287

1578043757120

@:在JDK1.6环境下,抛出OutOfMemoryError:PermGen space,永久代空间不足。

@:在JDK1.7JDK1.8环境下,抛出OutOfMemoryError:Java heap space,堆空间不足。

@:通过上面的报错信息也正好印证了咱们上面说的将常量池由永久代移动到了Java堆内存中。但是通过比对JDK1.7JDk1.8的报错信息咱们也可以看到,相比于JDK1.7,上图中JDK1.8的报错信息中多出了一部分红色的警告信息。Ignoring option PermSize/MaxPermSize= XXM;support was removerd in 8.0;意思就是,忽略这两个参数,这两个参数已经被删除了。这是因为从JDK1.8之后,永久代(PermGen)被完全的移除了,所以永久代的参数-XX:PermSize-XX:MaxPermSize也被移除了。

2.3、搞懂代码内存分配

2.3.1、堆和栈

2.3.1.1、栈:随着方法的结束和结束

由编译器自动分配释放存放基本类型的变量和对象的引用(函数调用参数,函数返回值,函数返回地址,局部变量)

也就是说线程上下文的东西都在栈上,其他都在堆上(包括类的成员变量也在堆上) , 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。 也就是随着方法而生,随着方法而亡

1、存放栈中的数据大小是固定的,生命周期也是固定的

大小固定:解释,写好代码后,其实里面的大小就已经固定了

int a = 3;,这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

2、栈的数据共享

int a = 3; int b = 3

@:编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。

@:接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

3、字面值的引用不同于类对象的引用

@:假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。

@:通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。

如2上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。而对象就会随着改变

4、栈优势:

栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以被内部线程共享

2.3.1.2、堆 :不会随着方法的结束而结束

一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回 收,堆内存用来存放由new创建的对象和数组。在堆中分配的内存,(包括类的成员变量上)

2.3.1.3、堆优势

堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢

2.3.2、java中的基本数据类型一定存储在栈中的吗

当然是错误的, 基本数据类型是放在栈中还是放在堆中,这取决于基本类型在何处声明,下面对数据类型在内存中的存储问题来解释一下:

2.3.2.1、在方法中声明的变量,局部变量

即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因

@:当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中

@:当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。

2.3.2.2、 在类中声明的变量是成员变量,也叫全局变量

放在堆中的,因为全局变量不会随着某个方法执行结束而销毁)。

@:当声明的是基本类型的变量其变量名及其值放在堆内存中的

@:引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中

2.3.2.3、图文举例说明

图一

1578551508710

String str1 = abc;
String str2 = abc;
String str3 = abc;
String str4 = new String(abc);
String str5 = new String(abc);

图二

1578551935334

public class Test{  
    public static void main(String args[]){  
        int date = 9;  
        Test test = new Test();        
        test.change(date);   
        BirthDate d1= new BirthDate(7,7,1970);         
    }    

    public void change1(int i){  
        i = 1234;  
    }   
}


class BirthDate {  
    private int day;  
    private int month;  
    private int year;      
    public BirthDate(int d, int m, int y) {  
        day = d;   
        month = m;   
        year = y;  
    }  
    省略get,set方法………  
}  




1. main方法开始执行int date = 9; date局部变量基础类型引用和值都存在栈中
2. Test test = new Test(); test为对象引用存在栈中对象(new Test())存在堆中
3. test.change(date); i为局部变量引用和值存在栈中当方法change执行完成后i就会从栈中消失
4. BirthDate d1= new BirthDate(7,7,1970);  d1为对象引用存在栈中对象(new BirthDate())存在堆中其中dmy为局部变量存储在栈中且它们的类型为基础类型因此它们的数据也存储在栈中day,month,year为成员变量它们存储在堆中(new BirthDate()里面)当BirthDate构造方法执行完之后d,m,y将从栈中消失  


2.3.3、final常亮存储在哪里

final修饰的成员变量!常量在类编译时期载入类的常量池中。

当 final 修饰的是一个引用类型数据时,也就是修饰一个对象时,引用在初始化后将永远指向一个内存地址,不可修改。但是该内存地址中保存的对象信息,是可以进行修改的。

@:final 修饰的成员变量(实例变量)和静态变量(静态变量也只能是用static修饰的成员变量)在常量池中,//暂时认为 final 对象也在常量池中

@:final修饰的局部变量,我们也可以称之为不可变变量。引用存储在栈中,对象存储在堆中

2.3.4、包装类 Integer

Java的8种基本类型(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除Float和Double以外, 其它六种都实现了常量池, 但是它们只在大于等于-128并且小于等于127时才使用常量池。

public void test3() {
    Integer i = 100;
    Integer j = 100;
    System.out.println(i == j); //true //常量池存储

    Integer i2 = 128;
    Integer j2 = 128;
    System.out.println(i2 == j2); //false 堆存储
}

2.4、异常判断

WX20190207-190335@2x

2.4.1、java堆溢出:java.lang.OutOfMemoryError: Java heap space

2.4.1.1、测试代码
 -Xms20m -Xmx20m
package com.hlj.moudle.jvm001.service;

import java.util.ArrayList;
import java.util.List;

public class Jvm01HeadOOM {

    static class OOMObject{}

    public static void main(String[] args) {
        List<OOMObject> oomObjects = new ArrayList<>();

        while (true){
            oomObjects.add(new OOMObject());
        }
    }
}

过一段时间之后报下面错误

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at 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 com.hlj.jvm.memory.JavaHeap.main(JavaHeap.java:19)

Process finished with exit code 1
2.4.1.2、异常分析和解决

可以看到上面的提示Java heap space java堆内存,如果出现上面的异常,从代码上看是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的的内存消耗

2.4.2、虚拟机栈和本地方法栈溢出:java.lang.StackOverflowError

-Xss:设置每个线程的栈大小。

JDK5.0以后每个线程栈大小为1M,以前每个线程栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

2.4.2.1、测试代码
 -Xss128k
package com.hlj.moudle.jvm001.service;

public class Jvm02JavaVMStackSOF {
    private int stackLength = 1; //堆内存

    public void stackLeak() {
        //栈内存存在 读取和相加,会导致一直加加加,导致溢出
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        Jvm02JavaVMStackSOF oom = new Jvm02JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}
2.4.2.2、异常分析和解决
stack length1547
Exception in thread "main" java.lang.StackOverflowError
	at com.hlj.moudle.jvm001.service.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
	at com.hlj.moudle.jvm001.service.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
	at com.hlj.moudle.jvm001.service.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)

解释:

​ 操作系统分配给每个进程的内存是有限制的,虚拟机提供了参数来控制方法区(非堆区MaxPermSize)和Java堆(Xmx)区这两部分的最大值

​ 操作系统剩余的内存减去Xmx(最大堆区容量)MaxPermSize(最大方法区容量),,程序计数器内存消耗很小可以忽略掉,如果虚拟机进程本身消耗的内存不计算在内,剩下的就给了虚拟机栈和本地方法栈了。

​ 每个线程分配到的栈容量越大,可以建立的线程数就越少,建立线程时就越容易将资源耗尽。

​ 栈深度在大多数情况下达到1000到2000没有问题,对于正常的方法调用(包括递归),这个深度完全够用了,但是如果是建立多线程导致的内存溢出,在不能减少线程数,或者更换虚拟机的情况下,就只能通过减少最大堆(这样可以让栈区多一些)、减少线程分配的栈容量(如果当前线程占用的栈资源允许的情况下)

3、JVM日志介绍

System.GC()

调用System.GC()方法也仅仅是一个请求(或建议)。JVM接收这个请求后,并不是立刻做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。

不要显示频繁调用System.GC()。虽然只是建议而已,但是很多情况下会触发GC回收,从而增加GC的回收频率,降低程序的运行性能;

3.1、案例分析

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度非常快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC,Major GC的速度一般会比Minor GC慢10倍以上。

3.1.1、垃圾收集器名称 (名称通过收集器而定)

收集器是Parallel Scavenge。新生代为PSYoungGen,老年代为ParOldGen,Metaspace代表元空间(JDK 8中用来替代永久代PermGen)。

3.1.2、JVM参数

-Xms20M -Xmx20M -Xmn10M  -XX:SurvivorRatio=8 -verbose:gc -XX:+PrintGCDetails
输出gc日志, 堆内存初始化大小20M,堆内存最大20M,新生代大小10M,那么剩余分配给老年代就是10M, 输出GC的详细日志,

3.1.3、GC控制台日志

[GC (System.gc()) [PSYoungGen: 4683K->1016K(9216K)] 4683K->3144K(19456K), 0.0018542 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1016K->0K(9216K)] [ParOldGen: 2128K->2968K(10240K)] 3144K->2968K(19456K), [Metaspace: 3238K->3238K(1056768K)], 0.0053601 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 164K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 2% used [0x00000000ff600000,0x00000000ff6290e0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 2968K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 28% used [0x00000000fec00000,0x00000000feee6290,0x00000000ff600000)
 Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 311K, capacity 388K, committed 512K, reserved 1048576K


3.1.4、日志总结

3.1.4.1、GC (System.gc():年轻代GC

user : 此次垃圾回收, 垃圾收集线程消耗的所有CPU时间(Total CPU time).

sys : 操作系统调用(OS call) 以及等待系统事件的时间(waiting for system event)

real: 等于 user 以及 system time 的总和。

1576220763413

[GC (System.gc()) [PSYoungGen: 4683K->1016K(9216K)] 4683K->3144K(19456K), 0.0018542 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 

 
年轻代可用大小为 9216K= 9*1024 (eden+survivor=9,另一个只是起零时存储的作用) 

第一个: 4683K->1016K(9216K)

表示GC前该新生代已使用容量4683K->GC后该新生代已使用容量1016K(新生代的总容量9216K

解释:GC后该新生代已使用容量1016K(也就是Survivor中的存储的大小)

第二个:4683K->3144K(19456K)

表示GC前Java堆已使用容量6812K->GC后Java堆已使用容量4936K(Java堆总容量19456K

3.1.4.2、Full GC (System.gc() :老年代/永久代的stop the world的GC

1576220906092

[Full GC (System.gc()) [PSYoungGen: 1016K->0K(9216K)] [ParOldGen: 2128K->2968K(10240K)] 3144K->2968K(19456K), [Metaspace: 3238K->3238K(1056768K)], 0.0053601 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

第一个 :PSYoungGen: 1016K->0K(9216K)

老年代GC前新生代已使用容量1016K———>老年代GC后新生代已使用容量(新生代总容量)

解释:老年代GC前新生代已使用容量1016K(一般有可能是,survivor中存放的内存大小)

第二个 :ParOldGen: 2128K->2968K(10240K)

表示老年代GC前老年代已使用的容量2128K->老年代GC后老年代已使用的容量2968K(老年代总容量)

第三个:3144K->2968K(19456K)

表示老年代GC前Java堆已使用的容量3144K->老年代GC后老Java堆已使用的容量2968K(Java堆的容量)

第四个:Metaspace: 3238K->3238K(1056768K)

解释:Metaspace代表元空间(JDK 8中用来替代永久代PermGen)。

表示老年代GC前永久代已使用的容量3238K->老年代GC后永久代已使用的容量2968K(永久代的容量)

1、Full Gc 和CMS Gc

@:Full GC == Major GC指的是对老年代/永久代的stop the world的GC

@:Full GC的次数 = 老年代GC时 stop the world的次数

@:Full GC的时间 = 老年代GC时 stop the world的总时间

@:Full GC本身不会先进行Minor GC,我们可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC。

@:CMS ,初始标记(Initial Mark) 、重新标记(Remark)两个阶段,会STW。所以这个一次正常的cms-gc就会产生两次fullgc。我们使用jstat -gcutil pid。就会看到fullgc进行了两次。

3.1.4.3、heap :表示的为Gc之后的内存分配情况
Heap
 PSYoungGen      total 9216K, used 164K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 2% used [0x00000000ff600000,0x00000000ff6290e0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 2968K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 28% used [0x00000000fec00000,0x00000000feee6290,0x00000000ff600000)
 Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 311K, capacity 388K, committed 512K, reserved 1048576K
2、解释元空间日志
Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 311K, capacity 388K, committed 512K, reserved 1048576K

可以看到整个metaspace使用了3262K,其中class space使用了311K,,整个metaspace的reserved大小为1056768K,其中class space的reserved大小为1048576K

##

问题

2、String

2.1、String.intern()分析 -结合2看

@:JDK1.6:复制的是字符串,返回常量池字符串地址

@:JDK1.7:复制的是引用,返回引用

判断这个常量是否存在于常量池。

  如果存在

   判断存在内容是引用还是常量,

    如果是引用,

     返回引用地址指向堆空间对象

    如果是常量,

     直接返回常量池常量  

  如果不存在,

   将当前对象引用复制到常量池,并且返回的是当前对象的引用

结合 2 分析


String a1 = "AA";
System.out.println(a1 == a1.intern()); //true

String a2 = new String("B") + new String("B");
a2.intern();
String a3 = new String("B") + new String("B");
System.out.println(a2 == a3.intern());//true 
System.out.println(a3 == a3.intern());//false


String a4 = new String("C") + new String("C");
System.out.println(a4 == a4.intern()); //true

2.2、创建字符串分析

结1、只在常量池上创建常量

    String a1 = "AA";

结2、只在堆上创建对象

String a2 = new String("A") + new String("A");

结3、在堆上创建对象,在常量池上创建常量

    String a3 = new String("AA");

结4、在堆上创建对象,在常量池上创建引用

String a4 = new String("A") + new String("A");//只在堆上创建对象AA
a4.intern();//将该对象AA的引用保存到常量池上

2.2.1、创建字符串分析开始

2.1.1、直接使用双引号" "创建字符串

判断这个常量是否存在于常量池,

  如果存在,

   判断这个常量是存在的引用还是常量

     如果是引用,返回引用地址指向的堆空间对象,  

    如果是常量,则直接返回常量池常量,

  如果不存在,

    在常量池中创建该常量,并返回此常量

String a1 = "AA";//在常量池上创建常量AA
String a2 = "AA";//直接返回已经存在的常量AA
System.out.println(a1 == a2); //true
String a3 = new String("AA");    //在堆上创建对象AA,在常量池创建对象AA
a3.intern(); //发现在常量池已经有了字符串AA,所以不做操作,返回字符串AA的地址
String a4 = "AA"; //常量池上存在字符串AA,所以使用常量池中的对象
System.out.println(a3 == a4); //false,   
2.1.2、new String创建字符串

1、首先在堆上创建对象(无论堆上是否存在相同字面量的对象

2、然后判断常量池上是否存在字符串的字面量,

  如果不存在

   在常量池上创建常量

  如果存在

   不做任何操作

String a1 = new String("AA");
String a2 = new String("AA");
System.out.println(a1 == a2); //false

//如果常量池上不存在常量AA,也不存在引用AA,则创建常量AA
String a1 = new String("AA");
System.out.println(a1 == a1.intern()); //false // a1是堆上"AA"对象的地址,a1.intern()是字符串常量池中字符串AA对象的地址,所以返回false
2.1.3、双引号相加

判断这两个常量、相加后的常量在常量池上是否存在

​   如果不存在

   则在常量池上创建相应的常量

  如果存在

   判断这个常量是存在的引用还是常量,

    如果是引用,返回引用地址指向的堆空间对象,

    如果是常量,则直接返回常量池常量,

String a1 = "AA" + "BB";//在常量池上创建常量AA、BB和AABB,并返回AABB
//常量池上存在常量AABB
String a2 = "AABB";
String a3 = "AA" + "BB";
System.out.println(a2 == a3); //true
//常量池上存在引用AABB
String a4 = new String("AA") + new String("BB"); //在堆上创建对象AA、BB和AABB,在常量池上创建常量AA和BB
a4.intern(); //常量池没有AABB,复制a4的引用到常量池,然后返回a4的引用
String a5 = "AA" + "BB";  //常量池中已经有了AABB的引用,则a5指向的是a4的引用
System.out.println(a4 == a5); //true 
2.1.4、两个new String相加

 首先会创建这两个对象以及相加后的对象

 然后判断常量池中是否存在这两个对象的字面量常量

  如果存在

   不做任何操作

  如果不存在

   则在常量池上创建对应常

//常量AA不存在,所以第一步在常量池中创建了常量AA
String a2 = new String("AA") + new String("BB");
String a3 = new String("A") + new String("A"); //创建对象AA,
System.out.println(a3 == a3.intern()); //false  //AA已经在常量池存在了,所以a3.intern() 直接返回字符串AA的地址,然后和引用a3比较,肯定不相等
//只在堆上创建AABB对象,没有在常量池中创建常量AABB
String a2 = new String("AA") + new String("BB");
System.out.println(a2 == a2.intern()); //true 

2.2.2、String创建字符串的问题

String s1 = "a";
String s2 = "a" + "b";
System.out.println(s2 == "ab");//true

String s3 = s1 + "b";
System.out.println(s3 == "ab");//false

2.2.2.1、理解说明
String s = new String("a");

这相当于在java堆内存中创建一个对象

String s = "a" + "b";

java的编译器会为该语句进行优化,变成String s = “ab”;放入的是常量池

2.2.2.2、解答疑问

s3 = s1 + "b";由于编译器不能在编译过程中确定s1的值,所以在这一步相当于String s3 = new String(“ab”);所以s3对象是堆内存中的对象,所以与常量池中的”ab”对比,自然答案是false。

加入把s1变成一个常量,s3 == "ab" 就会为true

final String s1 = "a";
String s2 = "a" + "b";
String s3 = s1 + "b";

System.out.println(s2 == "ab");//true
System.out.println(s3 == "ab");//true

2.2.3、String.intern()问题

2.2.3.1、jdk1.6 :复制的是字符串
String s1 = new String("aaa")+new String("bbb");
s1.intern();
String s2 = "aaabbbb";
System.out.println(s1==s2); //false 引用和字符串比较
2.2.4.2、jdk1.7 :复制的是引用
String s1 = new String("aaa")+new String("bbb");
s1.intern();
String s2 = "aaabbb";
System.out.println(s1==s2); //true 引用和引用比较 
String s1 = new String("aaa")+new String("bbb");
String s2 = "aabbcc";
s1.intern(); //已经存在了字符串,不会受到影响了
System.out.println(s1==s2); //false 

3、包装类:Integer内存分配

3.1、两个 new Integer() 变量比较 ,永远是 false

生成的是堆内存对象,引用地址不相等 ,比较为false

@Test
public void test1() {
    Integer i = new Integer(100);
    Integer j = new Integer(100);
    System.out.print(i == j);  //false
}

3.2、Integer变量 和 new Integer() 变量比较 ,永远为 false

而 new Integer() 的变量指向 堆中 新建的对象

Integer j = 100; -128到127,常量池,之外的也是堆中,但是和new 的不一样

 public void test2() {
        Integer i = new Integer(100);
        Integer j = 100;
        System.out.print(i == j);  //false 常量池和堆中的对象比较

        Integer i2 = new Integer(128);
        Integer j2 = 128;
        System.out.println(i2 == j2); //堆中建立的不同对象的比较
    }

3.3、 2、两个Integer 变量比较,-128到127 之间为true

java对于-128到127之间的数,会进行缓存,在常量池存储。

public void test3() {
    Integer i = 100;
    Integer j = 100;
    System.out.println(i == j); //true

    Integer i2 = 128;
    Integer j2 = 128;
    System.out.println(i2 == j2); //false
}

4.3、4、int 变量 与 Integer、 new Integer() 比较时,只要两个的值是相等,则为true

包装类Integer 和 基本数据类型int 比较时,java会自动拆包装为int ,然后进行比较,实际上就变为两个int变量的比较。

int是基本数据类型,不能在对中创建数据,所以堆中的Integer拆箱(xx.intValue)为int类型的数据,然后进行比较int和integer(无论new否)比,都为true,因为会把Integer自动拆箱为int再去比,也就是栈内存中的拆箱后进行比较

@Test
public void test4() {
    Integer i = new Integer(100);
    int j = 100;
    System.out.print(i == j); //true
}

ContactAuthor