Java基础(十)

框架是个好东西,可早晚有一天会过时,这世界上就没有亘古不变的东西,来学下Java基础吧

JUC基础介绍

进程和线程

进程是程序的一次执行过程,是系统运行程序的基本单位

在Java中,当我们启动main函数的时候其实就启动了一个JVM的进程

线程和进程,但线程是比进程更小的执行单位,一个进程可以产生多个线程,与进程不同的是同类的多个线程可以共享进程的堆和方法区资源,每个线程都有自己的程序计数器,虚拟机栈和本地方法栈,系统在各个线程中切换的时候,压力要小得多,所以线程也被称为轻量级进程

Java运行程序只有两个原生线程:Main方法的主线程和JVM的垃圾回收线程

Java的线程创建是调用本地的C++的方法,因为Java的权限不够

Java程序天生就是多线程程序,通过JVM来查看下普通的Java程序有那几个线程

public static void main(String[] args) {
    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
    // 不需要获取同步的 monitor 和 synchronizer 信息
    ThreadInfo[] threadInfos = bean.dumpAllThreads(false, false);
    //        遍历输出
    for (ThreadInfo i : threadInfos) {
        System.out.println(i.getThreadId()+"  "+i.getThreadName());
    }
}

<<<1  main
<<<2  Reference Handler
<<<3  Finalizer
<<<4  Signal Dispatcher
<<<5  Attach Listener
<<<11  Common-Cleaner
<<<12  Monitor Ctrl-Break

进程与线程的关系,区别,优缺点

首先需要了解 Java的内存区域分配

Java语言将内存管理的工作交给JVM虚拟机的内存管理机制,不需要像C和C++程序员一样去new和free,不容易出现内存泄漏和内存溢出问题,但是一旦出现以上问题,如果不了解虚拟机的内存管理机制,那么排查错误将会是一个非常艰难的任务

如下图所示

在这里插入图片描述

有的是线程私有的,有的部分是线程共享的

私有的:

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

共享的:

  • 方法区
  • 直接内存

组件介绍:

  • 程序计数器

物理模型是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示。虚拟机内的字节码解释器都通过程序计数器找到下一条要执行的指令的字节码的位置,分支,循环,跳转,异常处理,线程恢复等功能都依赖于程序计数器的存在

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

程序计数器作用:

  1. 记录下一条指令的位置,传递给字节码解释器,实现流程控制,如:顺序,选择,循环,异常处理等
  2. 多线程的情况下,程序计数器还担任着记录当前线程执行的位置,当线程切换回来的时候能知道运行到了那儿

⚠️ : 程序计数器是唯一一个不会出现OutOfMemoryError 异常的内存区域,生命周期依赖于线程的生命周期

  • 虚拟机栈

也是私有的,生命周期与线程的生命周期相同,描述的是Java方法执行的内存模型。

Java内存可以粗糙的分为堆(Heap)和栈(Stack)两部分,其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分(实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧都拥有:局部变量表,操作数栈,动态链接,方法出口信息。)

局部变量表主要存放了编译器可知的各种数据类型(基本数据类型:boolean、byte、char、short、int、float、long、double),对象引用(reference类型,可能是一个对象地址的引用指针,代表对象的句柄或其他与此对象相关的位置)

句柄的本质:一个唯一的整数,作为对象的身份id,区分不同的对象,和同类中的不同实例。程序可以通过句柄访问对象的部分信息。句柄不代表对象的内存地址。

句柄和指针的区别:程序不能通过句柄直接阅读文件中的信息,指针是可以的。从所起的作用这点来说,句柄就是弱化后的指针,更安全,功能减少。

⚠️ :虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError

  • StackOverFlowError:如果Java虚拟机栈的内存不允许动态扩展,那么当线程请求栈的深度超过当前虚拟机栈的深度的时候,就会抛出此异常

  • OutOfMemoryError:如果Java虚拟机栈内存允许动态扩展,那么当其无法扩展的时候,就会抛出此异常

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

Java堆是垃圾回收机制管理的主要区域,因此也被称为GC堆(Garbage Collected Heap)【直译:垃圾回收堆】,采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代,再细致划分可分为:Eden空间,From Survivor,To Survivor空间等。进一步划分的目的是更高效地回收内存,更快的分配内存,内存示意图如下

Java1.8之后使用元空间(Metaspace)来取代了永久代。永久代占用的是JVM的堆内存空间,而元空间占用的是本机的物理内存

  • 方法区

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

HotSpot虚拟机中方法区也常被称为“永久代”,两者本质上并不等价,仅仅只是因为HotSpot的虚拟机设计团队用永久代来实现了方法区而已,这样的话垃圾回收器就可以使用管理永久代的内存那一套来管理方法区了,但是这样会更容易遇到内存泄漏问题

相对而言,垃圾回收行为在这片内存上是比较少出现的,但并非数据进入方法区后就永久保存了

HotSpot是较新的Java虚拟机,用来代替JIT(Just in Time),可以大大提高Java运行的性能。

Java原先是把源代码编译为字节码在虚拟机执行,这样执行速度较慢。而HotSpot将常用的部分代码编译为本地(原生,native)代码,这样显着提高了性能。

  • 运行时常量池

运行时常量池是方法区的一部分,类文件里面除了有各类的版本,字段,方法和接口信息之外,还有常量池信息

因为运行时常量池属于方法区,会受到方法区内存的限制,当常量池无法继续申请到内存的时候会抛出OutOfMemoryError异常

JDK1.7以及之后将运行时常量池从方法区中移动到了堆区【自然可能会抛出堆区出现的异常】

在这里插入图片描述

常量池的细分:https://blog.csdn.net/qq_26222859/article/details/73135660

  • 直接内存

可能抛出OutOfMemoryError异常,不是虚拟机规范中定义的内存区域,但是经常被使用

JDK1.4中新加入了NIO类【new input/output】,引入了一种基于通道和缓存区的IO方式,直接调用本地方法分配退堆外内存,然后在堆中保留一个DirectByteBuffer对象作为这一块内存的引用,这样就避免了数据在Java堆和native堆之间来回传递的弊端,在某些场景中可以显著提高运行速率

本地堆的内存分配不会受到Java虚拟机的限制,但是会受到本机的内存总量和处理器寻址空间的限制

简单总结:

​ 共有的为堆和方法区,堆中存放类的实体对象等信息,方法区存放类本体等信息

​ 私有的为程序计数器,虚拟机栈和本地方法栈

​ 程序计数器保证方法的流程控制,并记录每个线程执行的位置

​ 虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机调用的native方法服务

为了确保线程中的局部变量是私有的,所以虚拟机栈和本地方法栈为线程所私有

Hotspot虚拟机对对象的操作

1.对象的创建过程:

类加载检查 ->分配内存 ->初始化零值 -> 设置对象头 -> 执行init方法

1).类加载检查:虚拟机遇到new指令时候,首先会去检查这个指令的参数能否在常量池中定位到某个类的常量引用,并检查这个类是否被加载过,如果没有,则执行相应的类加载过程

2).内存分配:在类加载检查通过后,接下来虚拟机会为这个对象分配内存,分配的内存大小在类加载过程中已经确定,为对象分配内存相当于划出一块Java堆内存。分配方式有:“指针碰撞”和“空闲列表”,选择哪种分配方式由Java 堆是否规整决定,Java堆是否规整则依赖于所采用的垃圾回收器是否带有压缩整理功能决定

在这里插入图片描述

内存分配的并发问题【需要掌握】:

创建对象的一个重要的问题:保证线程安全,因为在实际代码运行过程中会创建大量的对象,虚拟机需要保证线程是安全的,通常虚拟机采用以下两种方式来保证线程安全:

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

3).初始化零值:

内存分配完成后,虚拟机将分配到的内存空间都初始化为0值【对象头除外】,保证了对象的字段【非静态字段】在Java代码中可以不需要初始化就可以使用,静态字段也是不需要初始化就能够使用的【静态字段属于类,类被存放在了方法区中】

4).设置对象头:

初始化零值完成之后,虚拟机要对对象进行必要的设置,在对象头中保存对象的哈希码,对象的GC分代年龄(Garbage Collection)用于垃圾回收,另外,虚拟机会根据当前运行状态的不同,如时候启用偏向锁等,对象头会有不同的设置方式

5).执行init方法

从虚拟机角度来看,一个对象已经创建完成了,但从Java程序的角度来看,一个对象的创建才刚刚开始,构造函数还没有执行,所有字段的数值都为零,接下来就执行构造函数。

2.对象的内存布局

在HotSpot虚拟机中,对象在内存中的布局分为三块,对象头,实例数据和对齐填充

HotSpot虚拟机的对象头包括两部分:一部分储存对象自身的运行时数据(如上面说到了哈希码,GC分代年龄,锁状态标识等等),另一部分是类型指针,即对象指向类元数据的指针,虚拟机来通过指针来确定这个对象是哪个类的实例

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

对齐填充部分不是必然存在的,没有什么特殊的含义,仅仅起到了占位的作用,因为HotSpot虚拟机要求对象的字段信息必须是8的整数倍,需要通过对齐填充来填补

3.对象的访问定位

我们的程序通过虚拟机栈上面的reference来操作具体的对象的,访问方式由虚拟机决定,主流的访问方式由使用句柄直接指针

  • 句柄:如果使用句柄的话,那么Java堆中会划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含实例对象数据和类型数据各自的具体地址信息
  • 直接指针:reference里面直接保存的是对象的地址

优劣比较:

​ 句柄访问的最大好处是reference中存储的是稳定的句柄地址,如果对象的地址改变,只需要改变句柄中的实例数据指针

而reference本身并不需要修改

​ 直接指针访问最大的好处是速度块,省去了一次寻址浪费的时间

重点补充内容

1.String对象的两种创建方式

	String str1 = "abcd";
     String str2 = new String("abcd");
     System.out.println(str1==str2);//false

创建方法是有差别的,str1是在常量池中拿对象,第二种是直接在堆内存新建一个对象【只要有new关键字,就会在堆内存中创建对象,在虚拟机栈中存储reference信息】

string常量池比较特殊,主要使用方法有两种:

  • 使用双引号声明的string对象会直接存储到常量池中
  • 使用实例字符串对象的intern方法,如果该字符串在常量池中存在,则返回常量池中的引用,如果不存在,则在常量池中创建该常量,并返回创建的常量的引用

以下例子可以佐证上述观点

public static void main(String[] args) {
    String str1 = new String("hello world");
    String str2 = "hello world";
    String str3 = str1.intern();
    System.out.println(str1 == str3);
    System.out.println(str2 == str3);
}

<<<false
<<<true

字符串的拼接:

会直接在常量池中产生新的对象

public static void main(String[] args) {
    String str1 = new String("hello world");
    String str2 = new String("hello world");
    String str3 = str1 + str2;
    System.out.println(str3 == str3.intern());
}

<<<true

尽量避免多个字符串拼接,拼接后的字符串会存储早常量池中

String str1 = new String(“hello world”);创建了几个对象

1.先将"hello world"字符串放入常量池,然后在Java的堆中创建了一个字符串对象存储helloworld,至于Java栈上的str1,是在程序运行,主线程启动的情况下才确定的,指向堆中的字符串

  • Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
  • 两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值