java程序执行过程 +JVM内存管理 + GC垃圾回收机制

java程序执行过程 +JVM内存管理 + GC垃圾回收机制

1、Java程序执行过程

一个java程序的编译和执行过程如下:

  • .java ——编译——> .class
  • 类加载器负责加载各个字节码文件(.class)
  • 加载完.class后,由执行引擎执行,在执行过程中,需要运行时数据区提供数据

这里写图片描述


补充:手动编译.java

Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println("asdf");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

使用命令javac Main.java编译,编译后能看到一个.class文件,里面长这样

cafe babe:前四个字节称为魔数,作用是标识不同版本的虚拟机 
0000 0034:是java的版本号,0000是次版本号,0034是主版本号

  • 0x0034 = 3 * 16 + 4 = 52 表示JDK1。8版本

0013:是常量池入口,0x0013 = 19,表示常量池有18项常量(1~18,第0项空出来)

cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 0009 4d61 696e 2e6a
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

最后使用命令java Main运行刚才的程序

关于.class文件的分析,请见:http://www.cnblogs.com/winner-0715/p/4935256.html



2、JVM内存管理

JVM将内存划分为6个部分:PC寄存器(也叫程序计数器)、虚拟机栈、堆、方法区、运行时常量池、本地方法栈

这里写图片描述


  • PC寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。

  • java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有的。java程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的临时数据和中间结果,包括局部变量表、操作数栈、动态链接、方法出口等信息。这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出StackOverflowError异常。

    • 局部变量表:方法的局部变量列表,在编译时就写入了class文件
    • 操作数栈:int x = 1; 就需要将 1 压入操作数栈,再将 1 赋值给变量x
  • java 堆:java堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出OutOfMemoryError异常。

    StackOverflowErrorOutOfMemoryError
    java栈java堆
    栈深度超过范围了(比如:递归层数太多了)内存空间不够了(需要及时释放内存)


  • 方法区:方发区被各个线程共享,用于存储静态变量、运行时常量池等信息。

  • 本地方法栈:本地方法栈的主要作用就是支持native方法,比如在java中调用C/C++


3、GC回收机制

  • 哪些内存需要回收?——who
  • 什么时候回收?——when
  • 怎么回收?——how

1、哪些内存需要回收?

  • java堆、方法区的内存
线程私有线程共享
程序计数器、虚拟机栈、本地方法栈java堆、方法区
随线程生而生,随线程去而去。线程分配多少内存都是有数的,当线程销毁时,内存就被释放了堆和方法区的内存都是动态分配的(使用new关键字),所以也需要动态回收。
这部分内存的回收依赖GC完成


2、什么时候回收?

  • 引用计数法
  • 可达性分析

(1)、引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一。反之每当一个引用失效时,计数器减一。当计数器为0时,则表示对象不被引用。

举个例子:

Object a = new Object(); // a的引用计数为1
a = null; // a的引用计数为0,等待GC回收
  • 1
  • 2

但是,引用计数法不能解决对象之间的循环引用,见下例

Object a = new Object(); // a的引用计数为1
Object b = new Object(); // b的引用计数为1

a.next = b; // a的引用计数为2
b.next = a; // b的引用计数为2

a = null; // a的引用计数为1,尽管已经显示地将a赋值为null,但是由于引用计数为1,GC无法回收a
b = null; // b的引用计数为1,同理,GC也不回收b
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(2)、可达性分析

设立若干根对象(GC Root),每个对象都是一个子节点,当一个对象找不到根时,就认为该对象不可达。

这里写图片描述

没有一条从根到Object4 和 Object5的路径,说明这两个对象到根是不可达的,可以被回收

补充:java中,可以作为GC Roots的对象包括:

  • java虚拟机栈中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象


3、怎么回收?

  • 标记——清除算法
  • 复制算法
  • 分代算法

(1)、标记——清除算法

遍历所有的GC Root,分别标记处可达的对象和不可达的对象,然后将不可达的对象回收。

缺点是:效率低、回收得到的空间不连续

(2)、复制算法

将内存分为两块,每次只使用一块。当这一块内存满了,就将还存活的对象复制到另一块上,并且严格按照内存地址排列,然后把已使用的那块内存统一回收。

优点是:能够得到连续的内存空间 
缺点是:浪费了一半内存

这里写图片描述

(3)、分代算法

在java中,把内存中的对象按生命长短分为:

  • 新生代:活不了多久就go die 了,比如局部变量
  • 老年代:老不死的,活的久但也会go die,比如一些生命周期长的对象
  • 永久代:千年王八万年龟,不死,比如加载的class信息

有一点需要注意:新生代和老年代存储在java虚拟机堆上 ;永久代存储在方法区上

 回收方法
新生代使用复制算法
老年代使用标记——清除算法
永久代————————



补充:java finalize()方法:

在被GC回收前,可以做一些操作,比如释放资源。有点像析构函数,但是一个对象只能调用一次finalize()方法。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页