内存优化(一)---JVM基础

1.JAVA虚拟机(JVM)

Java 虚拟机(JVM)是执行已编译 Java 字节码(形式为 .class 文件)的软件。它是 Java 平台的重要组成部分,包括程序、规范、库和数据结构,让它们协同工作。


1.1程序计数器

1.线程私有

程序计数器(Program Conputer Register)这是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

2.记录当前字节码指令执行地址

如果当前线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,则这个计数器值为空(Undefined)。

3.不抛 OutOfMemoryError 异常

程序计数器的空间大小不会随着程序执行而改变,始终只是保存一个 returnAdress 类型的数据或者一个与平台相关的本地指针的值。所以该区域是Java运行时内存区域中唯一一个Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

1.2虚拟机栈

Java虚拟机栈(Java Virtual Machine stack),这块区域也是线程私有的,与线程同时创建,用于存储栈帧。Java 每个方法执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

1.线程私有

随线程创建而创建,声明周期和线程保持一致。

2.由栈帧组成

线程每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

3.抛出 StackOverflowError 和 OutOfMemoryError 异常

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError  异常。

1.3本地方法栈

本地方法栈(Native Method Stacks)作用和虚拟机栈类似,虚拟机栈执行的是Java方法,本地方法栈执行的是 Native 方法,本地方法栈也会抛出抛出 StackOverflowError 和 OutOfMemoryError 异常。

1.4Java堆

Java堆是Java虚拟机所管理内存最大、被所有线程共享的一块区域,目的是用来存放对象,基本上所有的对象实例和数组都在堆上分配(不是绝对)。Java堆也是垃圾回收器管理的主要区域。

1.线程共享

堆存放的对象,某个线程修改了对象属性,另外一个线程从堆中获取的该对象是修改后的对象,为什么堆要设计成线程共享呢?

我们可以假设堆是线程私有的,很显然一个系统创建的对象会有很多,而且有些对象会比较大,如果设计成线程私有的,那么如果有很多线程同时工作,那么都必须给他们分配相应的私有内存,我相信内存很快就撑爆了,很显然将堆设计为线程共享是最好不过了,不过凡事都具有两面性,线程共享的设计这也带来了多线程并发资源冲突问题。

2.存放对象

基本上所有的对象实例和数组都要在堆上进行分配,但是随着 JIT 编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换等优化技术会导致对象不一定在堆上进行分配。

3.垃圾收集

Java堆也被称为“GC堆”,是垃圾回收器的主要操作内存区域。当前垃圾回收器都是使用的分代收集算法,所以Java堆还可以分为:新生代和老年代,而新生代又可以分为 Eden 空间、From Survivor 空间、To Survivor空间。这是为了更好的回收内存。


4.抛出 OutOfMemoryError 异常

根据Java虚拟机规范,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,实现时既可以实现成固定大小,也可以是扩展的。如果在堆中没有完成实例分配,并且堆也无法扩展,将抛出OutOfMemoryError 异常。

1.5方法区

方法区(Method Area)用来存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

方法区也称为“永久代”,这是因为垃圾回收器对方法区的垃圾回收比较少,主要是针对常量池的回收以及对类型的卸载,回收条件比较苛刻。经常会导致对此内存未完全回收而导致内存泄露,最后当方法区无法满足内存分配时,将抛出 OutOfMemoryError 异常。

2.垃圾回收

程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作,这几个区域的内存分配和回收都具备确定性,在方法结束或线程结束时,内存也就跟着回收了,所以不需要我们考虑。

那么现在就剩下Java堆方法区了,这两块区域在编译期间我们并不能完全确定创建多少个对象,有些是在运行时期创建的对象,所以Java内存回收机制主要是作用在这两块区域。

2.1GC如何确定内存回收

①、引用计数算法

给每一个创建的对象增加一个引用计数器,每当有一个地方引用它时,这个计数器就加1;而当引用失效时,这个计数器就减1。当这个引用计数器值为0时,也就是说这个对象没有任何地方在使用它了,那么这就是一个无效的对象,便可以进行垃圾回收了。

这种算法实现简单,而且效率也很高。但是Java没有采用该算法来进行垃圾回收,因为这种算法无法解决对象之间的循环引用问题。

```

public class Person {

    private Byte[] _1MB = null;

    public Person() {

        /**

        * 这个成员属性的作用纯粹就是占据一定内存

        */

        _1MB = new Byte[1*1024*1024];

    }

    private Person father;

    private Person son;

    public Person getFather() {

        return father;

    }

    public void setFather(Person father) {

        this.father = father;

    }

    public Person getSon() {

        return son;

    }

    public void setSon(Person son) {

        this.son = son;

    }

}

public static void main(String[] args) {

    Person father = new Person();

    Person son = new Person();

    father.setSon(son);

    son.setFather(father);

    father = null;

    son = null;

    System.gc();

}

```


 father对象和son对象,其引用计数第一个是栈内存指向,第二个就是其属性互相引用对方,所有引用计数器都是2。


将这两个对象置为null,也就是去掉了栈内存指向。这时候其实这两个对象只是自己互相引用了,引用计数器为1,那么这两个对象按照引用计数算法实现的虚拟机就不会回收


②、根搜索算法(可达性分析算法)

在主流的商用程序中(Java,C#),都是使用根搜索算法(GC Roots Tracing)来判定对象是否回收的。

该算法思路:通过一系列名为“GC Roots” 的对象作为终点,当一个对象到GC Roots 之间无法通过引用到达时,那么该对象便可以进行回收了。

上图ObjA,ObjB,ObjC到GC Roots是可达的,所以不会被垃圾回收。ObjD,ObjE,ObjF到GC Roots是不可达的,所以会被垃圾回收

在Java语言中,有如下4中对象可以作为 GC Roots:

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

2、方法区中的静态变量属性引用的对象

3、方法区中常量引用的对象

4、本地方法栈中(JNI)(即一般说的Native方法)的引用的对象

2.2如何进行垃圾回收

这里需要说明的是宣告一个对象死亡,至少要经历两次标记,前面我们说过,如果对象与GC Roots 不可达,那么此对象会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆盖 finalize()方法,或者该方法已经执行了一次,那么虚拟机都将视为没有必要执行finalize()方法。

如果这个对象有必要执行 finalize() 方法,那么该对象将会被放置在一个有虚拟机自动建立、低优先级,名为 F-Queue 队列中,GC会对F-Queue进行第二次标记,如果对象在finalize() 方法中成功拯救了自己(比如重新与GC Roots建立连接),那么第二次标记时,就会将该对象移除即将回收的集合,否则就会被回收。

2.3引用类型

1.强引用  Object a=new Object();

2.软引用

内存不足时回收,存放一些重要性不是很强又不能随便让清除的对象,比如图片切换到后台不需要马上显示了

3.弱引用

第一次扫到了,就标记下来,第二次扫到直接回收

```python

ReferenceQueue referenceQueue=new ReferenceQueue();

Object a=new Object();

WeakReference weakReference=new WeakReference(a,referenceQueue);

weakReference.get();//GC没有回收可以返回a对象,如果回收返回null

referenceQueue.poll();//GC没有回收返回null,如果回收返回a的引用

```

4.虚引用

幽灵 幻影引用 不对生存造成任何影响,用于跟踪GC的回收通知

```python

ReferenceQueue referenceQueue=new ReferenceQueue();

Object a=new Object();

PhantomReference phantomReference=new PhantomReference(a,referenceQueue);

phantomReference.get();//无论GC是否回收都返回null

referenceQueue.poll();//GC没有回收返回null,如果回收返回a的引用

```

3.内存泄漏

Android 中的内存泄漏的原因非常简单,那就是生命周期较长的对象持有生命周期较短的对象的引用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容