凌晨四点的洛杉矶


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

类的加载机制和对象的创建

发表于 2019-02-18 | 分类于 java面试准备 , jvm |

JVM类加载机制

  • 定义:在代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制。

  • Class文件中的“类”从加载到JVM内存中,到卸载出内存过程有七个生命周期阶段。类加载机制包括了前五个阶段。如下图所示:jvm1

  • 过程:

    1. 类的加载:我们平常说的加载大多不是指的类加载机制,只是类加载机制中的第一步加载。在这个阶段,JVM主要完成三件事:

      1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。

      2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

      3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

    2. 类的连接:类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。

      1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。

      2、准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。对于非静态的变量,则不会为它们分配内存。

      如static int a = 100,静态变量a就会在准备阶段被赋默认值0。

      对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。

      另外,静态常量(static final filed)会在准备阶段赋程序设定的初值。

      如static final int a = 666,静态常量a就会在准备阶段被直接赋值为666;对于静态变量,这个操作是在初始化阶段进行的。

      3、解析:将类的二进制数据中的符号引用换为直接引用。

    3. 类的初始化:初始化阶段是根据用户程序中的初始化语句为类的静态变量赋予正确的初始值。这里初始化执行逻辑最终会体现在类构造器方法clinit方法中。该方法由编译器在编译阶段生成,它封装了两部分内容:静态变量的初始化语句和静态语句块。

      类的初始化的主要工作是为静态变量赋程序设定的初值。

      如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。

      1. 初始化执行时机:jvm规范明确规定了初始化执行条件,只要满足以下四个条件之一,就会执行初始化工作

        (1) 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法(对应new,getstatic,putstatic,invokespecial这四条字节码指令)。

        (2) 通过反射方式执行以上行为时。

        (3) 初始化子类的时候,会触发父类的初始化。

        (4) 作为程序入口直接运行时的主类。

      2. 初始化过程:初始化过程包括两步:

        (1) 如果类存在直接父类,并且父类没有被初始化则对直接父类进行初始化。

        (2) 如果类当前存在clinit方法,则执行clinit方法。

        需要注意的是接口(interface)的初始化并不要求先初始化它的父接口,只有当使用父接口的变量的时候才会进行初始化。(接口中不能有static块,但可以有变量初始化)

      3. clinit方法存在的条件:并不是每个类都有clinit方法,如下情况下不会有clinit方法:

        a. 类没有静态变量也没有静态语句块

        b.类中虽然定义了静态变量,但是没有给出明确的初始化语句。

        c.如果类中仅包含了final static的静态变量的初始化语句,而且初始化语句采用编译时常量表达时,也不会有clinit方法。

      类的主动引用和被动引用的区别

      1. 类的主动引用(一定会发生类的初始化)

        —— new一个类的对象

        —— 调用类的静态成员(除了final常量)和静态方法

        —— 使用java.lang.reflect包的方法对类进行反射调用

        —— 当虚拟机启动,先启动main方法所在的类

        —— 当初始化一个类,如果父类没有被初始化,则先初始化它的父类

      2. 类的被动引用(不会发生类的初始化)

        —— 当访问一个静态域时,只有真正声明这个域的类才会被初始化

        ​ 通过子类引用父类的静态变量,不会导致子类初始化

        —— 通过数组定义类引用,不会触发此类的初始化

        —— 引用final常量不会触发此类的初始化(常量在编译阶段就存入类的常量池中)

  • 类加载器:类加载器的作用不仅仅是实现类的加载,它还与类的的“相等”判定有关,关系着Java“相等”判定方法的返回结果,只有在满足如下三个类“相等”判定条件,才能判定两个类相等:

    1、两个类来自同一个Class文件

    2、两个类是由同一个虚拟机加载

    3、两个类是由同一个类加载器加载

    JVM类加载器分类详解:

    1、Bootstrap ClassLoader:启动类加载器,也叫根类加载器,它负责加载Java的核心类库,加载如(%JAVA_HOME%/lib)目录下的rt.jar(包含System、String这样的核心类)这样的核心类库。根类加载器非常特殊,它不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的。

    2、Extension ClassLoader:扩展类加载器,它负责加载扩展目录(%JAVA_HOME%/jre/lib/ext)下的jar包,用户可以把自己开发的类打包成jar包放在这个目录下即可扩展核心类以外的新功能。

    3、System ClassLoader\APP ClassLoader:系统类加载器或称为应用程序类加载器,是加载CLASSPATH环境变量所指定的jar包与类路径。一般来说,用户自定义的类就是由APP ClassLoader加载的。

    jvm3

    各种类加载器间关系:参考ClassLoader源代码会发现,这些Class之间并不是采用继承的方式实现父子关系,而是采用组合方式。

  • 类加载器的双亲委派加载机制:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

    jvm2

    双亲委派模型的源码实现:主要体现在ClassLoader的loadClass()方法中,思路很简单:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
    }
    protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
    try {
    if (parent != null) {
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }
    if (c == null) {
    // If still not found, then invoke findClass in order
    // to find the class.
    c = findClass(name);
    }
    }
    if (resolve) {
    resolveClass(c);
    }
    return c;
    }

对象的创建、内存布局和访问定位

对象的创建

jvm4

jvm5

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

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

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的

jvm5

内存分配并发问题:(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配

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

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

⑤执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。

对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

jvm4

对象的访问定位

建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:

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

    jvm6

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

    jvm7

两种访问方式的比较:这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

jvm内存结构和GC机制

发表于 2019-02-15 | 分类于 java面试准备 , jvm |

java内存区域,也称运行时数据区

jvm2

jvm1

线程私有:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 堆
  • 方法区

JVM会分配一个运行时内存空间。包括5大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。线程私有的空间,会随着线程而生,随着线程而亡。这3个区域内存分配和回收都是确定了的,无需考虑内存回收的问题。但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的是这部分内存。

  1. 程序计数器:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。

    另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    从上面的介绍中我们知道程序计数器主要有两个作用:

    1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

    注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

  2. 虚拟机栈:与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 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虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

  3. 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

    本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

    方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

  4. 堆:Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

    Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

    下图是JDK1.7版本时的堆空间的内存分配示意图:

    jvm3

  5. 方法区:方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

    HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。

    相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

  6. 常量池:Java中有三种常量池,字符串常量池、class常量池、运行时常量池

    • 字符串常量池:

      在jdk1.6以及之前的版本,字符串常量池是放到方法区中的,在1.7及之后就被移到了堆中;在HotSpot VM里实现string pool功能的是一个StringTable类,它是一个Hash表,默认长度是1009;StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个个字符组成,放在了StringTable上。

      字符串常量池里放的是什么?

      在JDK6.0及之前的版本中,String Pool里放的都是字符串常量;在JDK7.0中,String Pool中可以存放放于堆内的字符串对象的引用。

    • class常量池:

      每一个Java类都会被编译成class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用,每个class文件都有一个class常量池。

      什么是字面量和符号引用:

      • 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
      • 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
    • 运行时常量池:

      运行时常量池存在于方法区中(JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。),也就是class常量池被加载到内存之后的版本。

      不同之处是:它的字面量可以动态的添加,符号引用可以被解析为直接引用。当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

      既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

    jvm4

GC机制

Java GC(Garbage Collection)垃圾回收机制,Java VM中,存在自动内存管理和垃圾清理机制。GC机制对JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,防止出现内存泄露和溢出问题。Java中不能显式分配和注销内存。有些开发者把对象设置为null或者调用System.gc()显式清理内存。设置为null至少没什么坏处,但是调用System.gc()会一定程度上影响系统性能。Java开发人员通常无须直接在程序代码中清理内存,而是由垃圾回收器自动寻找不必要的垃圾对象,并且清理掉它们。

Java GC主要做三件事:
(a)哪些内存需要GC?
(b)何时需要执行GC?
(c)以何策略执行GC?

  1. Java中什么哪些内存需要GC回收?
    JVM会分配一个运行时内存空间。包括5大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。其中程序计数器、虚拟机栈、本地方法栈是每个线程私有内存空间,随线程而生,随线程而亡。这3个区域内存分配和回收都是确定的,无需考虑内存回收的问题。
    但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的是这部分内存。GC主要进行回收的内存是JVM中的方法区和堆,涉及到多线程(指堆)、多个对该对象不同类型的引用(指方法区),才会涉及GC的回收。
    小结:Java GC针对的是JVM中堆和方法区。

  2. Java GC机制启动之前,需要确定堆内存中哪些对象是存活的,一般有两种方法:引用计数法和可达性分析法。
    引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。
    可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。通过称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索路径称为 “引用链(Reference Chain)”,以下对象可作为GC Roots:

    (a)、虚拟机栈(栈帧中的本地变量表)中引用的对象;

    (b)、方法区中的类静态变量引用的对象;

    (c)、方法区中常量引用的对象

    (d)、本地方法栈中JNI(即一般说的Native方法)中引用的对象

    小结:当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。

  3. 小结:Java GC垃圾回收机制,回收的是已死的Java对象(引用无法可达)。

  4. Java GC垃圾回收算法:

    • 标记-清除:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行标记和清除。

      标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有的GC Roots可达对象标记为存活的对象。

      清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

      之所以说它是最基础的内存回收算法,是因为后续的算法都是基于这种思路、并对其缺点进行改进而得到的。

      主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高(需要递归标记和全堆对象遍历清理);另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存时候,不得不提前触发另一次垃圾收集动作。

      jvm4

    • 标记-压缩:该算法与标记-清除算法类似,都是先对存活的对象进行标记,但是在清除后会把活的对象向左端空闲空间移动,然后再更新其引用对象的指针。

      jvm6

    • 复制:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

      这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

      jvm5

  5. Java GC的分代垃圾回收机制

    GC分代的假设:绝大部分对象的生命周期都非常短暂,存活时间短。

    “分代回收”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

新生代:

在新生代中,使用“停止-复制”算法进行内存清理。绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程称为“Minor GC” 。

老年代:

对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,这种情况下也叫Full GC。老年代存储的对象比年轻代多得多,而且不乏大对象。

对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。

JVM内存分配策略

  • 对象优先分配在Eden区
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 空间担保分配(老年代剩余空间要多于幸存区的一般,否则要进行Full GC)

小结:Java内存分配和回收机制是:分代分配,分代回收。新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中,其存活率较高、没有额外空间对它进行分配担保,就应该使用“标记-整理”或“标记-清理”算法进行回收。

新生代垃圾回收过程(复制)

基于大多数新生对象都会在GC中被收回的假设。新生代的GC 使用复制算法。在GC前To 幸存区(survivor)保持清空,对象保存在 Eden 和 From 幸存区(survivor)中。GC运行时,Eden中的幸存对象被复制到 To 幸存区(survivor),针对 From 幸存区(survivor)中的幸存对象,会考虑对象年龄,如果年龄没达到阀值(tenuring threshold),对象会被复制到To 幸存区(survivor),如果达到阀值对象被复制到老年代。复制阶段完成后,Eden 和From 幸存区中只保存死对象,可以视为清空。如果在复制过程中To 幸存区被填满了,剩余的对象会被复制到老年代中。最后 From 幸存区和 To幸存区会调换下名字,在下次GC时,To 幸存区会成为From 幸存区。

jvm7

上图演示GC过程,黄色表示死对象,绿色表示剩余空间,红色表示幸存对象

总结一下,对象一般出生在Eden区,年轻代GC过程中,对象在2个幸存区之间移动,如果对象存活到适当的年龄,会被移动到老年代。当对象在老年代死亡时,就需要更高级别的GC,更重量级的GC算法(复制算法不适用于老年代,因为没有多余的空间用于复制)

现在应该能理解为什么新生代大小非常重要了(译者,有另外一种说法:新生代大小并不重要,影响GC的因素主要是幸存对象的数量),如果新生代过小,会导致新生对象很快就晋升到老年代中,在老年代中对象很难被回收。如果新生代过大,会发生过多的复制过程。我们需要找到一个合适大小,不幸的是,要想获得一个合适的大小,只能通过不断的测试调优。

参考博客:

http://ifeve.com/useful-jvm-flags-part-5-young-generation-garbage-collection/

https://blog.csdn.net/anjoyandroid/article/details/78609971

https://blog.csdn.net/zhangphil/article/details/78260863

https://github.com/Snailclimb/JavaGuide/blob/master/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md

正则表达式

发表于 2019-02-09 | 分类于 java进阶 |

常用符号

字符类

类型 解释
[abc] a、b或c
[^abc] 任何字符,除了a、b或c
[a-zA-Z] a到z或A到Z,两头的字母包括在内
[a-d[m-p]] a到d或m到p(也可以写成[a-dm-p])
[a-z&&[^bc]] a到z除去b和c(也可以写成[ad-z])
[a-z&&[def]] d、e或f
[a-z&&[^m-p]] a到z除去m到p(也可以写成[a-lq-z])

预定义字符类

类型 解释
. 任何字符(与行结束符可能匹配也可能不匹配)
\d 数字([0-9])
\D 非数字([^0-9])
\s 空白字符([\t\n\x0B\f\r])
\S 非空白字符([^\s])
\w 单词字符([a-zA-Z_0-9])
\W 非单词字符([^\w])

边界匹配器

类型 解释
^ 行的开头
$ 行的结尾
\b 单词的边界
\B 非单词的边界

Greedy数量词

类型 解释
X? X,一次或一次也没有
X* X,零次或多次
X+ X,一次或多次
X{n} X,恰好 n 次
X{n,} X,至少 n 次
X{n,m} X,至少 n 次,但是不超过 m 次

元字符

元字符 举例
. 例如正则表达式r.t匹配这些字符串:rat、rut、r t,但是不匹配root。
$ 例如正则表达式weasel$,能够匹配字符串”He’s a weasel”的末尾,但是不能匹配字符串”They are a bunch of weasels.”
^ 匹配一行的开始。例如正则表达式^When in能够匹配字符串”When in the”的开始,但是不能匹配”What and When in the”
* 匹配0或多个正好在它之前的那个字符。例如正则表达式(.*)意味着能够匹配任意数量的任何字符。
\ 这个是用来转义用的。例如正则表达式\$被用来匹配美元符号,而不是行尾,类似的,正则表达式.用来匹配点字符,而不是任何字符的通配符。
将两个匹配条件进行逻辑“或”(or)运算
+ 匹配1或多个正好在它之前的那个字符。例如正则表达式9+匹配9、99、999等。
? 匹配0或1个正好在它之前的那个字符。
{i},{i,j} 例如正则表达式A[0-9]{3} 能够匹配字符”A”后面跟着正好3个数字字符的串,例如A123、A348等,但是不匹配A1234。而正则表达式[0-9]{4,6} 匹配连续的任意4个、5个或者6个数字字符。

正则表达式的() [] {}的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
() 是为了提取匹配的字符串。表达式中有几个()就有几个相应的匹配字符串。圆括号中的字符视为一个整体。

[]是定义匹配的字符范围。比如 [a-zA-Z0-9] 表示相应位置的字符要匹配英文字符和数字。

{}一般用来表示匹配的长度,比如 \s{3} 表示匹配三个空格,\s[1,3]表示匹配一到三个空格。

(0-9) 匹配 '0-9′ 本身。

[0-9]* 匹配数字(注意后面有*,可以为空)

[0-9]+ 匹配数字(注意后面有+,不可以为空),例如{1-9} 写法错误。

[0-9]{0,9} 表示长度为 0 到 9 的数字字符串。

version

发表于 2019-01-16 | 分类于 Git |

Version Control

初始化一个Git仓库

1
$ git init

添加文件到Git仓库

1
2
$ git add <file>
$ git commit -m "description"

git add可以反复多次使用,添加多个文件,git commit可以一次提交很多文件,-m后面输入的是本次提交说明。

添加全部修改到暂存区

1
git add -A .
  • git add -A表示添加所有内容
  • git add . 表示添加新文件和编辑过的文件不包括删除的文件
  • git add -u 表示添加编辑或者删除的文件,不包括新添加的文件。

查看工作区状态

1
$ git status

查看修改内容

  • git diff 查看工作区(work dict)和暂存区(stage)的区别
  • git diff --cached 查看暂存区(stage)和分支(master)的区别
  • git diff HEAD -- <file> 查看工作区和版本库里面最新版本的区别

版本回退

1
$ git reset --hard HEAD^

以上命令是返回上一个版本,在Git中,用HEAD表示当前版本,上一个版本就是HEAD^,上上一个版本是HEAD^^,往上100个版本写成HEAD~100。

回退指定版本号

1
$ git reset --hard commit_id

commit_id是版本号,是一个用SHA1计算出的序列

放弃暂存区修改

  1. 退回工作区
1
$ git reset HEAD <file>
  1. 撤销工作区的修改
1
$ git checkout -- <file>

Tip:

  1. 当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout -- <file>。
  2. 当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD <file>,就回到了第一步,第二步按第一步操作。
  3. 已经提交了不合适的修改到版本库时,想要撤销本次提交,进行版本回退,前提是没有推送到远程库。

很重要的git checkout注意点

git checkout -- file命令中的--很重要!很重要!很重要!,没有--,就变成了切换到另一个分支的命令

tag

发表于 2019-01-16 | 分类于 Git |

标签

tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。

新建一个标签

1
$ git tag <tagname>

命令git tag <tagname>用于新建一个标签,默认为HEAD,也可以指定一个commit id。

指定标签信息

you can use like this: git tag -a <tagname> -m "say something..."

1
$ git tag -a <tagname> -m <description> <branchname> or commit_id

PGP签名标签

you can use like this: git tag -s <tagname> -m "say something..."

1
$ git tag -s <tagname> -m <description> <branchname> or commit_id

查看所有标签

1
$ git tag

推送一个本地标签

1
$ git push origin <tagname>

推送全部未推送过的本地标签

1
$ git push origin --tags

删除一个本地标签

1
$ git tag -d <tagname>

删除一个远程标签

1
$ git push origin :refs/tags/<tagname>

other

发表于 2019-01-16 | 分类于 Git |

常用忽略文件

https://github.com/seeways/MyIgnore

配置别名

1
git config --global alias.<name> <git-name>

建议熟悉git命令后使用

1
2
3
4
5
# 设置别名
git config --global alias.unstage 'reset HEAD'

# 使用别名撤掉修改,实际执行git reset HEAD test.py
git unstage test.py

对长命令的别名尤其好用
git config –global alias.lg “log –color –graph –pretty=format:’%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset’ –abbrev-commit”

使用效果git lg

Gittwo

显示颜色

如果没有默认显示变更文件的颜色,推荐此选项

1
git config --global color.ui true

git config

  1. 配置Git的时候,加上–global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。
  2. 仓库的Git配置文件都在仓库.git/config文件中
  3. 全局的Git配置文件放在用户主目录下的.gitconfig中(隐藏文件)
  4. 如果想修改配置或者别名,直接修改或删掉即可

origin

发表于 2019-01-16 | 分类于 Git |

远程仓库

创建SSH Key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ssh-keygen -t rsa -C "youremail@example.com"

Generating public/private rsa key pair. # 生成密钥对
Enter file in which to save the key (/root/.ssh/id_rsa): # 保存路径
Enter passphrase (empty for no passphrase): # 密码,默认空
Enter same passphrase again: # 重复密码
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
92:41:73:6d:ba:03:bf:36:f8:ab:a2:90:0c:9c:a1:85 youremail@example.com
The key's randomart image is:
+--[ RSA 2048]----+
| o .. |
| . . o o |
|E.. . o |
|o.o .o. |
|oo ooS. |
|o. .+ |
|o. . o |
| . . . + |
| .. ..+oo |
+-----------------+

关联远程仓库

1
git remote add origin git@github.com:git_username/repository_name.git

添加后,远程库的名字就是origin

取消关联远程库

1
git remote remove origin

查看远程库

1
git remote

查看远程库详细信息

如果没有相关权限,则看不到相关地址信息

例如没有推送权限,则看不到push地址

1
git remote -v

推送到远程仓库

1
$ git push origin <branch-name>

-u 表示第一次推送master分支的所有内容,不过建议先clone在push,尽量避免此方法

1
$ git push -u origin <branch-name>

日常提交只需要使用git push即可

从远程克隆

1
$ git clone https://github.com/usern/repositoryname.git

日常获取只需要使用git pull即可

log

发表于 2019-01-16 | 分类于 Git |

查看

1
$ git log

简化版日志

1
$ git log --pretty=oneline

查看前N条

1
git log -n

变更日志

-n 同上,不加则显示全部

1
git log --stat -n

查看提交修改

查看某次commit做了哪些修改

1
git show <commit-hash-id>

退出log状态

有时候日志过长,可以按下英文q退出状态

查看提交历史和说明

1
$ git reflog

branch

发表于 2019-01-16 | 分类于 Git |

分支

常用分支命令

1
2
3
4
5
6
7
8
9
10
11
查看分支:git branch

创建分支:git branch <name>

切换分支:git checkout <name>

创建+切换分支:git checkout -b <name>

合并某分支到当前分支:git merge <name>

删除分支:git branch -d <name>

分支策略

在实际开发中,我们应该按照几个基本原则进行分支管理:

  1. master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活

  2. 在dev分支上进行开发,也就是说,dev分支是不稳定的,到版本发布时,再把dev分支合并到master上,在master分支发布版本

  3. 你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

  4. 如果有中间版本,比如测试版,预发布版,按照优先级和流程,从dev递归合并到master上。

  5. 合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。

创建分支

1
$ git branch <branchname>

查看分支

all branch

1
$ git branch

切换分支

1
$ git checkout <branchname>

创建+切换分支

1
$ git checkout -b <branchname>

合并某分支到当前分支

1
$ git merge <branchname>

删除分支

1
$ git branch -d <branchname>

强制删除

1
$ git branch -D <branchname>

查看分支合并图

当Git无法自动合并分支时,就必须首先解决冲突。 解决冲突后,才可以继续操作。

1
$ git log --graph

查看分支树(简版)

为什么要敲这么长的命令呢?因为树很长,而简版一眼可以看穿架构

1
git log --graph --pretty=oneline --abbrev-commit

普通模式合并分支:禁用no-ff

因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。

合并分支时,加上--no-ff参数就可以用普通模式合并,能看出来曾经做过合并,包含作者和时间戳等信息,而fast forward合并就看不出来曾经做过合并。

1
$ git merge --no-ff -m "description" <branchname>

保存工作现场

1
$ git stash

查看工作现场列表

1
$ git stash list

恢复工作现场

但是恢复后,stash内容并不删除,你需要手动删除

1
git stash apply

删除工作现场

1
git stash drop

恢复后自动删除工作现场

1
git stash pop

恢复指定的工作现场

1
git stash apply <stash version>

丢弃一个没有合并过的分支

1
$ git branch -D <branchname>

查看远程库信息

1
$ git remote -v

在本地创建和远程分支对应的分支

建议本地和远程分支的名称保持一致

1
$ git checkout -b branch-name origin/branch-name,

建立本地分支和远程分支的关联

1
$ git branch --set-upstream branch-name origin/branch-name;

从本地推送分支

如果推送失败,先用git pull抓取远程的新提交;

1
$ git push origin branch-name

从远程抓取分支

如果有冲突,要先处理冲突。

1
$ git pull

GitBase

发表于 2019-01-14 | 分类于 Git |

Git配置

1
2
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"

git config命令的--global参数,表明这台机器上的所有Git仓库都会使用这个配置,也可以对某个仓库指定不同的用户名和邮箱地址。

工作区、暂存区和版本库d的区别

  • 工作区:在电脑里能看到的目录;
  • 版本库:在工作区有一个隐藏目录.git,是Git的版本库。

Git的版本库中存了很多东西,其中最重要的就是称为stage(或者称为index)的暂存区,还有Git自动创建的master,以及指向master的指针HEAD。

Gitone

进一步解释一些命令:

  • git add实际上是把文件添加到暂存区
  • git commit实际上是把暂存区的所有内容提交到当前分支

    撤销修改

    丢弃工作区的修改

    1
    $ git checkout -- <file>

该命令是指将文件在工作区的修改全部撤销,这里有两种情况:

  1. 一种是file自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
  2. 一种是file已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。

总之,就是让这个文件回到最近一次git commit或git add时的状态。

克隆仓库

1
$ git clone https://github.com/usern/repositoryname.git

删除文件

1
$ git rm <file>

git rm <file>相当于执行

1
2
3
4
$ rm <file>
$ git add <file>
$ git commit -m "say something..."
$ git push(optional)

删除文件解释

Q:比如执行了rm text.txt 误删了怎么恢复?
A:执行git checkout -- text.txt 把版本库的东西重新写回工作区就行了
Q:如果执行了git rm text.txt我们会发现工作区的text.txt也删除了,怎么恢复?
A:先撤销暂存区修改,重新放回工作区,然后再从版本库写回到工作区

1
2
$ git reset head text.txt
$ git checkout -- text.txt

Q:如果真的想从版本库里面删除文件怎么做?
A:执行git commit -m "delete text.txt",提交后最新的版本库将不包含这个文件

1…34
FoBoHuang

FoBoHuang

乌云后面依然是灿烂的晴天。——朗弗罗

40 日志
12 分类
11 标签
GitHub
© 2019 FoBoHuang
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4