类的加载机制

目录介绍

  • 01.Java对象的建立过程php

    • 1.0 看下建立类加载过程
    • 1.1 对象的建立
    • 1.2 对象的内存布局
  • 02.Java内存区域java

    • 2.0 运行时数据区域
    • 2.1 程序计数器
    • 2.2 虚拟机栈
    • 2.3 本地方法栈
    • 2.4 Java堆
    • 2.5 方法区
    • 2.6 运行时常量池
    • 2.7 直接内存
  • 03.Java对象的访问定位方式git

    • 3.1 句柄
    • 3.2 直接指针
  • 04.Java对象销毁分析程序员

    • 4.1 JVM内存分配与回收
    • 4.2 判断对象是否死亡
    • 4.3 不可达的对象并不是“非死不可”
    • 4.4 如何判断一个常量是废弃常量
    • 4.5 如何判断一个类是无用的类
    • 4.6 GC回收算法详解
  • 05.String类和常量池github

    • 5.1 String对象的两种建立方式
    • 5.2 String类型的常量池

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深刻知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,固然也在工做之余收集了大量的面试题,长期更新维护而且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
  • 连接地址:https://github.com/yangchong2...
  • 若是以为好,能够star一下,谢谢!固然也欢迎提出建议,万事起于忽微,量变引发质变!

问题思考答疑

  • 说一下建立一个对象,类的加载过程。类信息,常量,变量,方法分别放到内存中哪里?
  • 对于运行时数据区域,哪些是私有的,哪些是共享的,为何要这样设计?
  • 程序计数器会出现OOM吗?它的生命周期是怎么样的?
  • 本地方法栈和Java虚拟机栈有什么区别?本地方法栈在什么状况下会形成OOM?
  • java堆主要是作什么做用的?
  • 什么是类的加载检查,主要检查什么,如何检查呢?
  • Java对象访问定位方式有哪些?主要有什么区别?为何说使用指针效率更高?
  • String类能够new吗?直接new和赋值的内容有什么区别,分别放在内存中什么地方?
  • 如何判断对象是否死亡(两种方法)。若是有不一样方法,那么之间有什么区别?
  • 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
  • 如何判断一个常量是废弃常量,如何判断一个类是无用的类?
  • 垃圾收集有哪些算法,各自的特色?常见的垃圾回收器有那些?
  • HotSpot为何要分为新生代和老年代?
  • 介绍一下CMS,G1收集器。Minor Gc和Full GC 有什么不一样呢?

01.Java对象的建立过程

1.1 看下建立类加载过程

  • Person p = new Person()请写一下类的加载过程?面试

    1).由于new用到了Person.class,因此会先找到Person.class文件,并加载到内存中;
    2).执行该类中的static代码块,若是有的话,给Person.class类进行初始化;
    3).在堆内存中开辟空间分配内存地址;
    4).在堆内存中创建对象的特有属性,并进行默认初始化;
    5).对属性进行显示初始化;
    6).对对象进行构造代码块初始化;
    7).对对象进行与之对应的构造函数进行初始化;
    8).将内存地址付给栈内存中的p变量

1.1 对象的建立

  • Java对象的建立过程,我建议最好是能默写出来,而且要掌握每一步在作什么。算法

    • 1.类加载检查
    • 2.分配内存
    • 3.初始化零值
    • 4.设置对象头
    • 5.执行init方法
  • ①类加载检查:segmentfault

    • 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,而且检查这个符号引用表明的类是否已被加载过、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
  • ②分配内存:缓存

    • 类加载检查经过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后即可肯定,为对象分配空间的任务等同于把一块肯定大小的内存从 Java 堆中划分出来。分配方式“指针碰撞”“空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
    • 内存分配的两种方式:安全

      • 选择以上两种方式中的哪种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",仍是"标记-整理"("标记-压缩"),值得注意的是,复制算法内存也是规整的
    • 内存分配并发问题

      • 在建立对象的时候有一个很重要的问题,就是线程安全,由于在实际开发过程当中,建立对象是很频繁的事情,做为虚拟机来讲,必需要保证线程是安全的,一般来说,虚拟机采用两种方式来保证线程安全:
    • CAS+失败重试:

      • CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操做的原子性。
    • TLAB:

      • 为每个线程预先在Eden区分配一起内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配
  • ③初始化零值:

    • 内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头),这一步操做保证了对象的实例字段在 Java 代码中能够不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • ④设置对象头:

    • 初始化零值完成以后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。
    • 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不一样,如是否启用偏向锁等,对象头会有不一样的设置方式。
  • ⑤执行 init 方法:

    • 在上面工做都完成以后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象建立才刚开始,<init> 方法尚未执行,全部的字段都还为零。因此通常来讲,执行 new 指令以后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算彻底产生出来。

1.2 对象的内存布局

  • 在 Hotspot 虚拟机中,对象在内存中的布局能够分为3快区域:对象头实例数据对齐填充
  • Hotspot虚拟机的对象头包括两部分信息第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是那个类的实例。
  • 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各类类型的字段内容。
  • 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位做用。
  • 由于Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。

02.Java内存区域

2.0 运行时数据区域

  • Java 虚拟机在执行 Java 程序的过程当中会把它管理的内存划分红若干个不一样的数据区域。
  • 这些组成部分一些事线程私有的,其余的则是线程共享的。

    • 线程私有的:

      • 程序计数器
      • 虚拟机栈
      • 本地方法栈
    • 线程共享的:

      • Java堆
      • 方法区
      • 运行时常量池
      • 直接内存
  • image

2.1 程序计数器

  • 程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是经过线程轮流切换并分配处理器时间来实现的,为了线程切换后能恢复到正确的位置,每条线程都须要一个独立的程序计数器,互不影响,该区域为“线程私有”。
  • 程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都须要依赖这个计数器来完。
  • 程序计数器主要有两个做用:

    • 1.字节码解释器经过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    • 2.在多线程的状况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候可以知道该线程上次运行到哪儿。
  • 注意:程序计数器是惟一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的建立而建立,随着线程的结束而死亡。

2.2 虚拟机栈

  • Java虚拟机栈:线程私有的,与线程生命周期相同,用于存储局部变量表,操做栈,方法返回值。局部变量表放着基本数据类型,还有对象的引用。
  • Java 内存能够粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是如今说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java虚拟机栈是由一个个栈帧组成,而每一个栈帧中都拥有:局部变量表、操做数栈、动态连接、方法出口信息。)
  • 局部变量表主要存放了编译器可知的各类数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不一样于对象自己,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或其余与此对象相关的位置)。
  • Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

    • StackOverFlowError: 若Java虚拟机栈的内存大小不容许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
    • OutOfMemoryError: 若Java虚拟机栈的内存大小容许动态扩展,且当线程请求栈时内存用完了,没法再动态扩展了,此时抛出OutOfMemoryError异常。
  • Java 虚拟机栈也是线程私有的,每一个线程都有各自的Java虚拟机栈,并且随着线程的建立而建立,随着线程的死亡而死亡。

2.3 本地方法栈

  • 本地方法栈:跟虚拟机栈很像, 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
  • 本地方法被执行的时候,在本地方法栈也会建立一个栈帧,用于存放该本地方法的局部变量表、操做数栈、动态连接、出口信息。
  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

2.4 Java堆

  • Java堆:全部线程共享的一块内存区域,此内存区域的惟一目的就是存放对象实例,对象实例几乎都在这分配内存。在虚拟机启动时建立。
  • Java 堆是垃圾收集器管理的主要区域,所以也被称做GC堆(Garbage Collected Heap).从垃圾回收的角度,因为如今收集器基本都采用分代垃圾收集算法,因此Java堆还能够细分为:新生代和老年代:在细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

    • image
  • 在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。

2.5 方法区

  • 方法区:各个线程共享的区域,储存虚拟机加载的类信息,常量,静态变量,编译后的代码。

    • 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
  • 相对而言,垃圾收集行为在这个区域是比较少出现的,但并不是数据进入方法区后就“永久存在”了。如何理解这句话?

2.6 运行时常量池

  • 运行时常量池:表明运行时每一个class文件中的常量表。包括几种常量:编译时的数字常量、方法或者域的引用。

    • 。Class 文件中包括类的版本、字段、方法、接口等描述信息
  • 既然运行时常量池时方法区的一部分,天然受到方法区内存的限制,当常量池没法再申请到内存时会抛出 OutOfMemoryError 异常。JDK1.7及以后版本的 JVM已经将运行时常量池从方法区中移了出来,在Java堆(Heap)中开辟了一块区域存放运行时常量池。

2.7 直接内存

  • 直接内存并非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用。并且也可能致使OutOfMemoryError异常出现。
  • JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)缓存区(Buffer) 的 I/O 方式,它能够直接使用Native函数库直接分配堆外内存,而后经过一个存储在 Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样就能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆之间来回复制数据
  • 本机直接内存的分配不会收到 Java 堆的限制,可是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

03.Java对象的访问定位方式

  • 创建对象就是为了使用对象,咱们的Java程序经过栈上的 reference 数据来操做堆上的具体对象。对象的访问方式有虚拟机实现而定
  • 目前主流的访问方式有

    • ①使用句柄
    • ②直接指针
  • 这两种对象访问方式各有优点。

    • 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 自己不须要修改。
    • 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

3.1 句柄

  • 若是使用句柄的话,那么Java堆中将会划分出一块内存来做为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

image

3.2 直接指针

  • 若是使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。

image

04.Java对象销毁分析

4.1 JVM内存分配与回收

  • Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 内存中对象的分配与回收。
  • JDK1.8以前的堆内存示意图:

    • image
    • 从上图能够看出堆内存的分为新生代、老年代和永久代。新生代又被进一步分为:Eden 区+Survior1 区+Survior2 区。值得注意的是,在JDK1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
  • 分代回收算法

    • 目前主流的垃圾收集器都会采用分代回收算法,所以须要将堆内存分为新生代和老年代,这样咱们就能够根据各个年代的特色选择合适的垃圾收集算法。
    • 大多数状况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
  • Minor Gc和Full GC 有什么不一样呢?

    • 新生代GC(Minor GC):指发生新生代的的垃圾收集动做,Minor GC很是频繁,回收速度通常也比较快。
    • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC常常会伴随至少一次的Minor GC(并不是绝对),Major GC的速度通常会比Minor GC的慢10倍以上。

4.2 判断对象是否死亡

  • 堆中几乎放着全部的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。

    • image
4.2.1 引用计数法
  • 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任什么时候候计数器为0的对象就是不可能再被使用的。

    • 这个方法实现简单,效率高,可是目前主流的虚拟机中并无选择这个算法来管理内存,其最主要的缘由是它很难解决对象之间相互循环引用的问题。
    • 所谓对象之间的相互引用问题,以下面代码所示:除了对象objA和objB相互引用着对方以外,这两个对象之间再无任何引用。可是他们由于互相引用对方,致使它们的引用计数器都不为0,因而引用计数算法没法通知 GC 回收器回收他们。
    public class Test {
        Object instance = null;
        public static void main(String[] args) {
            Test objA = new Test();
            Test objB = new Test();
            objA.instance = objB;
            objB.instance = objA;
            objA = null;
            objB = null;
        }
    }
4.2.2 可达性分析算法
  • 这个算法的基本思想就是经过一系列的称为 “GC Roots” 的对象做为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证实此对象是不可用的。

    • image
4.2.3 再谈引用
  • 不管是经过引用计数法判断对象引用数量,仍是经过可达性分析法判断对象的引用链是否可达,断定对象的存活都与“引用”有关。
  • JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
  • 关于四种引用以及源代码分析,能够看个人这篇文章:https://blog.csdn.net/m0_3770...

4.3 不可达的对象并不是“非死不可”

  • 即便在可达性分析法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过期,虚拟机将这两种状况视为没有必要执行。
  • 被断定为须要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象创建关联,不然就会被真的回收。

4.4 如何判断一个常量是废弃常量

4.5 如何判断一个类是无用的类

  • 方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?要断定一个类是不是“无用的类”的条件则相对苛刻许多。类须要同时知足下面3个条件才能算是 “无用的类”

    • 该类全部的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    • 加载该类的 ClassLoader 已经被回收。
    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。
  • 虚拟机能够对知足上述3个条件的无用类进行回收,这里说的仅仅是“能够”,而并非和对象同样不使用了就会必然被回收。

4.6 GC回收算法详解

05.String类和常量池

5.1 String对象的两种建立方式

  • 1 String 对象的两种建立方式:

    String str1 = "abcd";
    String str2 = new String("abcd");
    System.out.println(str1==str2);//false
  • 这两种不一样的建立方法是有差异的【记住:只要使用new方法,便须要建立新的对象】

    • 第一种方式是在常量池中拿对象
    • 第二种方式是直接在堆内存空间建立一个新的对象。
  • image

5.2 String类型的常量池

  • String 类型的常量池比较特殊。它的主要使用方法有两种:

    • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
    • 若是不是用双引号声明的 String 对象,可使用 String 提供的 intern 方String.intern() 是一个 Native 方法,它的做用是:若是运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;若是没有,则在常量池中建立与此 String 内容相同的字符串,并返回常量池中建立的字符串的引用。
String s1 = new String("yc");
String s2 = s1.intern();
String s3 = "yc";
System.out.println(s2);//yc
System.out.println(s1 == s2);//false,由于一个是堆内存中的String对象一个是常量池中的String对象,
System.out.println(s3 == s2);//true,由于两个都是常量池中的String对

关于其余内容介绍

01.关于博客汇总连接

02.关于个人博客

相关文章
相关标签/搜索