从头捋捋jvm(-java虚拟机)

@

jvm 是Java Virtual Machine(Java虚拟机)的缩写,java 虚拟机作为一种跨平台的软件是作用于操作系统之上的,那么认识并了解它的底层运行逻辑对于java开发人员来说很有必要!

让我们来看看它一次编译,到处运行的牛叉之处!
废话不多说,先看看jvm的架构图(无论何时脑子里要有这样一张图):

总概
从这副架构图可以看出jvm由类装载器、运行时数据区、执行引擎、本地方法接口还有垃圾回收器构成;其中垃圾回收器作用在整个jvm内存中(主要作用在堆和方法区),所以图中没有具体体现,但是我们要知道这么个东西,后面会聊jvm内存调优主要是在调堆

接下来,我们再结合jvm架构图一个个分析下:

类加载器

了解类加载器就是了解代码编译执行的机制

1.加载:查找并加载类的二进制数据

2.连接:

  • 验证:保证被加载的类的正确性;

  • 准备:给类静态变量分配内存空间,赋值一个默认的初始值;

  • 解析:把类中的符号引用转换为直接引用

    把java编译为class文件的时候,虚拟机并不知道所引用的地址;助记符:符号引用转为真正的直接引用,找到对应的直接地址!

3.初始化:给类的静态变量赋值正确的值;

来看下这行代码的执行顺序:

package jvm;

public class TestClassLoader {
    public static int a = 1;
    // 1、加载  编译TestClassLoader文件为 .class 文件,通过类加载,加载到JVM

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

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

了解了这个加载执行顺序后,我们再来看看下面这段代码,请你说说程序是如何输出的?

Demo1代码:

package jvm;
/** 
*JVM 参数:
*-XX:+TraceClassLoading // 用于追踪类的加载信息并打印出来
*分析项目启动为什么这么慢,快速定位自己的类有没有被加载!
*rt.jar jdk 出厂自带的,最高级别的类加载器要加载的!
*/ 
public class Demo1 {
    public static void main(String[] args) {
        System.out.println(Children1.str2);
        Children1.sayHello();
        // 输出:
        //      Parent1 static
        //      Children1 static
        //      hello,str2
        //      hello,str1
    }
}
class Parent1{
    public static String str1 = "hello,str1";
    public static void sayHello(){
        System.out.println(str1);
    }

    static {
        System.out.println("Parent1 static");
    }
}
class Children1 extends Parent1{
    public static String str2 = "hello,str2";
    public static void sayHello(){
        System.out.println(str1);
    }
    
    static {
        System.out.println("Children1 static");
    }
}

可以看出,子类继承父类 会优先加载父类的 然后静态块是先于方法和静态变量的.那么常量又是怎样的呢?

Demo2代码:

package jvm;

public class Demo2 {
    public static void main(String[] args) {
        System.out.println(Parent2.STR2);
        // 输出:
        // hello static final
    }
}
class Parent2{
    public static final String STR2 = "hello static final";
    static {
        System.out.println("Parent2 static"); // 这句话会输出吗?
    }
}

Demo3代码:

package jvm;

import java.util.UUID;
public class Demo3 {
    public static void main(String[] args) {
        System.out.println(Parent3.STR3);
        // 输出: Parent3 static
        //       ee33c452-70e2-47a6-946a-99261819a49d
    }
}
class Parent3{
    public static final String STR3 = UUID.randomUUID().toString();
    static {
        System.out.println("Parent3 static"); // 这句话会输出吗?
    }
}

理解:根据上面的jvm架构图可以知道类信息,常量,静态变量,编译后运行代码都是存在方法区里的,而常量是存在方法区的常量池中的。

而对于编译期可以“确定值”的常量:例如Demo2中STR2 是存放在Demo2类调用者所在的常量池中的,当STR2放到常量池后Demo2与Parent2类的关系就没有了!

对于编译期“不确定值”的常量:例如Demo3中STR3 是不存在Demo3的调用者的常量池中的,在程序运行期间会主动使用Demo3所在的类,会加载其静态块!

上面的程序Parent2.STR2输出后就退出了Parent2类,因为程序加载Parent2类是直接调用了常量池中关于STR2的引用,无需再加载类的其他信息包括静态块,说白了按我的理解就是常量池和类的其他信息不在一个内存区,程序根据指令扫描的时候无需扫描不相关的内存区!说到池(池?嗯,等等是不是和线程池和队列有关,哈哈。学东西要养成发散思维的习惯,这样就能串起来很多知识点,渐渐形成自己的知识面。)接下来就来扩展一下:

package jvm;
/**
 * 思考:
 * String == 比较的是什么?基本类型和包装类型==比较时会自动拆箱!
 * 八大基本类型哪些实现了池化技术
 */
public class Exclude {
    public static void main(String[] args) {
        String java = "java";
        String java1 = new String("java");
        String java2 = new String("java");
        System.out.println(java == java1);//输出false 
        System.out.println(java1 == java2);//输出false
        // 注⚠️:String用==比较的时候不仅比较的是对象的值还比较的是对象值的引用
        // equals

	Float f = 0.1f;
        Float f1 = 0.1f;
        System.out.println(f == f1);//输出false
        
        int i = 128;
        int i1 = 128;
        System.out.println(i == i1);//输出true
        
	// 1)基本类型和包装类型 比较时包装类自动拆箱变成基本类型
	int a = 100;
	Integer b = 100;
	System.out.println(a == b);//输出true

	// 2)两个包装类型 -128 到 127
	Integer c = 100;
	Integer d = 100;
	System.out.println(c == d);//输出true

	c = 128;
        d = 128;
        System.out.println(integer == integer1);//输出false
    }
}

注⚠️:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean
另外两种浮点数类型的包装类Double Float则没有实现。
另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用常量池,即对象不负责创建和管理大于127的这些类的对象

关于[基本类型]和[引用类型]的区别推荐博客:说说基本类型和包装类型的区别

类加载器 分类

1、java虚拟机自带的加载器

  • BootStrap 根加载器 (加载系统的包,JDK 核心库中的类 rt.jar)
  • Ext 扩展类加载器 (加载一些扩展jar包中的类)
  • Sys/App 系统(应用类)加载器 (我们自己编写的类)

2、用户自己定义的加载器

  • ClassLoader,只需要继承这个抽象类即可,自定义自己的类加载器

Demo4代码:

package jvm;

public class Demo4 {
    public static void main(String[] args) {
        Object o = new Object(); // jdk 自带的
        Demo demo = new Demo();  // 实例化一个自己定义的对象

        // null 在这里并不代表没有,只是Java触及不到!
        System.out.println(o.getClass().getClassLoader()); // null
        System.out.println(demo.getClass().getClassLoader()); // AppClassLoader
        System.out.println(demo.getClass().getClassLoader().getParent()); // ExtClassLoader
        System.out.println(demo.getClass().getClassLoader().getParent().getParent()); // null
        
        // jvm 中有机制可以保护自己的安全;
        // 双亲委派机制 : 一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推
        // ClassLoader         04
        // AppClassLoader      03
        // ExtClassLoader      02
        // BootStrap (最顶层)   01  java.lang.String  rt.jar

        // 双亲委派机制 可以保护java的核心类不会被自己定义的类所替代
    }
}
class Demo{

}

本地接口库

Native方法 JNI : Java Native Interface (Java 本地方法接口)

大家都知道java是从C过度过来的,C可以直接操作硬件
思考一个问题:线程是基于内存的,那么应该是操作硬件 让计算机cpu来划分时间片来“同时”执行多个任务,那么程序是怎么怎么达到底层的呢?java 能直接操作硬件吗?我们来看看线程是怎么启动的,看源码(面向源码编程)

 public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();  //启动 调用下面的native修饰的启动方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

native : 只要是带了这个关键字的,说明 java的作用范围达不到,只能去调用底层 C 语言的库!

这部分知识作为java开发只需要了解即可,如果想去研究也可以看看嵌入式C的相关东西(C语言->编译器—>汇编语言—>驱动->硬件)。

运行时数据区

程序计数器

每个线程都有一个程序计数器,是线程私有的。

程序计数器就是一块十分小的内存空间;几乎可以不计

作用: 看做当前字节码执行的行号指示器

分支、循环、跳转、异常处理!都需要依赖于程序计数器来完成!

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

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

iadd

imul

方法区

Method Area 方法区 是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享!

JDK1.7之前

永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等等,这些东西都会放到永久代中;

永久代大小空间是有限的:如果满了 OutOfMemoryError:PermGen

JDK1.8之后

彻底将永久代移除 HotSpot jvm ,Java Heap 中或者 Metaspcace(Native Heap)元空间;

元空间就是方法区在 HotSpot jvm 的实现;

方法区主要是存:类信息,常量,字符串、静态变量、符号引用、方法代码。

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

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

设置元空间大小:-XX:MetasapceSize10m

堆和栈

堆和栈都是内存中相对独立的一块区域,jvm内存分为5个区域,分别是寄存器、本地方法区、方法区、栈内存、堆内存!

注⚠️:每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器,我们习惯上称呼为程序计数器,并不是什么其他的东西!

栈和堆的区别

  • 1.堆是不连续的,所以分配的内存是在运行期确认的,因此大小不固定;堆对于整个应用程序都是共享、可见的;堆内存存储的是实体;堆内存存放的实体会被垃圾回收机制不定时的回收

  • 2.栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的;栈只对于线程是可见的,所以也是线程私有,他的生命周期和线程相同(所以说,栈里面是一定不会存在垃圾回收的问题);栈内存存储一些基本类型的值,对象的引用,方法等;栈内存的更新速度要快于堆内存(仅次于寄存器),因为局部变量的生命周期很短;栈内存存放的变量生命周期一旦结束就会被释放。

面试题:java中的基本数据类型都是存储在栈中的吗?

面试基本上问到这里都是三连,说说八大基本数据类型?说说堆和栈?接下来就是这个有坑的问题

答:不是

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

在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。

(1)当声明是基本类型的对象时,其变量及变量的引用都在栈中。

(2)当声明的是引用类型(new出来的)时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆内存中的。

二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。

同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量

(1)当为基本类型时,其变量名及其值放在堆内存中的

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

扩展-->请根据以下代码说说6个对象的在堆栈中是如何分配的?

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

结合下面的图来理解String 常量存放的位置:

题外话:JVM目前有 SUN公司 HotSpot、BEA公司 JRockit、IBM公司 J9VM
关于堆在垃圾回收器里面介绍!

执行引擎

类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),然后执行引擎执行会执行这些字节码。
字节码必须通过类加载过程加载到JVM后才能够被执行,执行有三种模式:

  • 1.解释执行
  • 2.JIT(Just-In-Time)编译即执行
  • 3.JIT与解释混合执行
    来看下执行引擎的简要执行流程:

关于执行引擎这种底层的东西了解即可,说白了就是执行编译后字节码的一套概念模型!执行引擎包含了众多可以操作字节码的执行指令,使程序能够按照一定的顺序执行!关于执行引擎(推荐知乎层层剥开JVM——字节码执行引擎,亦可以推荐孤尽老师《码出高效》一书中的第四章“走进JVM”来深入了解)

垃圾回收器

呼,长出了一口气!终于写到和堆相关的了
看堆之前我们先了解下JVM的内存布局:

物理上只有 新生、养老;元空间在本地内存中,不在JVM中!

GC 垃圾回收主要是在 新生区和养老区,又分为 普通的GC 和 Full GC,如果堆满了,就会爆出 OutOfMemory(OOM);

新生区

新生区 就是一个类诞生、成长、消亡的地方!

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

Sun HotSpot 虚拟机中,内存管理(分代管理机制:不同的区域使用不同的算法!)

Eden from to

99% 的对象在 Eden 都是临时对象;

养老区

15次都幸存下来的对象进入养老区,养老区满了之后,触发 Full GC

默认是15次,可以修改!

永久区(Perm)

放一些 JDK 自身携带的 Class、Interface的元数据;

几乎不会被垃圾回收的;

OutOfMemoryError:PermGen 在项目启动的时候永久代不够用了?加载大量的第三方包!

JDK1.6之前: 有永久代、常量池在方法区;

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

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

闲聊:方法区和堆一样,是共享的区域,是JVM 规范中的一个逻辑的部分,但是记住它的别名 非堆

jvm内存调优

来看以下代码:

package jvm;

/**
 * 环境:jdk1.8
 * jvm:HotSpot
 *
 *  默认情况:
 *  maxMemory : 1808.0MB (虚拟机试图使用的最大的内存量  一般是物理内存的 1/4)
 *  totalMemory : 123.0MB (虚拟机试图默认的内存总量 一般是物理内存的 1/64)
 *
 *  自定义: -XX:+PrintGCDetails; // 输出详细的垃圾回收信息
 *          -Xmx: 最大分配内存; 1/4
 *          -Xms: 初始分配的内存大小; 1/64
 *          -Xmx1024m -Xms1024m -XX:+PrintGCDetails
 */
public class HeapDemo {
    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");
    }
}

输出信息如下:
heapSpace
由计算得出(305664K+699392K)/1024 = 981.5MB
说明新生代和老年代内存相加为整个分配内存大小,也从侧面证明了元空间不存在于jvm中,它存在于本地空间中!

--认识了堆后,我们再来聊聊堆的内存调优,这是每个java开发都需要掌握的!
模拟oom,看看GC详情:

package jvm;

import java.util.Random;
/**
 * 设置内存大小:-Xmx8m -Xms8m -XX:+PrintGCDetails
 */
public class HeapDemo1 {
    public static void main(String[] args) {
        System.gc(); // 手动唤醒GC(),等待cpu的调用
        String str = "talk is cheap,show me your code";
        while (true){
            str += str
                    + new Random().nextInt(999999999)
                    + new Random().nextInt(999999999);
        }
        // 出现问题:java.lang.OutOfMemoryError: Java heap space
    }
}

输出如下:

"C:\Program Files\Java\jdk1.8.0_171\bin\java.exe" -Xmx8m -Xms8m -XX:+PrintGCDetails "-javaagent:D:\idea\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=63475:D:\idea\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_171\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\rt.jar;E:\sCloud\studyDemo\out\production\studyDemo" jvm.HeapDemo1
[GC (Allocation Failure) [PSYoungGen: 1536K->504K(2048K)] 1536K->632K(7680K), 0.0022005 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 877K->488K(2048K)] 1005K->712K(7680K), 0.0017541 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2048K)] [ParOldGen: 224K->645K(5632K)] 712K->645K(7680K), [Metaspace: 3441K->3441K(1056768K)], 0.0181221 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 1249K->325K(2048K)] 1894K->971K(7680K), 0.0010536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1531K->259K(2048K)] 2960K->1689K(7680K), 0.0009587 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1532K->487K(2048K)] 2961K->2165K(7680K), 0.0117368 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 1688K->128K(2048K)] 6500K->4940K(7680K), 0.1107265 secs] [Times: user=0.02 sys=0.00, real=0.11 secs] 
[GC (Allocation Failure) [PSYoungGen: 128K->128K(2048K)] 4940K->4940K(7680K), 0.0211234 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 128K->0K(2048K)] [ParOldGen: 4812K->2550K(5632K)] 4940K->2550K(7680K), [Metaspace: 3939K->3939K(1056768K)], 0.0311921 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[GC (Allocation Failure) [PSYoungGen: 138K->32K(2048K)] 4256K->4149K(7680K), 0.0064069 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->32K(2048K)] 4149K->4149K(7680K), 0.0007456 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(2048K)] [ParOldGen: 4117K->3332K(5632K)] 4149K->3332K(7680K), [Metaspace: 3942K->3942K(1056768K)], 0.0250859 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 3332K->3332K(7680K), 0.0014623 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 3332K->3277K(5632K)] 3332K->3277K(7680K), [Metaspace: 3942K->3942K(1056768K)], 0.0241555 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
Heap
 PSYoungGen      total 2048K, used 42K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 1536K, 2% used [0x00000000ffd80000,0x00000000ffd8a978,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 5632K, used 3277K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000)
  object space 5632K, 58% used [0x00000000ff800000,0x00000000ffb337e8,0x00000000ffd80000)
 Metaspace       used 3974K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 437K, capacity 460K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at jvm.HeapDemo1.main(HeapDemo1.java:11)

Process finished with exit code 1
// 分析:GC 普通GC(轻GC)、Full GC 重GC
// 1536K 执行 GC之前的大小,504K  执行 GC之后的大小
// (2048K) young 的total大小 ,(5632K)old 的total 大小 (7680K)整个堆已被占用的大小
// 0.0241555 secs 清理的时间、user 总计GC所占用CPU的时间   
// sys OS调用等待的时间   real 应用暂停的时间

好了,理解这些参数后基本知道怎么调优了。
其实Idea 还有一款插件JProfiler 专门用来分析程序内存占用情况的,用JProfiler 可以dump 内存快照帮助我们快速定位问题。教程百度上有,此处不做说明!

注⚠️:JVM在进行GC时,并非每次都是对三个区域进行扫描的!大部分的时候都是指的新生代!

  • 普通GC:只针对新生代 【GC】
  • 全局GC:主要是针对老年代,偶尔伴随新生代! 【Full GC】

GC四大算法:

引用计数法

特点:每个对象都有一个引用计数器,每当对象被引用一次,计数器就+1,如果引用失效,则计数器-1,如果为0,则GC可以清理;

缺点:

  • 计数器维护麻烦!
  • 循环引用无法处理!

注⚠️:JVM 一般不采用这种方式,现在一般使用可达性算法,GC Root...

复制算法

1、一般普通GC 之后,差不多Eden几乎都是空的了!

2、每次存活的对象,都会被从 from 区和 Eden区等复制到 to区,from 和 to 会发生一次交换;记住一个点就好,谁空谁是to,每当幸存一次,就会导致这个对象的年龄+1;如果这个年龄值大于15(默认值,后面我们会讲解调整),就会进入养老区;

优点:没有标记和清除的过程!效率高!没有内存碎片!

缺点:需要浪费双倍的空间

Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!推荐使用 复制算法

标记清除算法

老年代一般使用这个,但是会和我们后面的整理压缩一起使用!

优点:不需要额外的空间!

缺点:两次扫描,耗时较为严重,会产生内存碎片,不连续!

标记清除压缩

减少了上面标记清除的缺点:没有内存碎片!但是耗时可能也较为严重!

那我们什么时候可以考虑使用这个算法呢?

在我们这个要使用算法的空间中,假设这个空间中很少,不经常发生GC,那么可以考虑使用这个算法!

总结:
内存效率:复制算法 > 标记清除算法 > 标记整理(时间复杂度!)
内存整齐度:复制算法=标记整理>标记清除算法
内存利用率:标记整理 = 标记清除算法 > 复制算法

面试题:

最后再来看看几个面试题:

1.JVM 垃圾回收的时候如何确定垃圾,GC Roots?

  • 引用计数法 (每引用一次就加一,为0或-1就会被GC清除)
  • 可达性分析算法 (如果从 GC Root 这个对象开始,对于没有关联的对象GC认为不可达的,就会被清理)

2.什么是GC Root?

  • 1、虚拟机栈中引用的对象!
  • 2、类中静态属性引用的对象
  • 3、方法区中的常量
  • 4、本地方法栈中 Native 方法引用的对象!

3.jvm 常用参数有哪些,你都用过哪些?

  • 1、标配参数:-version,-help,-showversion

  • 2、X参数:-Xint # 解释执行,-Xcomp # 第一次使用就编译成本地的代码,-Xmixed # 混合模式(Java默认)

  • 3、XX参数:+ 或者 - 某个属性值, + 代表开启某个功能,- 表示关闭了某个功能 例如:jps -l ,jinfo -flag PrintGCDetail 进程号 # 查看某个运行中的java 程序,-XX:+PrintGCDetails # 打印GC详情

  • 4.XX 参数之key = value值;例如:元空间大小:-XX:MetaspaceSize=128m#设置元空间大小,-XX:MaxTenuringThreshold=15#设置新生代需要经历多少次GC晋升到老年代中的最大阈值(默认值是15,最大也是15)

  • 1).-Xms 初始堆的大小,等价: -XX:InitialHeapSize

  • 2).-Xmx 最大堆的大小 ,等价:-XX:MaxHeapSize

-XX:+PrintFlagsInitial 查看 java 环境初始默认值;这里面只要显示的值,我们都可以手动赋值,不建议修改,了解即可!

= 默认值

:= 就是被修改过的值

java -XX:+PrintCommandLineFlags -version 打印出用户手动选项的 XX 选项

4.你常用的项目,发布后配置过JVM 调优参数吗?

-Xms
-Xmx
-Xss  : 线程栈大小设置,默认 512k~1024k
-Xmn  设置年轻代的大小,一般不用动!
-XX:MetaspsaceSize  设置元空间的大小,这个在本地内存中!
-XX:+PrintGCDetails
-XX:SurvivorRatio
设置新生代中的 s0/s1 空间的比例;
`uintx SurvivorRatio  = 8`  Eden:s0:s1 = 8:1:1
`uintx SurvivorRatio  = 4`  Eden:s0:s1 = 4:1:1
-XX:NewRatio
设置年轻代与老年代的占比:
`NewRatio  = 2`   新生代1,老年代是2,默认新生代整个堆的 1/3;
`NewRatio  = 4`   新生代1,老年代+是4,默认新生代整个堆的 1/5;
-XX:MaxTenuringThreshold
进入老年区的存活阈值;
`MaxTenuringThreshold  = 15`

小结:至此JVM的知识,基本已总结完成,推荐书《码出高效》;买了很多书但是一直没看,个人博客新增了栏目“拆书” 有时间会把买的书啃完了写写心得体会,或者看到精彩的地方会分享出来,当然不至于技术一类。为能看到这儿的自己点个赞吧,加油!

posted @ 2020-04-10 09:47  叁有三分之一  阅读(934)  评论(2编辑  收藏  举报