一.JAVA技术体系
Sun官方所定义的JAVA技术体系包括以下几个组成部分:
- Java程序设计语言
- 各种硬件平台上的Java虚拟机
- Class文件格式
- Java API类库
- 第三方Java类库
- JDK(Java Development Kit):支持Java程序开发的最小环境,Java程序设计语言+Java API类库+Java虚拟机;
- JRE(Java Runtime Environment):支持Java程序运行的标准环境,Java API类库中的Java SE API子集+Java虚拟机。
按照技术所服务的领域,分为以下四个平台:
- Java Card:小内存设备;
- Java ME:移动终端;
- Java SE:桌面级应用,提供完整的Java核心API;
- Java EE:企业应用,除了提供Java SE API外,还对其做了大量的扩充。
二. Java内存模型
2.1.方法区
也称"永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
- 运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
2.2.虚拟机栈
描述的是java 方法执行的内存模型:每个方法被执行的时候都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
2.3.本地方法栈
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
2.4.堆
也叫做java 堆、GC堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。
- 新生代:
程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。
- 老年代:
用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,切数组中无引用外部对象。
老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
2.5.程序计数器
是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
2.6.直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
三. 对象的内存布局与创建
3.1内存布局
分为3个区域:
- 对象头:哈希码、GC分代年龄、线程持有的锁、类元数据的指针等;
- 实例数据:真正存储的在效信息,即各字段内容,包括父类继承来的;
- 对齐填充:保证该对象8B对齐。
3.2 创建
虚拟机遇到new指令时,将进行以下操作:
- 检查该类是否已被加载,没有则加载;
- 确定所需内存大小,根据内存分配算法在Java堆里分配一块内存(如果启动TLAB,则在所属线程的Java堆里分配),并初始化为零值;
- 设置对象头,如哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等。
- 执行<init>,初始化字段等。
四. 内存分配与垃圾回收策略
4.1. 内存分配策略
- 对象优先在Eden分配;
- 大对象直接进入老年代;
- 长期存活的对象将进入老年代;
- 动态对象年龄判断;
- 空间分配担保。
4.2. 对象存亡判断
- 引用计数法:循环引用的问题。
- 可达性分析法:GC Roots链。
4.3. 垃圾回收策略
- 标记-清除法
- 复制法
- 标记-整理法
- 分代收集法
五. 类文件格式
Class类文件严格按照顺序紧凑的排列,由无符号数和表构成,表是由多个无符号数或其他数据项构成的符合数据结构。
Class类文件格式按如下顺序排列:
类型 | 名称 | 数量 |
u4 | magic(魔术) | 1 |
u2 | minor_version(次版本号) | 1 |
u2 | major_version(主版本号) | 1 |
u2 | constant_pool_count(常量个数) | 1 |
cp_info | constant_pool(常量池表) | constant_pool_count-1 |
u2 | access_flags(类的访问控制权限) | 1 |
u2 | this_class(类名) | 1 |
u2 | super_class(父类名) | 1 |
u2 | interfaces_count(接口个数) | 1 |
u2 | interfaces(接口名) | interfaces_count |
u2 | fields_count(域个数) | 1 |
field_info | fields(域的表) | fields_count |
u2 | methods_count(方法的个数) | 1 |
method_info | methods(方法表) | methods_count |
u2 | attributes_count(附加属性的个数) | 1 |
attribute_info | attributes(附加属性的表) | attributes_count |
六. 类加载机制
6.1. 类加载过程
类从被加载到虚拟机内存开始,到制裁出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。
- 加载:类的全限定名-->二进制字节流-->方法区的Class对象(按JVM规范);
- 验证:确保字节流符合JVM规范;
- 准备:在常量区为类变量分配内存(final变量直接初始化,static变量先设为零值);
- 解析:将虚拟机常量池中的符号引用转换为直接引用;
- 初始化:通过<cinit>初始化类静态变量(包括static块)。
6.2. 双亲委派模型
- 启动类加载器:负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。)
- 扩展类加载器:负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器:负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。
其加载过程如下:
- 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成;
- 每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器;
- 如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。
双亲委派模式的类加载机制的优点:是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。