jvm内存管理和类加载学习(大纲)

jvm内存模型

Java虚拟机在执行java程序的过程中会把其所管理的内存区域划分为若干个不同的数据区域

jvm内存模型.png

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内存模型.png

jvm内存模型与linux进程

JVM本质就是一个进程,因此其内存模型也有进程的一般特点。但是,JVM又不是一个普通的进程,其在内存模型上有许多崭新的特点,主要原因有两 个:1.JVM将许多本来属于操作系统管理范畴的东西,移植到了JVM内部,目的在于减少系统调用的次数;2. Java NIO,目的在于减少用于读写IO的系统调用的开销。 JVM进程与普通进程内存模型比较如下图:


linux与jvm.jpg

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>方法:

  1. 该类既没有声明任何类变量,也没有静态初始化语句;
  2. 该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;
  3. 该类仅包含静态 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

https://www.cnblogs.com/heavenhome/articles/6364713.html

https://www.jianshu.com/p/8a14ed0ed1e9

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容