JVM的中文名称叫Java虚拟机,它是由软件技术模拟出计算机运行的一个虚拟的计算机。JVM也充当着一个翻译官的角色,我们编写出的Java程序,是不能够被操作系统所直接识别的,由JVM负责把程序翻译给系统“听”,告诉它我们的程序需要做什么操作。
JVM在每个操作系统中有其对应的Java解释器,解释器会将Java程序经过编译后产生的.Class文件解释成特定的机器码,被操作系统所识别,实现一次编译到处运行。
(1)类加载系统(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。
(2)内存空间(也叫运行时数据区):是在JVM运行的时候所分配的内存区,运行时内存区主要可以划分为5个区域;
(3)执行引擎:负责执行class文件中包含的指令;
(4)本地库接口:主要是调用C或C++实现的本地方法及返回结果;
JVM在Java程序开始执行的时候,它才运行,程序结束的时它就停止。一个Java程序会开启一个JVM进程,如果一台机器上运行三个程序,那么就会有三个运行中的JVM进程。
JVM中的线程分为两种:守护线程和普通线程。*守护线程是:JVM自己使用的线程,比如垃圾回收(GC)。普通线程是:Java程序的线程*,只要JVM中有普通线程在执行,那么JVM就不会停止。权限足够的话,可以调用exit()方法终止程序。
JVM的内存管理就是:内存对象的分配和释放问题,程序员需要为每个对象申请内存空间 (基本类型除外),对象的释放是由GC决定和执行的。
这种收支两条线的方法确实简化了程序员的工作,但也加重了JVM的工作,这也是Java程序运行速度较慢的原因之一。因为GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
public class B {
A a = null; //此a就是B类中对A的引用(或者说:B类对象持有A对象的引用)
a = new A(); //用此a引用创建了A类的实例
}
public class Main{
int a = 1;//堆中
Student s = new Student();//堆中 堆中
public void XXX1(){
int b = 1;//栈中
Student s = new Student();//栈中 堆中
}
public void XXX2(){
String s1 = new String("myString");//栈中 堆中
String s2 = "myString";//栈中 方法区
}
}
(1)局部变量的基本数据类型和引用存储于栈中,引用创建的对象存储于堆中——因为它们属于方法中的变量,生命周期随方法而结束。
(2)全局变量全部存储在堆中(包括基本数据类型,引用,和引用创建的对象)——因为它们属于类,类对象终究是要被new出来使用的。
(3)我们所说的内存泄露,只针对堆内存,它们存放的就是引用指向的对象实体。
释放对象的根本原则就是:对象不会再被使用:
①给对象赋予了空值null,之后再没有调用过;
②另一个是给对象赋予了新值,这样重新分配了内存空间。
JVM通过GC机制(内存垃圾回收机制)来释放回收堆和方法区中的内存,这个过程是自动执行的。GC会从根节点(GC Roots)开始对堆内存进行遍历,到最后,没有直接或者间接引用到根节点的就是需要回收的垃圾,会被GC回收掉。GC主要完成3件事:①确定哪些内存需要回收;②确定什么时候需要执行GC;③如何执行GC。
为了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外每个线程对象可以作为一个图的起始顶点。
例如:大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达,那么我们认为这个(这些)对象不再被引用,可以被GC回收。
图例演示–3分钟了解:Java垃圾收集器GC 如何确定哪些是“垃圾”?
(1)虚拟机栈中的对象引用(引用是在栈帧中的本地变量表中的),真正的对象在堆中;
(2)方法区中的类静态对象引用;
(3)方法区中的常量池对象引用;
(4)本地方法栈中的JNI的对象引用。
(1)引用计数法:使用计数器进行内存管理。被引用1次,计数器加1,没有被引用的时候,则回收。但是引用计数法无法解决对象之前相互引用的问题,因此已经废弃。
(2)可达性算法(根搜索算法):有向图的方式进行内存管理。通过GC ROOT对象开始搜索,不可达的对象则回收。这时候可以提到引用的类型,主要用得最多就是强引用和弱引用。当存在强引用的时候,内存不足宁愿抛出OOM也不会回收;但是是弱引用的话,就有可能会被回收,这样就防止了内存泄漏。
(1)标记-清除算法:搜索,发现没有引用的对象,直接回收,但是会导致内存碎片过多。
(2)复制算法:搜索,扫描没有引用的对象。开辟新的内存空间,将存活的对象复制到新的内存,旧的内存直接清除。由于需要多次交换内存空间,因此在对象数量比较少的时候效率比较高。
(3)标记-整理算法:在标记-清除算法的基础上,清除掉不存活的对象之后,把后面的存活对象搬移过来,似的内存连续,解决了内存碎片的问题。
(4)三种算法是混合使用的,不同情况,例如对象数量不同,采用不用的算法,已达到最大的效率。
(1)GC_CONCURRENT:当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。
(2)GC_FOR_MALLOC:当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。
(3)GC_HPROF_DUMP_HEAP:当生成Hprof文件的时候,系统会进行GC操作。
(4)GC_EXPLICIT:主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在Android Monitor中,通过工具按钮告诉系统进行GC操作的。
Android的类加载器跟原生的类加载器不一样,但是都大同小异:
public class Demo{
public Demo{}
}
Demo demo = new Demo();
//可以写成
Demo demo;//创建对象引用
demo=/*将对象引用指向对象*/new Demo();//创建对象
(1)右边的“new Demo”,创建一个Demo对象,存储在堆内存中。
(2)末尾的()意味着:在对象创建后,立即调用Demo类的构造函数,对对象进行初始化。
(3)左边的“Demo demo”声明了Demo类引用变量,存储在栈内存中。
(4)“=”操作符使对象引用指向刚创建的Demo对象。
图片来源于:java–对象引用与对象的区别
1.首先虚拟机读取指定的路径下的Person.class文件,并加载至内存(如果该对象有直接父类则会先加载父类)—-方法区
2.按顺序执行父类的static代码块和static变量,再执行子类的static代码块和static变量 —-方法区
3.创建Person对象,在堆内存开辟空间分配堆内存地址 —-堆中
4.将父类对象的属性和代码块默认初始化(int类型为0,String类型为null);对父类对应的构造函数进行初始化 —-栈中
5.将子类对象的属性和代码块默认初始化(int类型为0,String类型为null);对子类对应的构造函数进行初始化 —-栈中
6.进行子类构造函数的特定初始化例如声明赋值变量(这种情况较少) —-栈中
7.声明一个person对象引用—-栈中
8.初始化完毕,栈中的person对象引用指向堆内存中Person对象。
参考链接:Person p=new Person()的感悟
Dalvik是Google公司自己设计用于Android平台的Java虚拟机。dex格式是专为Dalvik应用设计的一种压缩格式,适合于内存和处理器速度有限的系统。Dalvik允许同时运行多个虚拟机的实例,并且每一个应用作为独立的Linux进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
(1)Dalvik指令集是基于寄存器的架构,dex字节码更适合于内存和处理器速度有限的系统,允许同时运行多个虚拟机的实例。
(2)而JVM是基于栈的。执行的是class文件。
2014年6月谷歌I/O大会,Android L 改动幅度较大,Google将直接删除Dalvik,代替它的是传闻已久的ART。
(1)在Dalvik下,应用每次运行都需要通过即时编译器(JIT)将字节码转换为机器码,即每次都要编译加运行,这虽然会使安装过程比较快,但是会拖慢应用以后每次启动的效率。
而在ART 环境中,应用在第一次安装的时候,字节码就会预编译(AOT)成机器码,虽然设备和应用的安装会变慢,但是以后每次启动执行的时候,都可以直接运行,因此运行效率会提高。
(2)ART占用空间比Dalvik大(字节码变为机器码之后,可能会增加10%-20%),这也是著名的“空间换时间大法”。
(3)预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。
虚拟机
类加载
Android类加载器ClassLoader
学习Java的内存分配机制和内存泄漏问题
JVM内存管理及GC机制