一. 基本概念
Java虚拟机: java程序 被编译后产生的.class字节码文件,会被加载到jvm,java.exe会执行这个class文件(java.exe实质是装载jvm.dll),只要操作系统在jvm,就可以运行这个字节码文件,即平台无关性;是java的运行环境也是jre的一部分
二. 生命周期
1.jvm实例的诞生:当一个java程序启动时,产生一个jvm实例。任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点
2.Jvm实例的运行: main()作为该程序初始线程的起点,任何其他线程均由该线程启动,
说明 :jvm内部有两个线程:
守护线程:jvm自己使用的线程(垃圾回收线程)
普通线程:java线程 (main函数)
3 .jvm实例的消亡: 当程序中的所有非守护线程都终止时,JVM才退出
三. jvm启动过程
1.创建JVM装载环境和配置:当运行一个java程序时,会根据path找到java.exe, java.exe会根据一系列的过 程查找到jvm的路径和参数的配置,过程如下:
a. 首先查找jre路径,Java是通过GetApplicationHome api来获得当前的Java.exe绝对路径, c:\j2sdk1.4.2_09\bin\Java.exe,那么它会截取到绝对路径c:\j2sdk1.4.2_09\,判断 c:\j2sdk1.4.2_09\bin\Java.dll文件是否存 在, 如果存在就把c:\j2sdk1.4.2_09\作为jre路径
b 如果不存在则判断c:\j2sdk1.4.2_09\jre\bin\Java.dll是否存在,如果存在这c:\j2sdk1.4.2_09\jre作为jre路径.如果不存在调用GetPublicJREHome 查 HKEY_LOCAL_MACHINE\Software\JavaSoft\Java RuntimeEnvironment\“当前JRE版本号”\JavaHome的路径为jre路径
2.装载JVM.dll: 通过第一步已经找到了JVM的路径,Java通过LoadJavaVM来装入JVM.dll文件
3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例:初始化JVM,获得本地调用接口
4.调用JNIEnv实例装载并处理class类。运行jar:Java -jar XXX.jar;运行class文件
四. 类加载子系统(类加载器)
1. JVM类装载过程
(1) 加载:通过一个类的全限定名来获取定义此类的二进制字节流。 将这个字节流所代表的静态存储结构转换为方法区(JDK1.8以前或者元数据(JDK1.8以的运行时数据结构。 在内存中生成一个代表这个类的java.lang.Class对象,作为方法 区 或元数据区这个类的各种数据的访问入口
(2)验证: 这一阶段的主要目的是为了确保Class文件的字节流中所包含的信息符合当前JVM的要求,并且不 会危害JVM自身的安全。
(3)准备:准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量 所使用的内存都将在方法区(<Jdk1.8)元数据区(>=Jdk1.8)中进行分配。这时候进行内存分配 的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化的时候随对象一起分配在 Java堆中
(4)解析:对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。解析阶段的目的就是将这些符号引用 解 析成为实际引用。而实际引用就是真正指向内存地址的指针、相对偏移量或能间接定位到目标的句柄。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限 定符这7类符号引用进行
(5)初始化:类初始化是类加载过程的最后一步,是为标记为常量值的字段赋值如果直接赋值的静态字段被 final所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量 值(ConstantValue),其初始化直接由Java 虚拟机完成。
2.类加载器
(1)Bootstrap ClassLoader:启动类加载器加载JDK中的核心类库,由c++编写 是jvm的一部分
(2)Extension ClassLoader:扩展类加载器,由Bootstrap classloader 加载,加载java的扩展类库
(3)App ClassLoader:系统类加载器,由Bootstrap classloader 加载 ,加载应用程序classpath目录下所有的jar和 class文件
说明 :类加载是有层次关系的这种关系被称之为类加载器的“双亲委派模式”,它要求除了顶层启动类加载器外,其余 所有的类加载器都应当有自己的父类加载器
五. Jvm内存模型
1.方法区
a 保存所加载的类的信息(名称,修饰符),类中的静态变量,类中定义为fianl类型的常量,
b 类中的field信息,类中的方法信息
c 内存可以在运行时扩展,会被垃圾回收
d 所有线程共享,必须保证线程安全 outofmemery
2.常量池(方法区的一部分):
a 存放编译期间生成的各种字面量和符号引用,
b 编译期间产生的+运行期间产生的都放在常量池
c 存放的是对象的引用而不是对象本身
d Byte、Short、Integer、Long、Character、Boolean、String这7种包装类都各自实现了自己的常量池 Float和 Double 没有实现常量池。
e Byte、Short、Integer、Long、Character这5种包装类都默认创建了数值[-128 , 127]的缓存数据。当对这5个类型的数据不在这个区间内的时候,将会去创建新的对象,并且不会将这些新的对象放入常量池中。
f String包装类:
以: String str1 = "aaa" 为例来说明string的创建过程
首先jvm会去常量池中是否有aaa这个对象;如果存在返回这个对象的引用给str1;如果不存在在堆中创建 一个相应的对象,将对象的引用存储在常量池中,返回对象的引用给str1 New构造的字符串对象,不管常量池中是否存在相同对象的引用,都会创建 新的字符串, 调用intern()方法会去检查常量池中是否有这个对象的引用
g Stringbuilder生成的字符串会自动加载到常量池中
3.java堆
a 存储数据实例和数组值即java通过new创建的对象
b 线程共享,所以给对象分配内存时要加锁
c 占用的空间内存最大
d 先进先出 可以动态分配内存大小 垃圾回收
3.1堆的分区
新生代: 新生代又可以划分为一块较大的Eden区域和两块较小的Survivor空间,每次使用Eden和其中一块Survivor区域。回收时,将存活的对象一次性拷贝到另一块Survivor空间上,再清理掉用过的Eden和Survivor空间。默认Eden区域:Survivor区域=8:1 也就是每次使用新生代容量的90%,只有10%被浪费; 复制算法进行垃圾回收
老年代: 默认情况下当年龄到达15后,就会晋升到老年代中。老年代采用标记清除和标记整理算法进行垃圾收集。
3.2 对象的创建
(1)首先程序计数器在收到这个new得到指令时候,先到方法区的常量池检查有没有这个类的符号引用,然后检查类是否加载解析初始化过没有就进行类加载
(2)类加载完成后,JVM就要在java堆上为对象分配内存,这个内存大小是在类加载的时候就确定的,从java堆中分配内 存有两种方式一个指针碰撞,一个空闲列表方式,两种方式各有优点
(3)初始化,JVM将分配到的内存区域初始化为零值,这个操作保证了对象的实例字段可以不赋初始值就可以使用,然后对对象头数据进行设置(对象分为3部分内容)
(4)执行方法,这个方法其实就是实现构造函数的赋值内容,对数据进行初始化,到此一个对象的创建就完成了
4. Java栈
Java栈中只保存基本数据类型和自定义的对象的引用
先进后出
数据超出作用域后会自动释放不进行垃圾回收
每个线程都是建立一个私有的栈,每个栈包含多个栈帧,每个方法的每次调用都会创建一个栈帧
注意:包装类型String Integer Byte Short Boolean Long Character 放在堆中
5. 本地方法栈
类似java栈 存储本地方法的局部变量
在调用本地方法接口或程序时启动
由于本地方法由c编写 不由jvm运行 不受jvm管理
6.程序计时器
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令;JVM的多线程是通过线程轮流切换并分配 处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器
程序计数器仅占很小的一块内存空间
说明 : 程序计数器这个内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError(内存不足错误)的区域。
六. GC的判定方法
即判定这个对象是否存活,包含引用计数+引用链
1 引用计数
给对象添加一个计数器,如果对象被引用 计数器+1,引用失效计数器-1,当计数器=0时则判定对象没有被引用
2 引用链
(可达性分析算法)通过GC-Root为对象起点 通过这个节点向下搜索,当一个对象到GC-Root没有被任何引用连接时,对象不可用
七. GC收集方法
1 标记清除算法
标记所有需要回收的对象,标记完后统一回收
缺点 : 效率不高标记清楚后会产生大量不连续内存碎片,当需要大块连续内存空间时 无法找到
2 复制算法
将内存按容量分为大小相等的两块区域,每次使用其中一块,当一块用完了,将存活的移到另一个区域,统一清除掉
缺点 : 每次只能使用内存的一半,存活多的话会大量复制 效率低
3 标记整理算法
先对死亡的进行标记,将存活的移到另一端,清理掉边界以外的内存
4.分代收集算法
根据存活的时间将对象分为新生代和老年代 新生代使用复制算法 老年代使用 标记清除算法+标记整理算法