jvm内存模型
Java虚拟机在执行java程序的过程中会把其所管理的内存区域划分为若干个不同的数据区域
1.程序计数器
程序计数器是一块较小的空间,可以看成当前线程所执行的字节码的行号指示器,在虚拟机的概念模型中,字节码解释器工作通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能需要依赖这个计数器完成。
由于虚拟机的多线程是通过线程轮流切换分配处理器执行时间的方式来实现的,所以任何一个时刻,一个处理器都会执行一条线程中的指令。因此,为了切换线程能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程之间互不影响,独立存储,是一个线程私有的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行Natvie方法,这个计数器值则为空。
2.Java栈
Java栈,即Java虚拟机栈是线程私有的,他的生命周期和线程相同。Java栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.
局部变量表:存放了编译器可知的各种基本数据类型,对象引用类型(reference类型,不同于对象本身,他可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象句柄或者其他于此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
局部变量表的所需内存空间在编译期间完成分配,当进入一个方法时候,这个方法所需要分配的局部变量空间是完全确定的,方法运行时不会改变局部变量表的大小。
Java栈是为虚拟机执行Java方法(即字节码)服务。
3.本地方法栈
本地方法栈执行的是Native方法服务
4.Java堆
Java堆是被所有线程共享的一块内存区域。 堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 Java堆也是垃圾收集器管理的主要区域,即"GC堆"。
Java堆可以出于物理上不连续的内存空间中,逻辑上连续即可。
5.方法区
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
存储内容:类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息
运行时常量池 是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class常量池来说,更具有动态性,Java语言不要求常量一定只有在编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入常量池,如String.intern()方法。
jvm内存模型与linux进程
JVM本质就是一个进程,因此其内存模型也有进程的一般特点。但是,JVM又不是一个普通的进程,其在内存模型上有许多崭新的特点,主要原因有两 个:1.JVM将许多本来属于操作系统管理范畴的东西,移植到了JVM内部,目的在于减少系统调用的次数;2. Java NIO,目的在于减少用于读写IO的系统调用的开销。 JVM进程与普通进程内存模型比较如下图:
1.用户内存
上图特别强调了JVM进程模型的代码区和数据区指的是JVM自身的,而非Java程序的。普通进程栈区,在JVM一般仅仅用做线程栈。JVM的堆区和普通进程的差别是最大的,下面具体详细说明:
首先是永久代。永久代本质上是Java程序的代码区和数据区。Java程序中类(class),会被加载到整个区域的不同数据结构中去,包括常量 池、域、方法数据、方法体、构造函数、以及类中的专用方法、实例初始化、接口初始化等。这个区域对于操作系统来说,是堆的一个部分;而对于Java程序来 说,这是容纳程序本身及静态资源的空间,使得JVM能够解释执行Java程序。
其次是新生代和老年代。新生代和老年代才是Java程序真正使用的堆空间,主要用于内存对象的存储;但是其管理方式和普通进程有本质的区别。 普通进程在运行时给内存对象分配空间时,比如C++执行new操作时,会触发一次分配内存空间的系统调用,由操作系统的线程根据对象的大小分配好空间后返 回;同时,程序释放对象时,比如C++执行delete操作时,也会触发一次系统调用,通知操作系统对象所占用的空间已经可以回收。 JVM对内存的使用和一般进程不同。JVM向操作系统申请一整段内存区域(具体大小可以在JVM参数调节)作为Java程序的堆(分为新生代和老年代); 当Java程序申请内存空间,比如执行new操作,JVM将在这段空间中按所需大小分配给Java程序,并且Java程序不负责通知JVM何时可以释放这 个对象的空间,垃圾对象内存空间的回收由JVM进行。
JVM的内存管理方式的优点是显而易见的,包括:第一,减少系统调用的次数,JVM在给Java程序分配内存空间时不需要操作系统干预,仅仅在 Java堆大小变化时需要向操作系统申请内存或通知回收,而普通程序每次内存空间的分配回收都需要系统调用参与;第二,减少内存泄漏,普通程序没有(或者 没有及时)通知操作系统内存空间的释放是内存泄漏的重要原因之一,而由JVM统一管理,可以避免程序员带来的内存泄漏问题。
最后是未使用区,未使用区是分配新内存空间的预备区域。对于普通进程来说,这个区域被可用于堆和栈空间的申请及释放,每次堆内存分配都会使用这个区 域,因此大小变动频繁;对于JVM进程来说,调整堆大小及线程栈时会使用该区域,而堆大小一般较少调整,因此大小相对稳定。操作系统会动态调整这个区域的 大小,并且这个区域通常并没有被分配实际的物理内存,只是允许进程在这个区域申请堆或栈空间。
2.内核内存
应用程序通常不直接和内核内存打交道,内核内存由操作系统进行管理和使用;不过随着Linux对性能的关注及改进,一些新的特性使得应用程序可以使 用内核内存,或者是映射到内核空间。Java NIO正是在这种背景下诞生的,其充分利用了Linux系统的新特性,提升了Java程序的IO性能。
类加载机制
一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的 类加载机制 。
类加载触发时机
遇到 new getstatic putstatic 或 invokestatic 这四条字节码指令时, 如果类没有进行初始化, 则需要先触发其初始化;
使用 java.lang.reflect 包的方法对类进行反射调用的时候, 如果类没有进行过初始化, 则需要先触发其初始化;
当初始化一个类的时候, 若其父类还没有进行过初始化, 则需要先触发其父类的初始化(类与接口在该点上有区别);
虚拟机启动时, 用户指定一个要执行的主类(包含main() 方法的那个类), 虚拟机会先初始化这个主类;
若 java.lang.invoke.MethodHandle 实例最后的解析结果是
REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄, 并且该方法句柄所对应的类没有进行初始化, 则需要先触发其初始化;
一般,以上5种情况最常见的是前三种:实例化对象、读写静态对象、调用静态方法、反射机制调用类、调用子类触发父类初始化(其实最常见的就是触发了初始化的条件)
类加载的步骤
1.加载
双亲委派模型是指:当一个类加载器收到类加载请求时,不会直接加载这个类,而是把这个加载请求委派给自己父加载器去完成。如果父加载器无法加载时,子加载器才会去尝试加载。
采用双亲委派模型的原因:避免同一个类被多个类加载器重复加载
2:验证
确保class文件的二进制字节流中包含的信息符号虚拟机要求,包括:文件格式验证、元数据验证(数据语义分析)、字节码验证(数据流语义合法性)、符号引用验证(符号引用的匹配性校验,确保解析能正确执行)
3:准备
为类变量(静态变量)在方法区分配内存,并设置零值。注意:这里是类变量,不是实例变量,实例变量是对象分配到堆内存时根据运行时动态生成的。
4:解析
把常量池中的符号引用解析为直接引用:根据符号引用所作的描述,在内存中找到符合描述的目标并把目标指针指针返回。
5:初始化
真正开始执行Java程序代码,该步执行<clinit>方法根据代码赋值语句,对 类变量和其他资源 进行初始化赋值。
初始化与实例化-<clinit>与<init>
clinit
<clinit>方法 的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作)
<clinit>方法 的内容: 所有的类变量初始化语句和类型的静态初始化器
类的初始化时机: 即在java代码中首次主动使用的时候, 包含以下情形:
(首次)创建某个类的新实例时--new, 反射, 克隆 或 反序列化;
(首次)调用某个类的静态方法时;
(首次)使用某个类或接口的静态字段或对该字段(final 字段除外)赋值时;
(首次)调用java的某些反射方法时;
(首次)初始化某个类的子类时;
(首次)在虚拟机启动时某个含有 main() 方法的那个启动类
注意: 并非所有的类都会拥有一个<clinit>方法, 满足下列条件之一的类不会拥有<clinit>方法:
- 该类既没有声明任何类变量,也没有静态初始化语句;
- 该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;
- 该类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式;
clinit : 静态变量的初始化语句和静态代码块-类的初始化
init
<init>方法 的执行时期: 对象的初始化阶段
实例化一个类的四种途径:
1. 调用 new 操作符
2. 调用 Class 或 java.lang.reflect.Constructor 对象的newInstance()方法
3. 调用任何现有对象的clone()方法
4. 通过 java.io.ObjectInputStream 类的 getObject() 方法反序列化
init 更应该成为实例化,针对非静态的成员变量和构造代码块进行执行,然后再执行父类的构造函数-由父类到子类的顺序
区别:
<clinit>:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行
<init>:在实例创建出来的时候调用,包括调用new操作符;调用Class或Java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。
代码范例
public class A {
static {
System.out.println("A static");
}
B b = new B();
{
System.out.println("A not static");
}
A(){
System.out.println("A constructor");
}
public static void main(String[] args) {
System.out.println("Hello World!");
A m = new A();
}
}
class B{
static {
System.out.println("B static");
}
{
System.out.println("B not static");
}
B(){
System.out.println("B constructor");
}
}
结果输出如下:
A static
Hello World!
B static
B not static
B constructor
A not static
A constructor
因为有静态代码块,所有有<clinit>函数;当首次调用静态方法时 public static void main时会调用<clinit>
因此打出A static
Hello Word
然后调用A的<init>方法,针对非静态的成员变量和构造代码块进行执行(两者统计),然后再执行构造函数-由父类到子类的顺序
静态内部类延伸
静态内部类也可以用来实现单例,保证线程安全,效率高;其主要原理为:Java中静态内部类可以访问其外部类的静态成员属性,同时,1.静态内部类只有当被调用的时候才开始首次被加载(懒加载),2.利用了classloader的机制来保证初始化instance时只有一个线程(多个线程就不是静态类了),所以也是线程安全的,同时没有性能损耗(加synchronized同步锁)
与饿汉方式不同的地方在,饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化(相当于一个在static语句中初始化,相当于在<clinit>中初始化;另一个相当于在静态内部类的<clinit>中进行单例的初始化)
https://www.cnblogs.com/zhaoyan001/p/6365064.html
总结
关键点: 1.jvm内存模型管理与linux进程间的关系,jvm的代码区和数据区(方法区)是操作系统进程中堆的一部分
2.类加载的条件一般就是初始化的触发条件;类的初始化包括jvm调用<clinit>(如果有的话-静态代码块和静态变量初始化),以及实例化<init>(针对非静态的成员变量和构造代码块进行执行,然后再执行构造函数-由父类到子类的顺序)
参考:
https://my.oschina.net/u/3863980/blog/1839508