JDK、JRE、JVM的区别:
三层的嵌套关系。JDK>JRE>JVM
1、JDK:编译Java源码,生成Java字节码。
/bin包含:
(1)、java:启动JVM,运行Java程序
(2)、javac:Java的编译器,将Java源文件(.java)编译为字节码文件(.class)
(3)、jar:将.class文件打包成一个.jar文件,便于发布;
(4)、javadoc:用于从Java源码中自动提取注释并生成文档;
(5)、jdb:Java调试器,用于开发阶段的运行调试。
2、JRE:Java运行时的环境,负责运行Java虚拟机,包含JVM+javase标准类库
3、JVM:即java虚拟机,能够识别 class字节码文件并调用操作系统向上的 API 完成动作。(实现跨平台的根本原因)
JVM结构原理:
重点关注在JVM运行数据区(即JVM内存)与类加载器
1、JVM内存:
栈(Stack)、堆(Heap)、方法区(Method Area)、PC寄存器、本地方法栈
(1)、栈(Stack)内存:
在栈中,每一个方法对应一个栈帧,JVM以栈帧为单位按照后进先出的原则执行压栈或出栈。线程结束,栈内存就释放,不存在垃圾回收问题,栈满了就存在栈内存溢出StackOverflowError(常发生于递归方法中)。
存储局部变量
(2)、堆(Heap)内存:
堆是JVM中最大的一块存储区域,一个Java虚拟实例中只存在一个堆空间,所有的对象实例都会分配在堆上,GC(垃圾回收)主要是对堆内存进行管理,当Full GC之后发现依然无法进行对象的保存,就会产生OOM异常(OutOfMemoryError,堆内存溢出)
存储new实例变量、数组
(3)、方法区(Method Area)内存:
方法区是各个线程共享的内存区域
存储字节码代码片段、静态变量
(4)、PC寄存器:
用于存放一条指令的地址,每一个线程都有一个PC寄存器
(5)、本地方法栈:
用来调用其他语言的native方法
注:
native关键字:
1)、native关键字用来修饰方法,其修饰的方法会进入本地方法栈
2)、凡是带native修饰的方法,主要是用来告诉JVM该方法在外部定义,也就是在java程序中调用c/c++语言去实现
2、类加载器:
启动类加载器、扩展类加载器、应用类加载器、自定义类加载器
(1)、启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
(2)、扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
(3)、应用类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
(4)、自定义类加载器
注:
双亲委派机制:
双亲委派机制保证类加载的安全:
1)、类加载器收到类加载的请求(默认情况,若无自定义类加载器默认使用应用类加载器)
2)、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3)、启动类加载器检测是否能够加载当前该类,能加载就结束,使用当前加载器,否则,反馈给子类,由子类去完成类的加载,
4)、都无法找到,抛出异常,Class Not Found
JVM垃圾回收(GC)算法:
1、标记-清除算法(Mark-Sweep):
从根对象开始遍历,标记无用对象,然后进行清除回收。
缺点:效率不高,内存碎片化严重
2、标记-复制算法(Copying):
按照容量划分二个大小相等的内存区域,当一块内存用完时,将其中活着的对象复制到另一块内存上,然后再将已使用的内存空间一次清理掉。
缺点:内存利用率低
3、标记-压缩算法(Mark-Compact):
标记可回收的对象后将所有存活的对象压缩并排列到内存的一端,然后对端边界以外的内存进行回收
缺点:效率低
4、总结:
(1)、内存效率:复制算法>标记清除算法>标记压缩算法
(2)、内存整齐度:复制算法=标记压缩算法>标记清除算法
(3)、内存利用率:标记压缩算法=标记清除算法>复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记-压缩算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记-清除多了一个整理内存的过程,总的来说,没有最好的算法,只有最合适的算法。
JVM堆内存的分代模型:
堆内存可细分为三个区域:次数上频繁收集 Young区,次数上较少收集 Old区,基本不动 Perm区
1、新生代(Young):
内存分成8(Eden伊甸区):1(survivor0生存0区):1(survivor1生存1区)
对象存活率低,GC采用复制算法,
2、老年代(Old):
对象存活率高,GC采用标记清除与标记压缩算法混合实现
3、永久代(Perm):
主要存放元数据,JDK1.8及之后无永久代,由元空间取代
备注:
当新生代的Eden区满了,将触发一次GC,这时还有被引用的对象,就会被分配到survivor0生存0区域,剩下没有被引用的对象就都会被清除。再一次触发GC时,Eden区和survivor0生存0区中的引用对象与存活对象会被一起移动到survivor1生存1区中,Eden区和survivor0生存0区中的未引用对象会被全部清除,然后再将survivor1生存1区移动到survivor0生存0区,接下来就是无限循环上面的步骤,当新生代中存活的对象过多,造成新生代内存已满,就会触发一次GC,此时会对整个新生代进行回收,将GC后存活的对象全部分配至老年代,老年代用来存储活时间较长的对象,当老年代中的内存不够时,就会触发一次GC,即FullGC,此时会对新生代和老年代所有对象的回收,这个回收采用标记-清理与标记压缩混合算法,该过程会相当慢,当Full GC之后发现依然无法进行对象的保存,就会产生OOM异常(OutOfMemoryError,堆内存溢出)。
垃圾回收器(GC)的选择:
随着堆内存大小的不断增大,GC也不断演进,对此JVM给了三种选择:串行收集器、并行收集器、并发收集器
1、串行收集器:
适用于几兆——几十兆内存
串行收集器用于小数据量的情况,采用Serial、Serial Old收集器,单线程STW(Stop-The-World)垃圾回收。
JDK5及之前版本默认采用——Serial+Serial Old收集器(复制算法)
2、并行收集器:
适用于几十兆——上百兆1G内存
并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等,采用Parallel、Parallel old、ParNew收集器,并行多线程。
JDK6/JDK7/JDK8版本默认使用——parallel+ Parallel old收集器(复制算法)
3、并发收集器:
适用于几十G内存
并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。采用CMS、G1等收集器。G1取消了分代模型,即物理上不分代,逻辑上分代 。
JDK9版本默认使用——G1收集器
4、备注:
(1)、GC选择参数设置:
Young |
Tenured |
JVM options |
Serial |
Serial Old |
-XX:+UseSerialGC |
Serial |
CMS+SerialOld |
-XX:-UseParNewGC -XX:+UseConcMarkSweepGC |
Parallel Scavenge |
Serial Old |
-XX:+UseParallelGC |
Parallel Scavenge |
Parallel Old |
-XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New |
Serial Old |
-XX:+UseParNewGC |
Parallel New |
CMS+SerialOld |
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 |
|
-XX:+UseG1GC |
(2)、STW:
Stop-The-World简称STW是在垃圾回收算法执行过程中,将jvm内存冻结、停顿的一种状态。当STW发生时,除了GC所需要的线程,其他的线程都将停止工作,中断了的线程只有等GC线程结束才会继续任务。STW是不可避免的,垃圾回收算法的执行一定会出现STW,而我们最好的解决办法就是减少停顿的时间,GC各种算法的优化重点就是为了减少STW,这也是JVM调优的重点。
(3)、分代模型:
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记清除与标记压缩混合算法进行垃圾回收。
并发收集器回收流程:
1、初始标记:
存在STW,只找到根对象,此时STW时间短,(在G1中标记对象是利用外部的bitmap来记录,而不是对象头)
2、并发标记:
对初始标记到的对象进行遍历标记
3、重新标记:
存在STW,修正后进行重新标记
4、并发清理:
CMS的并发标记:三色标记算法:
1、并发标记在遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:
(1)、白色:尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象。
(2)、黑色:本对象已经被GC访问过,且本对象的子引用对象也已经被访问过了(本对象的孩子节点也都被访问过)。
(3)、灰色:本对象已访问过,但是本对象的子引用对象还没有被访问过,全部访问完会变成黑色,属于中间态(本对象的孩子节点还没有访问)。
2、标记过程:
(1)、初始时,所有对象都在【白色集合】中;
(2)、将GC Roots 直接引用到的对象(即初始标记对象)挪到 【灰色集合】中;
(3)、从灰色集合中获取对象:
1)、将本对象引用到的其他对象全部挪到【灰色集合】中;
2)、将本对象挪到【黑色集合】里面。
(4)、重复步骤3,直至【灰色集合】为空时结束。
(5)、结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收
3、备注:
由于并发标记过程是在和用户线程并发运行的情况下,对象的引用处于随时可变的情况下,可能存在灰色对象指向白色对象的引用消失了,然后一个黑色的对象重新引用了白色对象,那么就会造成多标和漏标的问题,而CMS方案在并发标记时,一直存在的BUG就是易产生漏标,所以CMS的重新标记阶段,必须从头扫描一遍,STW过长。
G1的并发标记:分区算法:
1、G1取消了分代模型,即物理上不分代,逻辑上分代,采用分区算法,部分回收方式,
2、G1将堆划分为若干个区域( region),除此之外, G1还专门划分了一个 Humongous连续区,用来专门存放需要连续内存的巨型对象(对象占用的空间超过了分区容量50%以上)。
3、G1 从大局上看分为两大阶段,分别是并发标记和对象拷贝,在扫描region以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(因为其中存活对象较少,拷贝到新 region 中快)。
4、G1提供了两种 GC模式, YoungGC和 MixedGC,(young gc:指的是单单收集年轻代的 GC。mixed gc:指的是收集整个年轻代和部分老年代的GC。),其存在的问题在于一次回收会将整个新生代Eden伊甸区与survivor生存区全部回收,当年轻代很大时,造成回收的STW过长。
JVM调优——三大目的:
1、最短暂停时间(STW)与吞吐量:
(1)、并行收集器主要以到达一定的吞吐量为目标
(2)、并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。
举个例子:
方案一:每次 GC 停顿 100 ms,每秒停顿 5 次。
方案二:每次 GC 停顿 200 ms,每秒停顿 2 次。
两个方案相对而言第一个时延低,第二个吞吐高,基本上两者不可兼得。所以调优时候需要明确应用的目标,毕竟有句话叫:没有最好的,只有最合适的。在不考虑应付面试的因素,升级垃圾回收器确实会是最有效的方式之一,更高版本的垃圾回收器相当于是JVM开发人员对JVM做的优化,人家毕竟是专门做这个的,所以通常来说升级高版本的性能会有不少的提升。
2、OOM(OutOfMemoryError)错误调优:
在实际开发过程中,常见问题在于如何解决OOM(OutOfMemoryError)错误
(1)、尝试扩大内存,查看结果:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
(2)、分析内存,查看问题,借助JProfiler内存快照分析工具:
-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError
JProfiler内存快照分析工具作用:
1)、分析Dump内存文件,快速定位内存泄漏
2)、获得堆中的数据
3)、获得大的对象
JVM常用调优参数:
1、-Xms2g:初始化推大小为 2g;
2、-Xmx2g:堆最大内存为 2g;
3、-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
4、-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
5、–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
6、-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
7、-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
8、-XX:+PrintGC:开启打印 gc 信息;
9、-XX:+PrintGCDetails:打印 gc 详细信息;
10、-XX:+HeapDumpOnOutOfMemoryError:当JVM发生OOM时,自动生成DUMP文件。
JMM——Java内存模型:
1、JMM概述:
JMM与JVM是两个不同的概念。Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,定义了线程主内存与工作内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存——即:所有的变量都存储在主内存中,而主内存中的变量是所有的线程都可以共享的,但是对主内存中的变量进行操作时,必须在线程的工作内存进行操作,首先将主内存的变量copy到工作内存,进行操作后,再将变量刷回到主内存中,所有线程只有通过主内存来进行通信。
2、JMM存在的必要性——八大原子操作:
由于JVM运行时的实体是线程,如在多线程并发的情况下,则会出现线程安全问题,因此,JMM规范规定八大原子操作来保证一个变量在主内存与工作内存的执行顺序,从而完成数据同步:
(1)、lock(锁定):
作用于主内存的变量,把一个变量标记为一条线程独占状态
(2)、read(读取):
作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
(3)、load(载入):
作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(4)、use(使用):
作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(5)、assign(赋值):
作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(6)、store(存储):
作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
(7)、write(写入):
作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
(8)、unlock(解锁):
作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
注:
1)、不允许线程无原因的将变量从工作内存写回主内存(没有经过任何的assign)。
2)、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量(load或者assign)的变量,就是对一个变量进行use和store操作之前,必须先自行load与assign操作。
3)、一个变量在同一时刻只能被一个线程lock,同一个线程能多次lock,必须对应的多次unlock,lock与unlock必须成对的出现。
4)、如果对一个变量执行lock操作,将会清除工作内存中此变量的值,在执行引擎使用这个变量时,必须重新load、assign操作。
5)、如果一个线程对一个变量没有进行lock操作,则不允许unlock,也不允许对其他线程进行unlock。
6)、对一个变量进行unlock时,必须先将变量刷新到主内存。
3、JMM的三大特征:
JMM是围绕并发线程三大特征——原子性、可见性、有序性来建立的
(1)、原子性问题:
原子性指的是一个操作或多个操作是不可中断的,即使是在多线程环境下,一旦开始就不会被其他线程给打断,要么同时成功,要么同时失败,。
方案:
除了JVM自身提供的对基本类型的原子性操作以外,可以通过synchronized和Lock实现原子性。synchronized与lock在同一时刻始终只会存在一个线程访问对应的代码块。
(2)、可见性问题:
可见性指的是当一个共享变量被一个线程修改后,其他线程能够立即知道该变量的修改。
方案:
volatile关键字保证了可见性。当一个共享变量被volatile修饰时,它会保证共享变量修改的值立即被其他线程可见,即修改的值立即刷新到主内存,当其它线程去需要读取变量时,从主内存中读取。synchronized和Lock也保证了可见性。因为同一时刻只有一个线程能访问同步代码块,所以是能保证可见性。
(3)、有序性问题:
对于一个线程的代码而言,代码的执行总是从前往后依次执行的
方案:
volatile关键字保证了有序性,synchronized和Lock也保证了有序性(因为同一时刻只允许一个线程访问同步代码块,自然保证了线程之间在同步代码块的有序执行)。同时,JMM内部还定义了一套happens-before原则来保证多线程环境下两个操作间的原子性、可见性以及有序性
注:
happens-before原则:
1)、程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2)、锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3)、volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4)、线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
5)、传递性 A先于B ,B先于C 那么A必然先于C
6)、线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
7)、线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
8)、对象终结规则对象的构造函数执行,结束先于finalize()方法
4、synchronized 和 volatile 的区别:
(1)、volatile 是变量修饰符;synchronized是修饰类、方法、代码段。
(2)、volatile 仅能实现变量的修改可见性,不能保证原子性;synchronized 可以保证变量的修改可见性和原子性。
(3)、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5、synchronized与Lock的异同:
(1)、Lock是一个接口,而synchronized 是Java中的关键字
(2)、synchronized可以给类、方法、代码块加锁;而lock 只能给代码块加锁。
(3)、synchronized不需要手动获取锁和释放锁,发生异常会自动释放锁,不会造成死锁;而lock需要手动加锁Lock()和释放锁unLock(),如果没有unLock()去释放锁就会造成死锁,故Lock 使用时需要在finally块中释放锁。
(4)、通过Lock可以知道有没有成功获取锁,而synchronized 却无法办到。
(5)、Lock可以让等待锁的线程响应中断,而synchronized 却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
标签:变量,标记,对象,笔记,学习,线程,内存,JVM From: https://www.cnblogs.com/Iven-L/p/17146475.html