开发随手记-Java高级-多线程+JVM

Java多线程

线程与进程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.。

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

简单来说,一个程序至少有一个进程,一个进程里面至少有一个线程。线程可以看为是进程里的执行体,同一个进程中的多个线程之间可以共享资源。线程不能够独立运行,必须依托于进程,一个进程中的多个线程可以并发执行

多线程实现

Thread类(掌握)

继承Thread类,并重写run()方法

 

Runnable接口(掌握)

实现Runnable接口。

 

Thread和Runnable区别
  1. 由于Java只能单继承所以使用Runnable接口的方式比较灵活易于扩展,更利于资源共享。
  2. Thread类可以直接通过start()的方式进行启动,而Runnable接口需要借助Thread类。

 

Callable接口和FutureTask

利用Callable和FutureTask来完成具有返回值的线程创建。

 

 

JDK组成

JDK

JDK(Java Development ToolKit)Java开发的工具箱,JDK是Java的整个核心,里面包含了JRE。它除了包含jre之外还包含了一些javac的工具类,把Java源文件编译成class文件,java命令是用来运行这个程序的,除此之外,里边还包含了Java源生的API,java.lang.Integer在rt的jar包里边【可以在项目中看到】,通过rt这个jar包来调用我们的这些io流写入写出等

JDK有以下三种版本:

JAVASE,standard edition,标准版,是我们通常用的一个版本

JAVAEE,enterpsise edtion,企业版,使用这种JDK开发JAVAEE应用程序

JAVAME,micro edtion,主要用于移动设备、嵌入式设备上的java应用程序

JRE

JRE(Java  Runtime  Enviromental)Java运行时环境,所谓的Java运行时环境,就是为了保证java程序能够运行时,所必备的基础环境,它只是保证Java程序运行的,不能用来开发,而JDK才是用来开发的,所有的Java程序都要在JRE下才能运行。

JRE包括JVM和JAVA核心类库和支持文件。与JDK相比,它不包含开发工具编译器、调试器和其它工具。

JVM

JVM(Java Virtual Mechinal)Java虚拟机,JRE是Java运行时环境,Java运行依赖于JVM,JVM用来加载类文件,Java中之所以有跨平台的作用,就是因为JVM。

不同操作系统的所安装的JDK不一样,而不同的JDK中的JVM不一样(Java跨平台的关键)。

Java程序运行流程

通过前面的学习,我们都知道一个Java程序的运行需要编写源码编译生成.class文件(javac)JVM加载class文件解释或编译运行class中的字节码指令(java)

 

而JVM具体的过程就如上图所示:在执行java命令后,JVM会将所有.class文件通过类加载器,加载到内存中并初始化一些必要数据信息。当执行引擎找到程序主入口main()后就会执行其中的字节码。

JVM(JDK8)

类加载器

类加载运行流程/生命周期

JVM把.Class文件加载到内存,并对数据进行校验转换解析初始化,最终形成可以被JVM直接使用的Java类型。类加载和连接的过程都是在运行期间完成的。

 

构成 

JVM中提供了三种加载器:

启动类加载器(Bootstrap ClassLoader):该类加载器负责将JDK中的类加载到内存中,如rt.jar等。开发者无法直接操作该类加载器。

标准扩展类加载器(Extension ClassLoader):该类加载器负责将其它ext中的类加载到内存中,开发者可以直接使用该类加载器。

应用程序类加载器(Application ClassLoader):该类加载器负责将应用程序中的类加载到内存中。开发者可以直接使用该类加载器。

自定义类加载器(User ClassLoader):该类加载器由开发者自行扩展,来完成特殊的任务,如:热加载/热更新、读取配置文件等功能。

双亲委派模型

JVM的类加载器之间的层次关系就被称之为双亲委派模型,该模型要求除了顶层的启动类加载器之外,其余的加载器都应该有自己的父加载器,而这种父子关系是通过组合关系来实现,而不是通过继承

双亲委派的原理

一个类加载器收到类加载的请求,这个类加载器会先把这个请求委派给父类加载器,每一层类加载器都会重复这个流程(递归),直到没有父类加载器(启动类加载器),当父类加载器没有成功加载,子类加载器才尝试加载,并重复这个流程,直到接收加载请求的那个类加载器,如果还是没有成功加载,就会抛出ClassNotFound异常。

双亲委派的过程
  1. 先检查是否被加载过。
  2. 若没被加载过先调用父类的loadCass()方法。如果父加载器为空则调用启动类加载器作为父加载器。
  3. 若父类加载失败,再调用自己的findClass()进行加载。
  4. 没有完成加载任务,抛出异常。
双亲委派的好处

层次结构清晰,有优先级概念,并可以避免类被重复加载。并可以保证JVM类基础安全,不会轻易被覆盖掉,而且更容易实现热加载。

内存模型

 

 

程序计数器(了解)

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError内存溢出的区域。

虚拟机栈/Java栈Stack

虚拟机栈也是线程私有/线程隔离,生命周期与线程相同,也是我们平时说的(栈先进后出)是Java方法执行的内存模型。Java方法被调用一次都会开启一个新的线程。

栈中的数据包含:局部变量表,操作栈,动态链接,方法出口等信息。

局部变量表:我们平时说的栈就是指的这部分内的数据。它是一片连续的内存空间,用来存放方法参数、方法内定义的局部变量、方法返回地址、编译期已知的数据(常量)Java基本数据类型引用数据类型的引用

需要注意的是:

  1. 栈中所需要的内存空间在编译期就基本完成分配,当进入一个方法时,这个方法在栈中需要分配多大的栈空间是基本固定的,在方法运行期间不会改变大小。
  2. 在运行期间线程请求的栈空间大于虚拟机允许的栈空间会抛出栈溢出(StackOverflowError),如方法死循环调用等。
  3. 栈空间也可以动态扩展,当动态扩展时无法申请到足够的空间时,抛出内存溢出(OutOfMemoryError,如大量重复对象的创建等。
本地方法栈(了解)

本地方法栈则为JVM调用Native(本地)方法服务的区域,比如IO操作,网络通信等,不同的操作系统实现不一样。

堆(Heap

堆是JVM管理内存最大的区域,因为堆存放的对象线程共享的,所以多线程的时候也需要同步机制,当申请不到空间时会抛出OutOfMemoryError

方法区/元数据区

方法区同堆一样,是所有线程共享的内存区域但有别于堆。用于存储已被JVM加载的类信息、常量静态变量,如static修饰的变量加载类的时候就被加载到方法区中。JDK8废弃方法区,而使用元空间(Metaspace)。

元空间并不在虚拟机中,使用的是系统本地内存。元空间的大小仅受本地内存限制。

图解

 

GC(垃圾回收机制)

简介

GC(Garbage Collection):即垃圾回收器,主要是用来回收释放垃圾(不在使用的对象)占用的内存空间。该机制也是Java与C/C++的主要区别之一,日常开发Java代码的时候,一般都不需要编写内存回收或垃圾清理的代码,也不需要像C/C++那样做类似内存释放的操作。

工作原理

哪些需要回收

由于JVM的内存模型设计,程序计数器虚拟机栈本地方法栈都是线程私有(同线程的生命周期一致),而元数据区使用本地内存所以不需要GC过多的关心。

堆(Heap)则不一样,它是所有线程共享的,只有在运行期间才可能知道用了多少内存。这部分内存的分配和回收都是动态的,也是GC最关注的部分。

怎么回收
堆内划分

为了高效回收,GC将分为三个区域:

年轻代(Young):所有新生的对象都在里面。

老年代(Old):在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象就会转到年老代。

永久代(Permanent):用于存类、方法等信息,或特大对象。(JDK8中已取消永久代,改为使用元数据区)

判断对象是否存活算法
引用计数算法(已废弃)

简单来说,就是给一个对象添加一个引用计数器,对象每被引用一次,计数器就+1,引用失效计数器就-1,当计数器为0时就回收。

优点:实现简单,效率高。

缺点:难以解决对象的循环引用问题(A和B互相引用,但这两个对象已不被其它对象引用,导致计数器不为0,无法回收)。

可达性分析算法(主流)

它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。

Java中可以作为GC Roots的对象:

  1. 栈中引用的对象。
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的native方法)中引用的对象。
回收算法
标记-清除法

标记-清除算法将垃圾回收分为两个阶段:标记阶段清除阶段。在标记阶段,首先通过可达性分析算法将可达的对象标记出来。因此,未被标记的对象就是垃圾对象。然后,在清除阶段,清除所有未被标记的对象

 

 

缺点:

  1. 效率比较低。
  2. 标记清除后会出现大量不连续的内存碎片,这些碎片太多(碎片指内存中非连续的空间)可能会使存储大对象时会再次触发GC回收,造成内存浪费以及时间的消耗。
标记-整理法

和标记-清除算法一样,标记-整理算法也分两个阶段:标记阶段整理清除阶段。首先对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间

 

标记-整理算法相比标记-清除算法可以避免内存碎片的产生。

复制算法

与标记-清除/整理算法相比,复制算法是一种相对高效的回收方法。将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

分代回收

分代收集是目前JVM使用的回收方式,将内存(堆)分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法。

新生代对象存活率低,可以使用复制算法

老年代对象存活率高,没有额外空间对它进行分配担保,所以使用标记-整理算法

回收器(了解)

年轻代回收器:Serial(单线程)、ParNew(多线程)、Parallel Scavenge(多线程-复制算法)

老年代回收器:Serial Old(单线程-标记-整理)、Parallel Old(多线程-标记-整理)、CMS(支持并发-标记-清除)

混合回收器:G1收集器,JDK7后出现,强化了分区,弱化分代。支持并发、使用复制算法、标记整理算法。

什么时候回收

当应用程序需要创建新的对象,而内存预算的大小已经达到阈值或代码主动显式调用System.GC.Collect()等都可以触发GC的执行。

GC分为Minor GC 、Major GCFull GCMixed GC针对与不同情况进行回收。

Minor GC:只回收年轻代。当JVM无法为一个新的对象分配空间时会触发Minor GC,对年轻代内的对象采用复制算法进行回收。

Major GC:只回收老年代。但Major GC的执行可能会触发 Minor GC,从而变为Full GC。因为回收老年代的时候往往也会伴随着升级年轻代,从而整个堆都会被回收。

Full GC:对年轻代,老年代进行统一回收。

Mixed GC:G1收集器独有,收集整个年轻代以及部分老年代的GC。

OOM

OutOfMemory(OOM-内存溢出)内存用完了,当JVM因为没有足够的空间来为对象分配空间并且GC也已经没有可回收空间时,就会抛出这个Error。(注意:OOM不是异常,而是错误,程序会停止!!!)

根据不同的产生情况OOM分为:堆内存溢出、方法区内存溢出、栈溢出。

堆溢出

java.lang.OutOfMemoryError:Java heap space(堆内存溢出)此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件(jmap)查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms(初始堆大小),-Xmx(最大堆大小)等修改。

方法区溢出

java.lang.OutOfMemoryError: PermGen space(方法区溢出),即方法区溢出了,一般出现于程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,或过多的常量尤其是超大字符串也会导致方法区溢出。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize(方法区初始大小) -XX:MaxPermSize(方法区最大大小)的形式修改。

栈溢出

java.lang.StackOverflowError(JAVA虚拟机栈溢出)不会抛OOM Error,但也是比较常见的Java内存溢出。一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

内存溢出和内存泄漏区别

内存泄漏:指程序中动态分配内存给一些临时对象,但是对象不会被GC回收,它始终占用内存。即被分配的对象可达但已无用。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于GC回收后,仍然无内存空间容纳新的对象的情况。

内存泄漏也是导致内存溢出的原因之一。内存泄漏出并不会直接导致内存溢出,当时长期积累就会导致内存溢出。

防止OOM的建议

  1. 尽早释放无用对象。
  2. 使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域。
  3. 尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收。
  4. 避免在循环中创建对象。
  5. 开启大型文件或一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
  6. IO操作后要及时释放资源,避免内存泄漏导致的内存溢出。

堆、栈、常量池关系

所以比较字符串要用equal(),而不能直接是==。 

 

 

 

值传递与引用传递

在了解值传递于引用传递前,我们首先了解一下,形参实参

形参:就是形式参数,方法参数列表就是形参。

实参:就是实际参数,在方法体或代码块内声明的参数就是实际参数。

例如:

 

基本数据类型在进行参数传递时属于值传递,所以上面案例中程序运行的结果还是1。

引用数据类型在进行参数传递时也属于值传递,只不过引用数据类型的值是引用地址

 

 

总结:

  1. Java不存在引用传递

无论是基本数据类型,还是引用数据类型,都是值传递。

JDK1.7新特性

  1. 语言改进

1、switch支持String。

2、简洁泛型。

3、数字支持_分隔。int I = 1_00_00;

  1. 垃圾回收器升级。
  2. IO/网络更新。更新了更多的NIO2的API。
  3. 核心类库新增API(CLassLoad、URLClassLoad等)。
  4. 新增了椭圆曲线加密算法。
  5. 程序的稳定性和效率、安全性都有所提高。

JDK1.8新特性

  1. 语言新特性。
  1. Lambda表达式。
  2. 函数式编程。
  1. 新增Streams API。
  2. Clock新的时间类。
  3. Base64加入Java官方库。
  4. JVM新特性。

 

 

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值