Java编程基础.md

运行时常量池

运行时常量池是方法区的一部分,Class 文件常量池存储了编译时的字面值常量和富豪引用,这部分内容将在类加载后放到运行时常量池

堆,栈,方法区,

  1. 是jvm 运行时内存区域中容量最大的一块内存空间
  2. 所有线程共享该空间,随着jvm启动而创建
  3. java中对象实例和数组分配在这里
  4. gc发生最频繁的一块内存空间,根据内存回收可划分为新生代
  5. 堆是由垃圾回收来负责的,堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行期动态分配内存,Java 的垃圾收集器会自动收走这些不再使用的数据。但确定是,由于要在运行时动态分配内存,存取速度较慢
  6. 当堆内存无法为对象实例分配内存空间,并且也无法进行空间扩展时抛出异常OutOfMemoryError

栈:

  1. Java 程序员常说的栈特指Java 虚拟机栈
  2. 基本数据类型以及对象实例和数组引用分配在java 虚拟机栈的局部变量表中(局部变量表以slot为最小单位,一个slot可以存在一个32位的数据,long和double 类型和需要两个连续的slot存储)
  3. 当一段代码块定义了一个变量,jvm在栈中为这个变量分配内存空间,当变量离开它的作用域后,jvm为自动释放为该变量分配的内存空间,该内存可以立即另作他用。
  4. Java 虚拟机栈是线程私有的,声明周期同线程
  5. 虚拟机描述的java 方法内存模型是这样的: 当一个方法执行的时候,会同时创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法返回地址等信息。每一个方法从调用到执行结束,都对应一个栈帧在虚拟机栈中入栈到出栈的过程。(在编译程序代码的时候,栈帧需要多大的局部变量表,需要多深的操作数栈都已确定。并且保存在方法表的code 属性中,需要分配多深内存,不会受到运行期的数据影响,而取决于具体的虚拟机实现)
  6. 如果线程请求的栈深度超过了虚拟机所允许的深度,则抛出StackOverFlowError; 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时,抛出OutOfMemoryError
  7. 在栈中定义的变量不存在默认值 初始值
int i = 1;
// 在栈中局部变量表分配一个slot, 存储一个int 类型的i
double d = 1.23;
// 在栈中的局部变量表分配两个连续slot, 存储double类型的d
Object obj = new Object();
//在堆中开辟一块空间,存放Object类型的对象,并在栈中的局部变量表分配1或者2个连续slot,存储堆中obj对象的引用。(对象的引用可能是32位也可能是64位)

方法区

  1. 同java 堆一样,是各个线程共享的一块内存区域,用来存储已经被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码
  2. 因为方法区是所有线程共享的,所以必须考虑数据的线程安全。假设两个线程都是找java 类, 当这个类没有被加载时,只能有一个线程去加载,而其他线程等待
  3. 方法区除了和java 堆一样不需要再物理上连续的内存,和可以设置固定大小和可扩展内存外,它还可以设置不实现垃圾收集。相对而言,GC在这个区域的收集行为很少,当并不意味着数据进入这个区域就长生不老,这个区域的内存回收目的是常量池的回收和类型的卸载,当方法区无法满足内存的分配需求时,抛出OutOfMemoryError 错误

类加载器

JVM必须直到一个类是启动加载器加载的还是用户加载器加载的,如果一个类型是用户加载器加载得,jvm 会把这个类加载器的一个引用作为类信息的一部分保存到方法区

两种方式获取Class实例:

  1. Class 类的静态方法,Class.forName() 通过类的全限定名称获取该类的Class实例
  2. 通过类对象实例的getClass()

程序计数器

是一块较小的内存空间。他可以看做是当前线程所执行的字节码信号指示器。字节码解释器工作就是通过改变这个计数器的值选择下一条需要执行的字节码指令,分支,循环,跳转,异常,线程恢复等基础功能都是依赖这个计数器

由于JVM虚拟器多线程是通过线程轮流切换处理器执行事件的方式实现的,在任何一个确定的时刻,一个处理器(一个内核)只会执行一条线程中的指令,当切换到另一条线程时,若不保存当前未执行完的线程的位置,下次处理器再执行这条线城时,又要重新开始执行。所以每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存为 " 线程私有 "内存

如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,这个计数器为undefined,

Java 虚拟机栈(就是一般说的栈)

与程序计数器一样也是线程私有的,它的生命周期与线程相同。每个方法在执行的同时都会创建一个栈帧,栈帧里面包含局部变量表,操作数表,动态链接,方法出口。每个方法从调用到结束这个过程,就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程

一般说的堆和栈中的栈, 就是虚拟机栈,或者说虚拟机栈中的栈帧的局部变量表部分。 局部变量表存在了编译器可知的各种基本数据类型,returnAddress 类型,对象引用(可能是指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)。

64位的long 和double 类型会占用2个局部变量空间,其余数据类型只会占用1个局部变量空间。局部变量表所需的内存 空间在编译期间完成内存分配,当进入一个方法时,这个方法需要在帧中分配多大的内存空间是完全正确的,在方法啊运行期间不会改变局部变量表的方法。

在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverFlowError异常(栈溢出),如果虚拟机栈可以动态扩展(现在大部分java 虚拟机都可以动态扩展,只不过java虚拟机规定中也允许固定长度的java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出OutOfMemoryError 异常(没有足够内存)

本地方法栈(为native 方法服务)

本地方法栈和虚拟机栈所发挥的作用是非常相似的,他们之间不过是虚拟机栈为虚拟机执行java 方法(字节码文件)服务,而本地方法栈为虚拟机使用到的本地Native方法服务,在虚拟机规范中对本地方法栈中使用的方法,语言,与数据结构没有强行规定,虚拟机可以自由实现它。(Sun HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一)。与虚拟机栈一样本地方法栈会抛出StackOverFlowError 和OutOfMemoryError 异常

如果本地方法接口是C链接模型的话,它的本地方法栈是c栈,当c程序调用一个c函数时,传递给该函数的参数以相应的顺序压入栈,它的返回值以确定的方式返回给调用方,这就是虚拟机实现中本地方法栈的行为。

可以这样理解:一个线程首先调用了两个java 方法,然后第二个java方法调用了本地方法,假设这是一个c栈,第一个c函数有调用了第二个c函数,第二个函数又调用了一个java方法,进入java栈,然后这个java方法又调用一个java方法。

由于局部变量表在虚拟机栈中,是线程私有的数据,所以无论读写两个连续的slot是否是原子性操作,都不会出现线程安全的问题。

虚拟机通过索引定位的方式定位局部变量表,索引的范围从0开始到局部变量表最大的slot数量。如果访问的是32位数据类型,索引n就代表使用了第n个slot;如果访问的是64位数据类型,索引n就代表使用了第n和n+1个slot。对于两个相邻的存放64位数据的slot,不能单独访问其中一个,java虚拟机规范中明确要求了如果遇到了这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。

动态连接

两个栈帧会有重叠的部分。 每一个栈帧内部包含一个指向运行时常量池的引用来支持当前方法的代码实现动态链接。在Class 文件里面,描述一了方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转化为实际方法的直接引用。

这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

方法返回地址

遇到一个方法返回的字节码指令, 方法执行过程中遇到了异常

无论采用何种退出方式,方法退出之后,都需要返回到方法被调用的位置。

对象大小分析

Java 对象的内存布局: 对象头, 实例数据, 对齐填充

对象头分为两部分: 第一部分存储对象自身的运行时的数据, 第二部分是类型指针

static class A {
    int a;
}
static class B{
    int a;
    int b;
}
public static void main(String[] args){
// 指针压缩:12B对象头+ 4B示例数据+0B的填充
// 指针不压缩: 16B对象头+ 4B实例数据 + 4B的填充
    new Integer(1);
 // 指针压缩:12B对象头+4B的实例数据+0B的填充=16B
 // 指针不压缩:16B对象头+4B的实例数据+4B的填充=24B
    new A();
    // 指针压缩:12B对象头+2*4B的实例数据=20B,填充之后=24B
    // 指针不压缩:16B对象头+2*4B的实例数据+0B的填充=24B
    new B();
    
    //  指针压缩:12B对象头+压缩情况下数组比普通对象多4B来存放长度+2*4B的int实例大小=24B
    //指针不压缩:16B对象头+未压缩情况下数组比普通对象多8B来存放长度+2*4B实例大小=32B
    new int[2];
    // 指针压缩:12B对象头+4B长度+3*4B的int实例大小=28B,填充4B =32B
    //指针不压缩:16B+8B+3*4B+4B填充=40B
    new int[3];
    // 指针压缩:12B对象头+4B长度+2*2B的实例大小=20B,填充4B=24B
    //指针不压缩:16B+8B+2*2B+4B填充=32B
    new char[2];
    // 指针压缩:12B对象头+4B长度+3*2B的实例大小+2B填充=24B
    // 指针不压缩:16B+8B+3*2B+2B填充=32B
    new char[3];
}

指针压缩:

MySizeOf.sizeOf(new String("a"))的大小=12B对象头+2*4B(成员变量hash和hash32)+4B(压缩的value指针)=24B
MySizeOf.fullSizeOf(new String("a"))的大小=12B对象头+2*4B(成员变量hash和hash32)+4B指针+(value数组的大小=12B对象头+4B数组长度+1*2B实例大小+6B填充=24B)=12B+8B+4B+24B=48B
(PS: new String("aa")new String("aaa")new String("aaaa")的fullSizeOf大小都为48B)
MySizeOf.fullSizeOf(new String("aaaaa"))的大小=12B+2*4B+4B+(12B+4B+5*2B+6B填充)=24B+32B=56B
MySizeOf.sizeOf(new String("a"))的大小=16B+2*4B+8B(位压缩的指针大小) =32B
MySizeOf.fullSizeOf(new String("a"))的大小=16B对象头+2*4B(成员变量hash和hash32)+8B指针+(value数组的大小=16B对象头+8B数组长度+1*2B实例大小+6B填充=32B)=32B+32B=64B
(PS: new String("aa")new String("aaa")new String("aaaa")的fullSizeOf大小都为64B)
MySizeOf.fullSizeOf(new String("aaaaa"))的大小=16B+2*4B+8B+(16B+8B+5*2B+6B填充)=32B+40B=72B

这些计算结果只会少不会多,因为在代码运行过程中,一些对象的头部会伸展,mark区域会引用一些外部的空间(轻量级锁,偏向锁,这里不展开),所以官方给出的说明也是,最少会占用多少字节,绝对不会说只占用多少字节。

我们来看一下new String() 大小计算:

  1. 指针压缩:
12B对象头+2*4B实例变量+4B指针+12B对象头+4B数组长度大小+0B实例大小)=24B+16B=40B
  1. 指针不压缩
16B+2*4B+8B指针+16B+8B数组长度大小+0B)=32B+24B=56B

一个空String对象堆笑40Bytes

Jvm的内存区域有5大块

  1. 程序计数器:县城私有,用于记录,当前线程所执行class 字节码的行号计数器
  2. Java 虚拟机栈: 县城私有,指Java方法执行的内存模型
  3. 本地方法栈: 线程私有,指Java本地方法执行的内存模型
  4. Java 堆: 线程公有, 用于存储所有对象实例和数组 (要回收)
  5. 方法区: 线程公有,用于存储编译后的类信息(Class 类),常量,静态变量和即时编译后的数据 (要回收)

GC Roots 的对象包括下面几种

  1. 虚拟机栈或本地方法栈中引用的对象
  2. 方法区中常量或类静态属性引用的对象

GC的几张算法:

通过称为 “GC Root”的对象作为起点,点向下搜索,搜索所走过的路径称为引用链,但一个对象到GC Root 没有任何引用链相连的话,则证明对象是不可用的。

标记-清除算法: 首先根据可达性分析标记处需要回收的对象;之后再统一回收所有被标记的对象

复制算法: 暂停虚拟机,将内存中存活的对象拷贝到另一块内存中,然后一次性回收老的内存空间。

复制算法变体算法: 内存不是两块,而是八块。

前期绑定和后期绑定

前期绑定是编译期就能确定的类型,后期绑定是在运行期才确定类型

static final

static 变量如果没有初值,有默认值,final 变量必须赋初值

for循环,定义变量

for(int i=5, j=10; i<10; i++){ } 此语句不会发生编译错误。


int i;

for(i=5, int j=10; i<10;i++, j++){ } 此语句将发生编译错误。

接口中的数据类型和方法类型

默认数据类型等价于 public static final , 方法默认是public abstract

byte short char int 运算

		byte b1 = 1, b2 = 2, b3;
        b1++;  //  不错
        b3 = b1 + b2;  //错

        short s1 = 1, s2 = 2, s3;
        s1++;//  不错
        s3 = s1 + s2;  //错

        char c1 = 1, c2 = 2, c3;
        c1++;//  不错
        c3 = c1 + c2;  //错
final byte b1 = 1, b2 = 2, b3;
b3= b1+b2; // 不错

使用匿名内部类创建线程

new Thread(new Runnable() {
			
			@Override
			public void run() {
				int i=0;
				while(true){
					i++;
					System.out.println("this is 线程"+i);
				}
				
				
			}
		}).start();

Integer.

integer a = 100;
integer b = 100;
System.out.println("a==b"); // true

Integer.valueOf() 有一个内部类,类中有一个缓存数组,存放着-128,到127的整数。

向上转型和向下转型

Father f1 = new Son(); // 这就叫 upcasting (向上转型), 父类引用指向子类对象,父类引用调用的变量只能是父类中的方法,但还是默认调用子类对象的方法

Son s1 = (Son) f1; // 这就叫 downcasting (向下转型),

JVM内存

堆内存分为两个不同的区域: 新生代,老年代

新生代: Eden, From Survivor, To Survivor

新生代: 主要用来存放新生的对象

老年代:主要存放应用程序生命周期长的内存对象

新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。 Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

常用GC算法

  1. 引用计数法
    • 每个对象赋值时都要维护引用计数器,而计数器本身有一定损耗
    • 较难处理循环引用
  2. 跟踪() 具体方法1. 复制 2. 标记-清除 3. 标记-压缩

复制: 原理从根集合开始,通过tracing从from内存块中寻找存活的对象,拷贝到To内存块中,然后From 和To交换身份,下次内存分配从To开始

标记-清除: 原理 1.标记 从根集合开始扫描,对存活的对象进行标记。2. 清除扫描整个内存空间,回收未被标记的对象,(两次扫描,耗时严重,会与内存碎片)

标记-压缩:原理1. 标记: 从根集合开始扫描,对存活的对象进行标记。压缩: 再次扫描,并往一端滑动存活对象。

HotSpot内存管理

经研究: 不同对象的生命周期不同,98%的对象是临时对象,根据各代特点应用不同GC算法,提高GC效率

gc类型:

  • Minor GC 针对新生代的GC
  • Major GC 针对旧生代的GC
  • Full GC 针对永久代,新生代,旧生代三者的GC

新生代:

  • 由Eden,两块相同大小的Survivor(又称为from/to)构成,to总为空
  • 一般在Eden分配对象
  • 保存80% 90%生命周期较短的对象,GC频率高,采用效率较高的 复制 算法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值