Java内存模型
为什么说java是跨平台语言
所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行;
实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序;
这个跨平台是中间语言(JVM)实现的跨平台;
Java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统;
Jdk和Jre和JVM的区别
Jdk包括了Jre和Jvm,Jre包括了Jvm;
Jdk是我们编写代码使用的开发工具包;
Jre 是Java的运行时环境,他大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库;
Jvm俗称Java虚拟机,他是java运行环境的一部分,它虚构出来的一台计算机,在通过在实际的计算机上仿真模拟各种计算机功能来实现Java应用程序;
JVM由那些部分组成
JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引擎);
两个组件为Runtime data area(运行时数据区jvm的内存)、Native Interface(本地接口);
运行时数据区
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
方法区(Methed Area):方法区是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码的一些数据。
线程共享的:堆和方法区
线程独享的:程序计数器,java虚拟机栈,本地方法栈
什么是直接内存?
直接内存并不属于JVM的内存结构,它是物理机的内存,但是JVM虚拟机可以调用该部分内存。
我的理解就是直接内存是基于物理内存和Java虚拟机内存的中间内存
详细说一下Java虚拟机栈
1.局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型;
(returnAddress中保存的是return后要执行的字节码的指令地址。)
2.操作数栈:操作数栈就是用来操作的,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去;
3.动态链接:假如我方法中,有个别的方法,需要调用这个方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
4.出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常。
一个方法调用另一个方法,会创建很多栈帧吗?
答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面
递归的调用自己会创建很多栈帧吗?
答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去
栈指向堆是什么意思?
答:栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址
详细介绍一下Java堆
java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
从内存回收角度来看java堆可分为:新生代和老生代。
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
什么是JVM字节码执行引擎
虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。
“虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要JVM字节码执行引擎- 编译成机器码后才可在物理机上执行。
Java内存泄露
内存泄露:是指不再被使用的对象或者变量一直被占据在内存中。长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
内存溢出:申请内存空间,超出最大堆内存空间。Xms:堆内存 Xmx:堆最大内存。
栈溢出堆溢出
栈内存溢出:最常见的就是递归。每次递归就相当于调用一个方法,方法每次被调用时都会在栈中创建一个栈帧。占用的内存直到整个递归结束才会被释放,在递归过程中只会累加,不会释放。所以会造成栈内存溢出。
堆内存溢出(OutOfMemoryError : java heap space):需要分清是 内存溢出 还是 内存泄漏:
(1)如果是内存溢出,则通过 调大 -Xms,-Xmx参数。
(2)如果是内存泄露,则看对象如何被 GC Root 引用。
垃圾回收机制和算法
Java垃圾回收机制
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
Java垃圾回收分为两步:
一、标记(怎么标记看 <怎么判断对象是否可以被回收? 章节>)
二、清除(怎么清除看 <JVM垃圾回收算法> 章节)
GC是什么
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
垃圾回收的优点和缺点
优点:JVM的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点
缺点:程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收
垃圾回收器的原理是什么?有什么办法手动进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
JVM 中都有哪些引用类型?
强引用:发生 gc 的时候不会被回收。
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
弱引用:有用但不是必须的对象,在下一次GC时会被回收。
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,虚引用的用途是在 gc 时返回一个通知。
怎么判断对象是否可以被回收?
一般有两种方法来判断:
程序计数器:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
缺点:对于相互引用的对象,没法进行清除。
可达性分析:从GcRoot根节点出发,然后向下标记那些直接或间接引用到的对象,形成引用链,不在引用链上的对象就会被视为垃圾。
Full GC是什么
清理整个堆空间—包括年轻代和老年代和永久代
因为Full GC是清理整个堆空间所以Full GC执行速度非常慢,在Java开发中最好保证少触发Full GC
对象什么时候可以被垃圾器回收
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
JVM垃圾回收算法
复制算法(1):将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除;
特点:不会产生空间碎片;会造成空间浪费(因为只用到了原先内存的一半。);
标记清除法(2):第一步:利用可达性去遍历内存,把存活对象进行标记;第二步:在遍历一遍,将所有没有被标记的对象回收掉;
特点:第一是执行效率不稳定,执行效率都随对象数量增长而降低;第二是:空间的碎片化问题:标记、清除之后会产生大量不连续的内存碎片;
标记整理法(3):第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;第二步:将所有的存活的对象向一端移动,将边界以外的对象都回收掉;
特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;
分代收集算法(4):根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;
System.gc():
Java的GC是由JVM自行调动的,在需要的时候才执行,上面的指令只是告诉JVM尽快GC一次,但是什么时候执行不一定。
新生代老年代永久代
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。
永久代:在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。
为什么大对象直接在老年代里面分配?
如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制,造成空间浪费。
Minor GC、Major GC、Full GC是什么
Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾)
Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法)
Full GC是清理整个堆空间,包括年轻代和老年代
Minor GC、Major GC、Full GC区别及触发条件:
Minor GC 触发条件一般为:
1.eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
2.新创建的对象大小 > Eden所剩空间时触发Minor GC
Major GC和Full GC 触发条件一般为:
1.从幸存区晋升到老年代的对象大小大于老年代剩余空间
2.MinorGC后存活的对象超过了老年代剩余空间
3.永久代空间不足
4.分配一个很大的对象的时候
为什么新生代要分Eden和两个 Survivor 区域?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。
老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例?
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,Edem 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),
但是JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
为什么要这样分代:
其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法:
新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。
说一下 JVM 有哪些垃圾回收器?
Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
G1(Garbage First)收集器 ( 标记整理 + 复制算法来回收垃圾 ): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
类加载过程
加载:虚拟机把描述类的数据从Class文件加载到内存,
验证:检查加载的 class 文件的正确性,主要验证是否符合Class文件格式规范。
准备:给类中的静态变量分配内存空间,并设置类变量初始值。
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:初始化阶段是执行类构造器() 方法的过程。
****方法:这不是由程序员写的,而是根据代码由javac编译器生成的。
它是由类里面所有的类变量的赋值动作和静态代码块组成的。
JVM内部会保证一个类的方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的方法,其他的线程都要阻塞等待,直到这个线程执行完方法。然后执行完方法后,其他线程唤醒,但是不会再进入()方法。也就是说同一个加载器下,一个类型只会初始化一次。这也是为什么通过类加载器创建对象是线程安全的原因;
描述一下JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
类加载器有哪些
1.启动类加载器(Bootstrap ClassLoader):这个类加载器使用C/C++语言实现,在JVM内部。用来加载Java核心类库。
2.扩展类加载器(Extension ClassLoader):父加载器是启动类加载器,它负责加载JRE的扩展目录,加载lib下面的ext子目录的类库。
3.应用程序类加载器(Application ClassLoader):父加载器是扩展类加载器,加载我们自己定义的类,他是程序中默认的加载器。
4.自定义类加载器:将所有继承抽象类ClassLoader的类加载器都划分为自定义类加载器。
什么是双亲委派模型
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父加载器无法完成此加载任务,子加载器才会尝试自己去加载,如果均加载失败,就会抛出ClassNotFoundException异常,这就是双亲委派模式。
即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了了时,儿子自己才想办法去完成。
优点:为了安全,避免自己写的类替换掉了java的核心类。
怎么打破双亲委派模型
打破双亲委派机制,即在类加载的时候不是传递到父类加载器中加载,而是由自己加载。
此时需要自定义一个类加载器,继承ClassLoader类,然后重写父类的findClass方法和loadClass方法。
1、自定义一个类加载器,继承ClassLoader类
2、重写findClass方法和loadClass方法
这里最主要的是重写loadclass方法,因为双亲委派机制的实现都是通过这个方法实现的,先找父加载器进行加载,如果父加载器无法加载再由自己来进行加载,源码里会直接找到根加载器(启动类加载器),重写了这个方法以后就能自己定义加载的方式了。
JVM调优
常用的 JVM 调优的参数都有哪些?
-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。
-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。
-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。
-XX:NewSize=n 设置年轻代初始化大小大小
-XX:MaxNewSize=n 设置年轻代最大值
-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4
-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8
-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。
-XX:ThreadStackSize=n 线程堆栈大小
-XX:PermSize=n 设置持久代初始值
-XX:MaxPermSize=n 设置持久代大小
-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。
#下面是一些不常用的
-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小
-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能
-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用
-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动
-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用
-Xnoclassgc 是否禁用垃圾回收
-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用
…
JVM的GC收集器设置
-xx:+Use xxx GC
xxx 代表垃圾收集器名称
-XX:+UseSerialGC:设置串行收集器,年轻带收集器
-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。
-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量
-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。
-XX:+UseConcMarkSweepGC:设置年老代并发收集器
-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器
避免FullGc
1.Full GC:因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。
2.导致Full GC的原因:
1)年老代(Tenured)被写满:调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象。
2)永久代Pemanet Generation空间不足:增大Perm Gen空间,避免太多静态对象,控制好新生代和旧生代的比例。
3)System.gc()被显示调用:垃圾回收不要手动触发,尽量依靠JVM自身的机制。
因为 System.gc 是一个 Full gc,所以会暂停整个进程。如果进程经常被频繁暂停,就要注意超时、并发的问题。
FullGc是怎么做到暂停进程的(stop the world)
Java里面的 GC 有一个重要的线程 VMThread。在 jvm里,这个线程会不断轮询它的队列,这个队列里主要是存一些 VM_operation 的动作,比如最常见的就是内存分配失败要求做 GC 操作的请求等,在对gc 这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的 stop the world(STW),整个进程相当于静止了。