JVM学习思考

孤人 提交于 2020-12-11 13:35:37

  毕业以来技术上一直没有太大进步,仔细一想可能是没有做技术分享,我喜欢把学习总结记录在印象笔记中,那么理解的是对是错也就没人能评判一下。为了技术进步,接下来将陆续把一些学习总结迁移到博客园中,欢迎大家多多指正!

JVM的定义

Jvm

Java虚拟机。一次编译,到处运行的前提

Jre

JVM+核心类库

Jdk

JVM+核心类库+扩展类库

JMM

Java内存模型。主要用于多线程共享数据

子模块

自动内存管理(分配、回收内存)、虚拟机执行子系统(类加载机制、虚拟机字节码执行引擎)

 

JVM运行时数据区

 

JDK8以后:

(图摘自java3y)

程序计数器:

当前线程所执行的字节码的行号指示器。

Java虚拟机栈:

Java方法执行的内存模型:每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等。

本地方法栈:

为虚拟机执行的native方法服务。

Java堆:

存放对象实例。

常量池:常量池记录的是代码出现过的常量、类名、成员变量等以及符号引用(类引用、方法引用,成员变量引用等)。

方法区(元空间):

存储已被虚拟机加载的类元数据信息。

 

内存溢出:

内存不够用。OutOfMemoryError: Java heap space、StackOverFlowError、OutOfMemoryError: Metaspace

内存泄漏:

无用内存未及时回收。最终可能导致内存溢出。

 

示例:

1、各种内存溢出。2、创建String对象时的内存分配

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * jvm内存溢出示例
 * -Xms 堆初始值
 * -Xmx 堆最大值
 * -Xmn 新生代大小
 * -Xss 每个线程的栈大小
 * -server -Xmx20m -Xms20m -Xmn10m -Xss1m -XX:+HeapDumpOnOutOfMemoryError
 * -server -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails  -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
 */
public class MemErrorDemo {
    int depth = 0;

    /**
     * 内存溢出错误(OOM)
     */
    public void OOMError() {
        List<byte[]> list = new ArrayList<>();
        int i = 0;
        while (true) {
            list.add(new byte[5 * 1024 * 1024]);
            System.out.println("loop count: " + (++i));
        }
    }

    /**
     * 栈溢出错误(StackOverFlowError)
     */
    public void SOFError() {
        try {
            depth++;
            SOFError();
        } finally {
            System.out.println("递归count: " + depth);
        }
    }

    /**
     * 元空间错误
     * 使用cglib生成动态代理类
     */
    public void MetaSpaceError() {
        int i = 0;
        try {
            while (true) {
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                        return proxy.invokeSuper(obj, args);
                    }
                });
                enhancer.create();
            }
        } catch (Exception e) {
            System.out.println("generate class count" + i);
            e.printStackTrace();
        }
    }

    static class OOMObject {

    }

    public static void main(String args[]) {
        MemErrorDemo memErrorDemo = new MemErrorDemo();
        //memErrorDemo.OOMError();
        //memErrorDemo.SOFError();
        memErrorDemo.MetaSpaceError();
    }
}
/**
 * 字符串对象创建时内存分配
 * 字符串常量池(内容在编译期确定)、堆
 * 编译期、运行期
 */
public class StringObjDemo {
    public static void main(String args[]) {
        String a = "he";
        String b = "llo";

        String c = "hello";
        String d = "he" + "llo";
        String e = new String("hello");
        String f = a + "llo";
        String g = a + b;
        String h = e.intern();

        System.out.println(c == d);
        System.out.println(c == e);
        System.out.println(c == f);
        System.out.println(c == g);
        System.out.println(f == g);
        System.out.println(c == h);
    }
}

 

JVM垃圾回收

回收的内容和区域

内容:废弃的对象、常量和无用的类;区域:堆、元空间

 

判断对象是否生存

引用记数法:

难解决两个或多个对象的循环引用状况。

可达性分析算法:

可作为GCRoots的对象包括:

1、 虚拟机栈中(栈帧中本地变量表)中引用的对象。

2、 元空间中静态属性引用的对象

3、 常量池中常量引用的对象

4、 本地方法栈中native方法引用的对象。

 

强引用:平时使用最多的的引用,若对象有强引用,并且从GCRoots到其可达(可达性分析),则不会被GC回收。

软引用:在堆内存未发生溢出时不会回收有软引用的对象,在内存溢出将要发生前,先对软引用关联的对象进行回收,回收后仍溢出,则抛OOM异常。

弱引用:对象只有弱引用,会在下一次GC进行回收时被回收

虚引用:不能通过虚引用获取对象实例,不影响对象生存时间,唯一作用是在对象回收时收到一个系统通知

 

垃圾收集算法

标记-清除算法:标记出需要回收的对象,然后清楚有标记的对象。

标记-整理算法:标记出需要回收的对象,然后移动到一端,直接清理边界外的内存。

复制算法:将内存容量分两部分,保持在某一时刻始终有一部分是空的,将仍然存活的对象复制到此区域。

分代收集算法:

新生代:复制算法。朝生夕死,对象存活率低,将区域分3份,eden:survivor0:survivor1=8:1:1

老年代:标记-清楚/标记-整理算法。

 

回收事件:

新生代回收事件:minor gc

老年代回收事件:major gc

全部回收(包括MetaSpace):full gc(Stop the world),默认堆空间使用到达80%(可调整)的时候会触发fgc

 

内存分配与回收策略

1、 对象优先在Eden分配

2、 大对象直接进入老年代

3、 长期存活的对象进入老年代。每熬过一次minor gc年龄增加1岁,默认15岁进入老年代。

4、 动态对象年龄判断。Survivor空间中相同年龄所有对象大小的和大于survivor空间的一半,大于或等于该年龄的对象直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

5、 空间分配担保。新生代进行复制回收时,survivor空间不够用,survivor无法容纳的对象进入老年代。

 

垃圾收集器

 

JVM类加载机制

类加载时机

加载、验证、准备、初始化和卸载顺序确定,解析不一定,有时会在初始化之后——java运行时绑定(动态绑定/晚期绑定)。

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

初始化的5种情况,如果类未初始化则立刻初始化:

1、 遇到new、getstatic、putstatic、或invokestatic4条字节码指令。这4条指令常见场景:使用new关键字实例化对象、读取一个类的静态字段(被final修饰、已在编译期将结果放入常量池的静态字段除外)、调用一个类的静态方法。

2、 使用java.lang.reflect包的方法对类反射调用。

3、 初始化一个类的时候,如果父类未初始化,则优先初始化父类(接口除外,只在使用时初始化)。

4、 虚拟机启动时,用户需要指定一个要执行的主类(main方法所在类),虚拟机会先初始化这个主类。

5、 使用动态语言时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,其所对应的类未初始化,则先触发其初始化。

这5类是对一个类进行主动引用,除此之外所有引用类的方式都不会触发初始化,称为被动引用。

示例:类的被动引用

 

/**
 * 类加载阶段,被动引用示例
 * 1、通过子类引用父类静态字段,不会导致子类初始化
 * 2、通过数组定义引用类不会触发此类的初始化。
 *    数组本身不通过类加载器创建,由java虚拟机直接创建。
 * 3、常量在编译期存入调用类的常量池,本质上没有直接引用到定义常量的类
 */
public class ClassLoadingDemo {
    public static void main(String[] args) {
        //System.out.println(SubClass.h);
        //SubClass[] scs = new SubClass[10];
        System.out.println(SubClass.w);
    }
}

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

    public static String h = "hello";
    public static final String w = "world";
}

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

 

类加载器

即使两个类来自同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类必定不相等。

双亲委派模型

 

对类的加载请求,优先让父加载器加载,只有父加载器反馈无法加载时,子加载器才会尝试加载。

类加载器之间的父子关系并不是通过继承来实现的,而是通过组合关系。

 

JVM字节码执行引擎

运行时帧栈结构——动态连接

Class文件的常量池中存在大量的符号引用,这些符号引用一部分会在类加载阶段或者第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,称为动态连接。

方法调用

目的:唯一任务是确定方法调用的版本即调用哪一个方法。

理解概念:静态类型、实际类型

解析调用:

静态过程,在编译阶段就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转化为直接引用,不会延迟到运行期。

在解析阶段确定唯一的版本,此类方法包括:静态方法、私有方法、实例构造器、父类方法4类。

 

分派:

理解概念:多态

方法的宗量:方法的接收者与方法的参数

静态分派通过静态类型确定方法的执行版本,发生在编译阶段。典型应用:方法的重载

示例:方法的重载

/**
 * 静态分派示例
 * 根据静态类型确定方法版本
 */
public class MethodOverLoadDemo {
    public static class SuperClass {
    }

    public static class Sub1 extends SuperClass {
    }

    public static class Sub2 extends SuperClass {
    }

    public void sayHello(SuperClass s) {
        System.out.println("hello super");
    }

    public void sayHello(Sub1 s) {
        System.out.println("hello sub1");
    }

    public void sayHello(Sub2 s) {
        System.out.println("hello sub2");
    }

    public static void main(String[] args) {
        SuperClass s1 = new Sub1();
        SuperClass s2 = new Sub2();
        MethodOverLoadDemo ml= new MethodOverLoadDemo();
        ml.sayHello(s1);
        ml.sayHello(s2);
    }
}

动态分派

Java语言是一门静态多分派,动态单分派的语言。静态分派时通过方法接受者即静态类型、方法参数两个宗量确定方法版本,动态分派时则在已有版本(静态分派时确定)中只通过方法接收者即实际类型确定最终方法版本。

典型应用:方法的重写

示例:方法的重写

/**
 * 动态分派示例
 * 方法重写-jvm选择方法版本:
 * 1、在编译期间根据静态类型选择一个方法版本
 * 2、在运行期间根据实际类型和编译期间已选的版本选择最终版本
 */
public class MethodOverrideDemo {

    public static class O {
        public void m1(O o) {
            System.out.println("O-m1");
        }
    }

    public static class A extends O {
        public void m1(A a) {
            System.out.println("A-m1");
        }
    }

    public static class B extends A {
        @Override
        public void m1(A a) {
            System.out.println("B-m1");
        }

        public void m1(B b) {
            System.out.println("B-m2");
        }

        public void m1(O b) {
            System.out.println("B-m3");
        }
    }


    public static void main(String[] args) {
        A a = new B();
        a.m1(a);

        B b = new B();
        a.m1(b);

        b.m1(b);
    }
}

 

 

参考资料:

《深入理解Java虚拟机——JVM高级特性与最佳实践》 周志明 著

Java3y: https://www.cnblogs.com/Java3y/p/9296496.html

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!