Java 代码执行流程主要划分为以下5个步骤:编辑源代码、编译生成class文件、加载class文件、运行class文件、垃圾回收。
一个类文件加载到方法区,一些符号引用被解析(静态解析:如class文件的常量池被加载到方法区运行时常量池,各种其他静态存储结构被加载为方法区运行时数据结构等等)为直接引用或等到运行时分派(动态绑定),经过一系列加载过程后,程序可以通过Class对象方法方法区各种类型数据。
当前正在运行的方法的栈帧位于栈顶。若当前方法返回,则当前方法对应的栈帧出栈;当前方法的方法体中若是调用了其他方法,则为被调用方法创建栈帧,并将其压入栈顶。
Java内存结构描述的是Java程序执行过程中, 由JVM管理的不同的数据区域。包括以下5部分:
堆内存(heap)、方法区(method)、程序计数器、栈内存(stack)、本地方法栈(java中JNI调用)
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)
字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等。
符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
(1)类和接口的全限定名
(2)字段名称和描述符
(3)方法名称和描述符
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
1.虚拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用;
2.检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有,那必须先执行响应的类加载过程;
3.在类加载检查功通过后,为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。
分为3个区域:对象头,实例数据,对齐填充。
对象头:
包括两部分信息,第一部分:对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32 bit和64 bit,官方称它为“Mark Word”。
第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那在对象头中还必须有一块用于记录数组长度的数据。
实例数据:
是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
对齐填充:
对齐填充不是必然存在的。HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要通过对其补充来补全了。
Java程序需要通过栈上了reference数据来操作堆上的具体对象。
目前主流的访问方式有使用句柄和直接指针两种。
句柄访问:
Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对实例数据与类型数据的各自具体的地址信息。
直接指针访问:
reference中存储的直接就是对象地址。
Java的编译器在编译java类文件时,会将原有的文本文件(.java)翻译成二进制的字节码,并将这些字节码存储在.class文件。
也就是说java类文件中的属性、方法,以及类中的常量信息,都会被分别存储在.class文件中。当然还会添加一个公有的静态常量属性.class,这个属性记录了类的相关信息,即类型信息,是Class类的一个实例。
class文件存在的意义就是:跨平台。各种不同平台的虚拟机都统一使用这种相同的程序存储格式。不同平台的JVM运行相同的.class文件。
Java为什么能跨平台运行?
因为每个平台都拥有自己的JVM。Java编译器会先将Java代码编译成二进制字节码的class文件,并使用不同平台的JVM解释运行这些字节码文件。因此Java语言能跨平台运行。
把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点来实现的。
加载,验证,准备,解析,初始化,使用和卸载。其中验证,准备,解析3个部分统称为连接。
这7个阶段发生顺序如下图:
加载,验证,准备,初始化,卸载这5个阶段的顺序是确定的,而解析阶段则不一定:它在某些情况下可以在初始化完成后在开始,这是为了支持Java语言的运行时绑定。
其中加载,验证,准备,解析及初始化是属于类加载机制中的步骤。注意此处的加载不等同于类加载。
①.遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段的时候(被final修饰,已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。
②.使用java.lang.reflect包的方法对类进行反射调用的时候。
③.当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先出发父类的初始化。
④.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
⑤.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出发初始化。
加载:
①.通过一个类的全限定名来获取定义此类的二进制字节流
②.将这个字节流所代表的静态存储结构转换为方法区内的运行时数据结构
③.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:
是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
包含四个阶段的校验动作
a.文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
b.元数据验证
对类的元数据信息进行语义校验,是否不存在不符合Java语言规范的元数据信息
c.字节码验证
最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
d.符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在连接的第三个阶段——解析阶段中发生。
符号验证的目的是确保解析动作能正常进行。
准备:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中分配。只包括类变量。初始值“通常情况”下是数据类型的零值。
“特殊情况”下,如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量的值就会被初始化为ConstantValue属性所指定的值。
解析:
虚拟机将常量池内的符号引用替换为直接引用的过程。
“动态解析”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。
初始化:
类加载过程中的最后一步。
初始化阶段是执行类构造器()方法的过程。
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
()与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。
简单地说,初始化就是对类变量进行赋值及执行静态代码块。
通过上述的了解,我们已经知道了类加载机制的大概流程及各个部分的功能。其中加载部分的功能是将类的class文件读入内存,并为之创建一个java.lang.Class对象。这部分功能就是由类加载器来实现的。
不同的类加载器负责加载不同的类。主要分为两类。
启动类加载器(Bootstrap ClassLoader): 由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中,即负责加载Java的核心类。
其他类加载器: 由Java语言实现,继承自抽象类ClassLoader。如:
扩展类加载器(Extension ClassLoader): 负责加载\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库,即负责加载Java扩展的核心类之外的类。
应用程序类加载器(Application ClassLoader): 负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器,通过ClassLoader.getSystemClassLoader()方法直接获取。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
以上2大类,3小类类加载器基本上负责了所有Java类的加载。下面我们来具体了解上述几个类加载器实现类加载过程时相互配合协作的流程。
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
这样的好处是不同层次的类加载器具有不同优先级,比如所有Java对象的超级父类java.lang.Object,位于rt.jar,无论哪个类加载器加载该类,最终都是由启动类加载器进行加载,保证安全。即使用户自己编写一个java.lang.Object类并放入程序中,虽能正常编译,但不会被加载运行,保证不会出现混乱。
代码实现:ClassLoader中loadClass方法实现了双亲委派模型
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果该类没有加载,则进入该分支
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
long t1 = System.nanoTime();
c = findClass(name); //用户可通过覆写该方法,来自定义类加载器
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
整个流程大致如下:
a.首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
b.如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
c.如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载(自定义加载器)。
public class abc_test {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyObject object1=new MyObject(); // object1 引用计数 1
MyObject object2=new MyObject(); // object2 引用计数 1
object1.object=object2; // object1 引用计数 2
object2.object=object1; // object2 引用计数 2
object1=null; // object1 引用计数 1 ,无法回收
object2=null; // object2 引用计数 1 ,无法回收
}
}
Object obj = new Object();
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
若有效内存空间耗尽,JVM会停止应用程序的运行并开启GC线程,再开始标记清除
防止产生新对象,新引用关系。导致标记时遍历所有对象时结果不准确(使存活对象也被垃圾回收)
(1)Java垃圾回收
程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终导致内存溢出。为了让程序员更专注于代码的实现,而不用过多考虑内存释放的问题(内存的释放由系统自动识别完成)。Java语言中有了自动的垃圾回收机制,即GC回收机制。
(2)JVM堆内存结构(分代模型)
(2.1)jdk1.7 堆内存模型
(2.2)jdk1.8 堆内存模型
由上图可以看出,jdk 1.8 的内存模型由2部分组成,年轻代+老年代。
年轻代:Eden + 2*Survivor
年老代:Old
在jdk 1.8 中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。其中,Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中。(防止由于永久代内存经常不够用或发生内存泄露,改用本地内存空间)
(3)GC回收算法(分代算法)
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC和Full GC。
深入理解Java 垃圾收集器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
// 设置堆的初始和最大内存为16M
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m
-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xms16m -Xmx16m
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m
年轻代默认使用ParNew收集器,老年代使用CMS收集器。
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails -Xms16m -Xmx16m
G1垃圾收集器的原理是:
相比其他收集器而言,最大的区别在于G1垃圾收集器取消了年轻代、老年代的物理划分,取而代之将堆划分为若干个区域(Regin),这些区域包含了有逻辑上的年轻代、老年代区域。
每个区域被标记了Eden、Survivor、Old和Humongous,在运行时充当相应的角色。H代表Humongous,用于存放占用空间超过分区容量50%以上的大对象。
每个Regin都有一个RememberSet,用来记录该Regin对象的引用对象所在Regin。通过使用Remember Set,在做可达性分析时可以避免全堆扫描。
G1取消了新生代、老年代物理空间的划分。这样我们再也不用单独地对每个代的空间进行设置,也不用担心每个代地内存是否足够。
G1垃圾收集器提供三种垃圾回收模式:
// 当老年代大小占整个堆大小百分比达到 80% 时,触发一次mixed gc
-XX:InitiatingHeapOccupancyPercent = 80
Mixed GC执行过程分为以下几个步骤:
(1)初始标记 initial mark:STW,标记GC Roots直接关联对象
(2)并发标记 concurrent marking:与应用程序并发执行,在整个堆中查找从初始标记衍生出的存活对象
(3)最终标记 Remark:STW,修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
(4)清除垃圾 Cleanup:STW,采用复制算法进行垃圾回收,将一部分Regin里的存活对象复制到另一部分Regin中。
// 通过类名直接访问静态变量/方法
ClassName.propertyName
ClassName.methodName(……)
static方法
static方法一般称作静态方法,静态方法不依赖于任何对象就可以进行访问。因此静态方法不使用this,且在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为静态方法独立于任何对象实例,非静态成员方法/变量都是必须依赖具体的对象才能够被调用,因此:
(1)静态方法仅能调用其他static方法
(2)静态方法仅能访问static数据
(3)静态方法不能引用this或super
static变量
静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且JVM也只会为它分配一次内存,可以直接通过类名来访问它,同时类所有的实例都共享静态变量,所有实例的引用都指向同一个地方,任何一个实例对其的修改都会导致其他实例的变化。但是实例变量则不同,它是伴随着实例的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。
存放位置 | 生命周期 | |
---|---|---|
实例变量/方法 | 随着对象的创建存放于堆内存中 | 随对象的消失而消失 |
静态(类)变量/方法 | 随着类的加载存放于方法区 | 生命周期最长,随类的消失而消失 |