Java虚拟机1

Java内存区域

这里写图片描述

  • 程序计数器(Program Counter Register):记录当前线程所执行字节码的行号指示器。字节码解释器工作时,判断是循环,分支,跳转,异常等条件,然后更新这个计数器的值来选取下一条要执行的指令。

    • 这个部分是线程私有的,各线程之间不会相互影响
  • Java虚拟机栈(JVM Stacks):是Java方法执行的内存模型,每个方法在执行中会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。如果该方法中调用了别的方法,相当于新方法入栈;而方法执行完成后,相当于该方法出栈。

    • 这个部分也是线程私有的

    • 局部变量表存放原始类型和引用对象的指针。所需的内存空间在编译时就确定了。

  • 本地方法栈(Native Method Stack):与Java虚拟机栈类似,不过是存放Native方法的栈帧。

  • Java堆(Java Heap):是Java虚拟机所管理的内存中最大的一块。用于存放对象实例,几乎所有对象实例在这里分配内存。

    • Java堆是所有线程共享的一块内存区域。

    • 这里是垃圾收集器管理的主要区域,也被称为GC堆。

    • 还可以细分为新生代和老生代。见垃圾收集

  • 方法区(Method Area):用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。这块内存可以选择不实现垃圾收集,但有时也是很重要的,主要针对常量池的回收和对类型的卸载。

    • 所有线程共享的内存
  • 运行时常量池(Runtime Constant Pool):是方法区的一部分 ,用于存放编译器生成的各种字面量和符号引用。

  • 直接内存:不属于Java内存,但NIO中的Buffer和Channal,可以使用Native函数库来直接分配堆外内存。


GC算法(什么样的对象该死)

  • 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数值加1.当引用失效时,计数器减1,任何时刻的计数器为0的对象就是不可用的。python,FlashPlayer等就是这种算法。

  • 可达性分析算法:利用图论的性质。从一系列GC Roots对象为起点,向下搜索,走过的路径为引用链,当一个对象不在路径上(即对象不可达或未连通),则证明对象不可用,可以被判断为可回收。这是java,C#的判断对象是否存活的算法。

  • GC Roots包括:虚拟机栈中中引用的对象,方法区类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。


GC过程=>生存还是毁灭

堆的回收

  • 即使是可达性算法中不可达的对象,也并非一定会死,至少要经历两次标记过程才会真正死亡:

  • 如果发现对象是不可达的,标记第一次并且进行一次筛选。筛选条件是是否有必要执行finialize()方法,当对象没有覆盖finalize()方法,或者finalize()已经被调用过了,则视为没有必要执行。

  • 如果被判定有必要执行finalize()方法,那么该对象将会放置在一个F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它的finalize()。但不保证会运行结束。

  • 因此在finalize()中对象还有逃脱的机会(仅供演示,没有意义)

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes ,i am still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String...args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次自救,成功
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("i am dead");
        }

        //由于finalize()只能调用一次,所以自救失败
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("i am dead");
        }

    }

}

方法区的回收:废弃常量和无用的类

  • 例,常量池中有一个abc的字符串,但是没有任何一个String对象引用abc。有必要的话,这个常量会被清除。

  • 无用的类判断,同时满足下列条件:

    • 1.该类的所有实例被回收(Java堆中不存在该类的实例)
    • 2.加载该类的ClassLoader已经被回收
    • 3.该类对应的Class对象没有被任何地方引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法的发展(见深入理解JVM ==P69)

1.标记-清除算法

  • 首先标记出需要回收的对象,在标记完成后统一回收。

  • 有效率问题(标记和清除的效率都不高)

  • 有空间问题(内存中对象分布不均)

2.复制算法

  • 将可用内存划分为大小相等的两块,每次只使用一块。当一块用完了,就将还存活的对象复制到另外一半上面。然后对已使用的内存空间进行清理。

  • 对此的扩展:

    • 分为一块80%的Eden空间和两块10%的Survivor空间

    • 每次使用Eden和一块Survivor。

    • 当回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor上面。

3.标记-整理算法

  • 首先标记出所有可回收的对象

  • 将不可回收对象向内存首部移动

  • 然后清理掉 ,除首部存活对象之外的内存

垃圾收集器

新生代:Serial,ParNew,Parallel Scavenge

老生代:CMS,Serial Old,Parallel Old

一站式:G1

  • Serial收集器:采用复制算法,它在进行垃圾收集时,会停掉所有工作线程。Client默认

  • ParNew收集器:采用复制算法,多线程收集版本的Serial,和Serial基本一致。Server首选

  • Parallel Scavenge收集器:也是使用复制算法的多线程收集器。吞吐量优先的收集器,使得垃圾收集的占CPU运行比最小

  • Serial Old收集器:使用标记-整理算法,它在进行垃圾收集时,会停掉所有工作线程。Client默认

  • Parallel Old收集器:Parallel Scavenge收集器的老生代版本,多线程+标记-整理算法,如果新生代使用了Parallel Scavenge,可以使用Parallel Old来代替Serial Old来改善性能

  • CMS收集器(重点):并发低停顿收集器,以获取最短回收停顿为目标的收集器,多应用于互联网服务端。算法是标记-清除

    • 初始标记 => 并发标记 => 重新标记 => 并发清除

    • 初始标记,重新标记仍会Stop the World,但时间会很短

    • 时间最长的并发标记和并发清除可以与用户线程一起工作

    • 缺点:1.对CPU资源敏感。2.无法处理浮动垃圾。3.产生大量空间碎片

  • G1收集器(最前沿产品):面向服务端应用的垃圾收集器

    • 并发和并行:来缩短Stop-The-World的时间

    • 分代收集:可以使用不同的策略去收集新生代和老生代的垃圾

    • 空间整合:不会产生大量的空间碎片,有助于程序的长时间运行

    • 可预测的停顿:可以指定在M毫秒中用于垃圾收集的时间不超过N毫秒

    • G1收集器将Java堆不再视为新生代和老生代,而是分成多个大小相等的独立区域(Region)。并维护一个优先队列,每次根据允许的收集时间,优先回收价值大的区域。

GC日志理解

[GC (Allocation Failure) [PSYoungGen: 8192K->1000K(9216K)] 8192K->4441K(19456K), 0.0849028 secs]
        [Times: user=0.20 sys=0.00, real=0.09 secs] 

[Full GC (Ergonomics) [PSYoungGen: 9192K->0K(9216K)] [ParOldGen: 10232K->10214K(10240K)] 19424K->10214K(19456K), 
        [Metaspace: 3289K->3289K(1056768K)], 0.2874280 secs] 
        [Times: user=0.64 sys=0.02, real=0.28 secs]
  • 停顿类型:

    • Full GC: Stop-the-world停顿

    • GC: 并发停顿

  • 收集区域

    • DefNew: Serial收集器在新生代

    • ParNew: ParNew收集器在新生代

    • PSYoungGen: Parallel Scavenge收集器在新生代

    • DefOld:

    • ParOldGen:


内存分配和回收策略

这里写图片描述

  • Young中存放新生的对象(Eden:Survivor = 8:1)

  • Old中存放生命周期长的对象

  • Permanent中存放永久保存的,如Class和Meta信息

1.分代GC

  • 频繁收集Young

  • 较少收集Old

  • 基本不收集Perm

  • Minor GC(收集Young区域)后,会将存活对象移到Survivor区,对象过大则放到Old,所以Eden会为空

  • Full GC/Major GC:发生在老生代的GC,速度比Minor GC慢10倍以上

2.对象分配

  • 对象优先在Eden分配,当Eden空间不足,将会发起一次Minor GC。收集垃圾,并转移对象。

  • 大对象直接进入老年代,(避免短命的大对象)经常出现大对象会导致内存还有空间就提前触发GC

  • 长期存活的对象将进入老年代:虚拟机给每个对象设定一个Age计数器,每熬过一次Minor GC就Age加1。默认到15岁,会被移入老年代

  • 动态对象年龄判定: 当Survivor空间中相同年龄的对象内存总和达到一半,年龄大于等于该年龄的对象可以直接进入老年代。不必达到规定年龄

  • 空间分配担保:发生GC前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果成立,会进行Minor GC,否则查看是否有担保。


类加载器

  • 类加载的时机:加载=>验证=>准备=>解析=>初始化=>使用=>卸载

  • 有5种情况必须马上进行类的初始化:

    • 遇到new、getstatic、putstatic、invokestatic这4条字节码

    • 使用reflect包,进行反射调用的时候

    • 当初始化一个类的时候,发现其父类还没有被加载

    • 当使用JDK1.7的动态语言支持

    • 在类初始化阶段会将static final String的变量提升到掉用类的常量池中

public class ConstClass{
    static {
        System.out.print("ConstClass init");
    }
    public static final String HELLOWORLD = "hello world";
}

public class NotInitialization{
    public static void main(String...args){
        System.out.println(ConstClass.HELLOWORLD);
    }
}

//只会输出hello world
//没有初始化语句
//转化为NotInitialization对自身常量池的引用
//这两个类在编译后就没有任何联系了

  • 子类继承父类并在子类中访问父类,会初始化父类,而不初始化子类

public class SuperClass{
    static{
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}

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

==>
public class NotInitialzation{
    public static void main(String...args){
        System.out.println(Sunclass.value);
    }
}
//输出SuperClass init!
//因为对于静态字段,只有定义这个字段的类才会被初始化

==>
public class NotInitialzation{
    public static void main(String...args){
        SuperClass[] src = new SuperClass[10];
    }
}
//无输出
//不会触发SuperClass的初始化阶段
//会触发[L SuperClass的初始化阶段,这是一个虚拟机创建的一维数组类

类加载器演示

  • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同来确立其在JVM的唯一性。每一个类加载器,都拥有一个独立的类名称空间。

  • 比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一虚拟机加载,只要类加载器不同,则这两个类一定不同。

public class A {
    public void aa(){
        System.out.println("aaaaaaaaaaaaaaa");
    }

    public static void main(String...args) throws Exception {
        ClassLoader loader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null){
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                }catch (IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = loader.loadClass("aaa.A").newInstance();
        A a = (A)obj;
        a.aa();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof A);

        System.out.println(A.class.getClassLoader());
        System.out.println(new A().getClass().getClassLoader());
        System.out.println(obj.getClass().getClassLoader());
    }
}

class aaa.A
false
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$AppClassLoader@14dad5dc
aaa.A$1@1f32e575
  • obj instanceof A 一句中obj为自定义类加载器所加载的A类的对象。而A类是由系统的类加载器加载的。

  • 可见:A.class和A对象是由sun.misc.LauncherAppClassLoader@14dad5dcobjaaa.AAppClassLoader@14dad5dc加载的,而obj是由aaa.A1@1f32e575加载的。


双亲委派模型

  • 启动类加载器(Bootstrap ClassLoader): 这个类负责将存放在<JAVA_HOME>/lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,可被虚拟机识别的类库(如rt.jar)加载到虚拟机内存中。

  • 扩展类加载器(sun.misc.Launcher$ExtClassLoader): 它负责加载<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用

  • 应用程序类加载器(sun.misc.Launcher$AppClassLoader): 这个类加载器是ClassLoader.getSystemClassLoader()的返回值,所以也称为系统类加载器。负责加载ClassPath(用户类路径)上所指定的类库。如果没有自定义的类加载器,这是程序中默认的类加载器。

这里写图片描述

双亲委派模型

  • 双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都要有自己的父加载器,以组合关系来实现

  • 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(搜索范围内没有找到)时,子加载器才会尝试自己去加载。

  • 好处:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object, 它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载。因此Object类在程序的各种类加载器环境中都是同一个类。(否则,由各个类加载器自行加载,则会出现多个不同的Object类。)

  • 简单实现过程

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    Class c = findLoadedClass(name);                //首先,检查请求的类是否被加载过了
    if (c == null){                                 //如果没有被加载过
        try{
            if(parent != null){
                c = parent.loadClass(name,false);   //如果有父类加载器,则由父类加载器来加载(可能会递归)
            }else{
                c = findBootstrapClassOrNull(name); //没有父类加载器,由BootStrap ClassLoader加载
            }
        }catch(ClassNotFoundException e){
            //如果父类加载器
            //说明父类加载器无法完成加载请求
        }
        if (c == null){
            //在父类加载器无法加载时
            //再调用本身的findClass方法进行类加载
            c = findClass(name);
        }
    }

    if(resolve){
        resolveClass(c);    //?
    }

    return c;
}
原文地址:https://www.cnblogs.com/jtlgb/p/8743100.html