jvm.concurrent.parallel.collection.map

学习目标:

1.list、set、map之间的区别
2.并行和并发的区别
3.jvm的机制和原理

学习内容:

1.list、set、map底层计算,应用
2.并行和并发原理和应用
3.jvm运行

学习产出:

1 list、set、map之间的区别

1.1 表面区别
jvm.concurrent.parallel.collection.map
1.2 算法区别
https://blog.csdn.net/weixin_56219549/article/details/119445497
https://blog.csdn.net/lmarster/article/details/90672794

2 并行和并发原理和应用

2.1 并行并排而行,并发合一而行
jvm.concurrent.parallel.collection.map
2.2 浅谈并行与并发
https://www.zhihu.com/question/33515481
https://www.cnblogs.com/xc-chejj/p/10813692.html

3 jvm运行

3.1 JVM话术
3.1.1 介绍下JVM
jvm.concurrent.parallel.collection.map
JVM主要包括:类加载器(class loader )、执行引擎(exection engine)、本地接口(native interface)、运行时数据区(Runtimedata area)

类加载器:加载类文件到内存。Class loader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是由执行引擎负责的

执行引擎:负责解释命令,交由操作系统执行

本地接口:本地接口的作用是融合不同的语言为java所用。

JVM的运行时数据区分为五个区域:堆、虚拟机栈、本地方法栈、方法区、程序计数器。其中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区,JVM不同区域的占用内存大小不同,一般情况下堆内存最大,程序计数器较小。

程序计数器:这里记录了线程执行的字节码的行号,在分支、循环、跳转、异常、线程恢复等都依赖这个计数器。如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空

**java虚拟机栈:**每个方法执行的时候都会创建一个栈帧,用于存放局部变量表、操作栈、动态链接、方法出口。每一个方法从调用直到执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。

**本地方法栈:**与虚拟机栈很类似,区别是一个是执行Java方法,一个是执行本地方法。有的虚拟机会把这2个栈合二为一。

**堆:**Java堆是Java虚拟机所管理的内存最大的一块,被所有线程共享的一块内存区域,在虚拟机启动的时候就创建了。这个内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器(GC)管理的主要区域,有时候也被称为“GC堆”。因为现在收集器基本都采用分代收集算法,所有Java堆还可以细分为:新生代和老年代。堆是可以固定大小也是可以扩展的,如果在堆内存不足,并且也无法及时扩展时,会抛出OutOfMemoryError异常。

**方法区:**用于存储已被Java虚拟机加载的类信息、常量、静态变量、以及编译器编译后的代码等数据。

3.1.2 介绍下内存泄漏和内存溢出

1、内存泄漏叫memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
2、内存溢出叫out of memory :指程序申请内存时,没有足够的内存给申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,就是所谓的内存溢出。

Java内存泄漏的根本原因是,长生命周期的引用指向了短生命周期的对象,导致内存无法被回收,我给您说几个具体的场景吧

  • 静态集合类造成的内存泄漏
static List list = new ArrayList(10);
  public void method(){
  for (int i = 1; i<100; i++){
      Object o = new Object();
    list.add(o);
      o = null;
  }
  }

循环申请Object 对象,并将所申请的对象放入一个ArrayList 中,如果仅仅释放引用本身(o=null),那么ArrayList 仍然引用该对象,所以这个对象对GC 来说是不可回收的,就会导致内存泄漏。因此,如果对象加入到ArrayList 后,还必须从ArrayList 中删除,最简单的方法就是将ArrayList对象设置为null

  • 拦截器中导致内存泄漏

    在很多拦截器中,比如总是会是使用threadlocal存储一些线程变量,如果在方法请求完成时,没有将threadlocal中的变量释放,那么也会导致内存泄漏

  • 各种连接导致的内存泄漏

    比如数据库连接,网络连接(socket)和io连接,除非显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的

    下面我再给您说说内存溢出的情况吧(接着背第三题)

3.1.3 列举一些会导致内存溢出的类型都有哪些,分别怎么造成的

  • 第一种OutOfMemoryError: PermGen space

    发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,与Permanent Generation space有关。解决这类问题有以下两种办法:

    1、增加java虚拟机中的PermSize和MaxPermSize参数的大小,其中PermSize是初始永久保存区域大小,MaxPermSize是最大永久保存区域大小。比如说针对tomcat,在catalina.sh 文件中增加这两个参数的配置就行了(一系列环境变量名说明结束处(大约在70行左右) 增加一行:JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m")。如果是windows服务器还可以在系统环境变量中设置。用tomcat发布ssh架构的程序时很容易发生这种内存溢出错误。使用上述方法,我成功解决了部署ssh项目的tomcat服务器经常宕机的问题。 2、清理应用程序中web-inf/lib下的jar,如果tomcat部署了多个应用,很多应用都使用了相同的jar,可以将共同的jar移到 tomcat共同的lib下,减少类的重复加载。这种方法是网上有人推荐过,我没试过,但感觉减少不了太大的空间,最靠谱的还是第一种方法。

  • 第二种OutOfMemoryError: Java heap space

    发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之前,虚拟机分配的到堆内存空间已经用满了,与堆空间大小有关。解决这类问题有两种思路:

    1、检查程序,看是否有死循环或不必要地重复创建大量对象。找到原因后,修改程序和算法。 我以前写一个使用算法对几万条文本记录进行操作时,由于程序细节上有问题,就导致了 Java heap space的内存溢出问题,后来通过修改程序得到了解决。 2、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的值。(如:set JAVA_OPTS= -Xms256m -Xmx1024m)

  • 第三种OutOfMemoryError:unable to create new native thread

    在java应用中,有时候会出现这样的错误。这种错误是因为给JVM分配了过多内存导致的,比如超过可用内存的一半,就会导致这种问题。在线程个数很多的情况下, 你分配给JVM的内存越多,那么,这种错误发生的可能性就越大。

    比如说系统可用内存一共2G,这里假设分配1.5G给JVM,那么还余下500M可用内存。这 500M内存中的一部分必须用于系统文件加载,那么真正剩下的也许只有400M,但是关键是,当你使用Java创建一个线程,在JVM的内 存里也会创建一个Thread对象,但是同时也会在操作系统里创建一个真正的物理线程,操作系统会在剩下的400兆内存里创建这个物理 线程,而不是在JVM的1.5G的内存堆里创建。在jdk1.4里头,默认的栈大小是256KB,但是在jdk1.5之后,默认的栈大小为1M每个线程, 因此,在余下400M的可用内存里边我们最多也只能创建400个可用线程。

    这种情况的话,要想创建更多的线程,你必须减少分配给JVM的最大内存,或者增加系统的内存

3.1.4 JVM中垃圾回收的算法

  • 引用计数器算法

    对象中添加一个引用计数器,如果引用计数器为0则表示没有其它地方在引用它。如果有一个地方引用就+1,引用失效时就-1。看似搞笑且简单的一个算法,实际上在大部分Java虚拟机中并没有采用这种算法,因为它会带来一个致命的问题——对象循环引用。对象A指向B,对象B反过来指向A,此时它们的引用计数器都不为0,但它们俩实际上已经没有意义因为没有任何地方指向它们。

  • 可达性分析算法

    这种算法可以有效地避免对象循环引用的情况,整个对象实例以一个树的形式呈现,根节点是一个称为“GC Roots”的对象,从这个对象开始向下搜索并作标记,遍历完这棵树过后,未被标记的对象就会判断“已死”,即为可被回收的对象。

  • 标记-清除算法

    这个方法是将垃圾回收分成了两个阶段:标记阶段和清除阶段。

    在标记阶段,通过根对象,标记所有从跟节点可达的对象,那么未标记的对象就是未被引用的垃圾对象。

    在清除阶段,清除掉所以的未被标记的对象。

    这个方法的缺点是,垃圾回收后可能存在大量的磁盘碎片,准确的说是内存碎片。

  • 标记-整理算法

    在标记清除算法的基础上做了一个改进,可以说这个算法分为三个阶段:标记阶段,压缩阶段,清除阶段。标记阶段和清除阶段不变,只不过增加了一个压缩阶段,就是在做完标记阶段后,将这些未被标记过的对象集中放到一起,确定开始和结束地址,比如全部放到开始处,这样再去清除,将不会产生磁盘碎片。但是我们也要注意到几个问题,压缩阶段占用了系统的消耗,并且如果未标记对象过多的话,损耗可能会很大,在未标记对象相对较少的时候,效率较高。

  • 复制算法(Java中新生代采用)

    核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象,然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片。

    此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记出待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。

  • 分代搜集算法(Java堆采用)

    主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率。

    Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

    JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 90% 的新生代空间。新生代垃圾回收采用复制算法,清理的频率比较高。如果新生代在若干次清理中依然存活,则移入老年代,有的内存占用比较大的直接进入老年代。老年代使用标记清理算法,清理的频率比较低。

  • 分区算法

    这种方法将整个空间划分成连续的不同的小区间,每个区间都独立使用,独立回收,好处是可以控制一次回收多少个小区间。

3.1.5 类加载的过程

  • 加载:根据查找路径找到相应的 class 文件然后导入;

  • 检查:检查加载的 class 文件的正确性;

    • 准备:给类中的静态变量分配内存空间;
    • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而直接引用是直接指向内存中的地址;
    • 初始化:对静态变量和静态代码块执行初始化工作。

    说到这儿我再说下new一个对象之后,类实例化的顺序吧

    • 首先是父类的静态变量和静态代码块(看两者的书写顺序);
  • 第二执行子类的静态变量和静态代码块(看两者的书写顺序);

    • 第三执行父类的成员变量赋值
    • 第四执行父类的普通代码块
    • 第五执行父类的构造方法()
    • 第六执行子类的普通代码块
    • 第七执行子类的构造方法();

    也就是说虽然客户端代码是new 的构造方法,但是构造方法确实是在整个实例创建中的最后一个调用

3.1.6 怎么判断对象是否可以被回收

一般有两种方法来判断:

  • 引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;

  • 可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

3.1.7 说一下JVM调优的工具

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;

  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

    • jstat:jstat 命令可以查看堆内存各部分的使用量,以及加载类的数量
    • jmap:是用于查看指定Java进程的堆内存使用情况

3.1.8 详细介绍下JVM堆中的内存模型

我先给您介绍下jdk1.7中的堆的情况吧

  • Young 年轻代

年轻代区域被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候,GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过15次垃圾收集后,仍然存活于Survivor的对象被移动到老年代。

  • Tenured 年老代

老年带主要保存生命周期长的对象,一般是一些老的对象,当一些对象在年轻代复制转移一定的次数以后,对象就会被转移到老年代,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间

  • Perm 永久代

永久代主要保存 class、method、filed对象,这部分的空间如果使用不当,就会造成内存溢出,比如一次性加载了很多的类,或者一个tomcat下部署了几十个应用,不过在涉及到热部署的服务器的时候,有时候会遇到 java.lang.OutOfMemoryError:PermGenSpace的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在永久代中,不过这种情况下,一般重新启动服务器可以解决问题

  • Virtual虚拟区

最大内存和初始内存的差值,就是虚拟区

而在jdk1.8中,最大的变化就是用元数据空间替代了永久代,这块所占用的内存空间不是在虚拟机内部的,而是在本地内存空间中,我看了下官网的解释,是因为了后续要融合两个JVM的版本,因为一个版本中没有设计永久代这个概念,另外一方面就是在我们现实使用中,由于永久代内存经常不够用或发生内存泄露,因此将永久代废弃,而改用元数据空间,改为了使用本地内存空间
3.2 JVM面试题:说说Java虚拟机的生命周期及体系结构
https://maozj.iteye.com/blog/697376
3.3 jvm优化
https://blog.csdn.net/weixin_51297617/article/details/120981582

上一篇:异步多线程之Parallel详解


下一篇:C# 实现Parallel.For