JVM执行流程
- .java文件被编译为.class文件
- 加载.class文件(类加载器)
- 管理分配内存
- 执行垃圾收集
JVM的声明周期
- JVM实例对应了一个独立运行的java程序它是进程级别的
- 启动,启动一个java程序时,一个JVM实例就产生了,任何一个用户public static void main的函数的class都可以作为JVM实例的运行起点
- 运行,main()作为该程序初始线程的起点,任何其他线程均有该线程启动.JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表名自己创建的线程是守护线程
- 消亡,当程序中的所有非守护线程都终止时,JVM才推出;如安全管理器运行,程序也可以使用Runtime类,或者System.exit()来退出.
java内存分配
-
方法区
- 所有线程共享的内存区域
- 用于存储已被虚拟机加载类的信息,常量,静态变量,即时编译期编译后的代码等数据
- 运行时常量池:方法区中的一部分,class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项是常量池,用于存放便器期生成的字面量和符号引用,这部分信息将在类加载后存放到方法区的运行时常量池中
-
程序计数器
- 由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此在任一时刻,一个CPU的内核只会执行一条线程中的指令
- 因此,为了能够使得每个线程都在线程切换后能恢复到切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能相互被干扰,否则就会影响到程序正常执行次序。因此,可以这么说,程序计数器是每个线程私有的。由于程序计数器中存储的数据所占空间大小不会随程序的执行而发生改变,因此程序计数器是不会发生内存溢出现象的。
-
常量池
- 存放常量,在程序执行的时候,常量池会存在方法区中,而不是在堆中
-
寄存器
- 这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器
- 然而寄存器的数量十分有限
- 因为寄存器是根据需要由编译器分配
- 我们对此没有控制权,也不可能在自己的程序中找到寄存器存在的任何踪迹
-
栈
- 存放基本类型的数据和对象的引用,局部变量,随方法的消失而消失
- 但是对象本身不存放在栈中,而是存放在堆中
堆和栈的区别
1. 堆是由垃圾回收负责的,堆的优势是可以动态地分配内存大小,缺点是由于要在运行时动态分配内存,存取速度缓慢
2. 栈的优势是,存取速度比堆快,仅次于寄存器,栈数据可以共享,但是缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性
3. 栈和常量池的对象可以共享,对于堆中的对象不可以共享
-
堆(heap)
- 所有线程共享的一块区域,在虚拟机启动时创建;
- 存放对象实例,几乎所有的对象实例都在这里分配内存(不是绝对的);
- 垃圾回收器主要管理的区域,因此常称为"GC堆";
- 可分为"新生代"和"老年代",新生代又被进一步划分为一个Eden(一些大对象特殊处理)和survivor(存放每次垃圾回收后存活的对象)区
- 存放用new产生的数据,成员变量由垃圾回收机制处理
-
静态域
- 存放在对象中用static定义的静态成员
字符串内存配分配
- 常量池: 编译期已经创建好(直接用双引号定义的)final string a = “123”;
- 堆:运行期(new出来的) String a = new String();
equals相等的字符串,在常量池中永远只有一份,在堆中有多份,对于通过new产生一个字符串时,会先去常量池中查找是否已经有了对象,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此对象的拷贝对象。
问?String = new String(“xyz”)产生了几个对象?
一个或者两个,如果常量池中原来没有"xyz",会先在常量池中创建一个,在堆中再创建一个该对象的拷贝对象
成员变量和局部变量在内存中的分配
局部变量在栈中:随方法的消失而消失
成员变量在堆中:由垃圾回收负责回收
GC算法
GC的对象是java堆和方法区(永久区)
搜索算法
-
引用计数算法
- 原理:是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再使用,是垃圾
- 缺点:
- 无法处理循环引用的问题。如:对象A和B分别有字段b,a,令A.b=B和B.a=A,除此之外这2个对象再无任何引用,那实际上这两个对象已经不可能再被访问,但是引用计数算法已经无法回收它们
- 引用计数的方法需要编译器的配合,编译器需要为此对象生成额外的代码
-
根搜索算法
- 原理:根搜索算法是通过一些"GC Roots"对象作为起点,从这些节点往下搜索,搜索通过的路径称为引用链(Reference Chain),当一个对象没有被GC Roots引用链连接的时候,说明这个对象是不可用的。
- 可以作为GC Roots对象的有
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区域中的类静态属性引用的对象
- 方法区域中常量引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
回收算法
-
标记-清除算法
- 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个过程,然后进行两项工作,第一项是标记,第二项是清除
- 标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有的GC Roots可达的对象标记为存活对象
- 清除:清除的过程将遍历堆中的所有对象,将没有标记的对象全部清除掉
- 缺点:效率低;清理出来的空间不是连续的
-
复制算法
- 原理:将内存划分为两个区域:空闲空间和活动区间。当内存耗尽的时候,将活动区间所有的存活对象全部复制在空闲区间,安装内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。此时,空闲区间已经与活动空间交换,而垃圾对象现在已经全部留在原来的活动区间
- 缺点:浪费了一半的内存,存活率低的时候使用
-
标记-整理算法
- 原理:分为标记和整理两个阶段
- 标记:它的第一阶段与标记-清除算法一样,均是遍历GC Roots,然后将存活的对象标记
- 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此第二阶段才是整理阶段
- 原理:分为标记和整理两个阶段
三个算法的对比
1. 效率:复制算法>标记-整理算法-标记-清除算法
2. 内存整齐度:复制算法=标记-整理算法>标记-清除算法
3. 内存利用率:标记-整理=标记-清除>复制算法
分代搜索算法
- 对象的分类
- 夭折对象(新生代):比如某一个方法的局域变量,循环内临时变量(复制算法)
- 老不死对象(老年代):比如缓存对象,数据库连接对象,单例对象等(标记-整理/标记清除)
- 不灭对象: 比如String池中的对象(享元模式),加载过的类的信息。
Java中的引用类型
Java对象中的引用分为四种级别,这四种级别由高到低依次为:强引用,软引用,弱引用,虚拟引用,
- 强引用(Strong Reference)
这个就不多说,我们写代码天天在用的就是强引用。如果一个对象被人拥有强引用,那么垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象来解决内存不足的问题
Java的对象位于heap中,heap中的对象有强可及对象,弱可及对象,虚可及对象和不可到达对象。引用的强弱顺序是强,软,弱和虚
-
软引用(Soft Reference)
如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的告诉缓存,在 jvm 报告内存不足之前会清除所有的软引用。 -
弱引用(Weak Reference)
如果一个对象只具有弱引用,那么该类就是可有可无的对象,因为只要该对象被GC扫描到了随时都会把它干掉。弱引用和软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。只不过垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 -
虚引用(Phantom Reference)
虚引用顾名思义,就是形态虚设,与其他几种引用都不同。虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用和弱引用的区别在于:虚引用必须和引用队列(Reference Queue)联合使用。当垃圾回收器准备回收一个对象的时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否加入了虚引用,来了解被引用的对象是否将要被回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
堆和栈的区别
-
申请方式
- 栈:系统自动分配
- 堆:需要程序员自己申请,并指明大小(new Object()形式)。
-
申请后系统的响应
- 栈:只要栈的剩余空间大于所申请的空间,系统将为程序提供内存
- 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
-
申请大小限制
- 栈:向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
- 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
-
申请效率比较
- 栈:由系统自动分配,速度较快。但是程序员无法控制。
- 堆:由new分配内存,一般速度缓慢,而且容易产生内存碎片,不过用起来方便。
-
heap 和 stack 中的存储内容
- 栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
- 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
-
数据结构层面区别
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第 1 个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
Java的类加载器
- Java的类加载器有哪些?
- 跟加载器(BootStarp)
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System\App)
- 自定义加载器(必须继承ClassLoader)
Java类加载体系之ClassLoader双亲委托机制
类加载体系:java程序中的.java文件编译完生成.class文件,而.class文件就是通过称为类加载器的ClassLoader加载的,而ClassLoader在加载过程中,会使用双亲委托机制来记载.class文件
-
BootStrapClassLoader : 启 动 类 加 载 器 , 该 ClassLoader 是 jvm 在 启 动 时 创 建 的 , 用 于 加载 /JAVA_HOME/jre/lib 下面的类库(或者通过参数-Xbootclasspath 指定)。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。
-
ExtClassLoader:扩展类加载器,该ClassLoader是在sun.misc.Launcher里作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader 会加载 $JAVA_HOME/jre/lib/ext 下的类库(或者通过参数-Djava.ext.dirs 指定)。
-
AppClassLoader:应用程序类加载器,该 ClassLoader 同样是在 sun.misc.Launcher 里作为一个内部类AppClassLoader 定义的(即sun.misc.Launcher$AppClassLoader),AppClassLoader 会加载 java 环境变量
CLASSPATH 所 指 定 的 路 径 下 的 类 库 , 而 CLASSPATH 所 指 定 的 路 径 可 以 通 过System.getProperty(“java.class.path”)获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的 class 目录)。 -
CustomClassLoader:自定义类加载器,该 ClassLoader 是指我们自定义的 ClassLoader,比如 tomcat 的StandardClassLoader 属于这一类;当然,大部分情况下使用 AppClassLoader 就足够了。
ClassLoader的双亲委托机制是这样的
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader去加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成
- 如果BootStrapClassLoader加载失败,(例如在 J A V A H O M E JAVA_HOME JAVAHOME/jre/lib 里未查找到该 class),会使用ExtClassLoader来尝试加载
- 当ExtClassLoader也记载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,会报ClassNotFoundException。