其他
进阶 | JVM 深入解析(12000 字总结)
点击上方 Java后端,选择 设为星标
优质文章,及时送达
工作之余,想总结一下JVM相关知识。
Java运行时数据区
Java虚拟机在执行Java程序的过程中会将其管理的内存划分为若干个不同的数据区域,这些区域有各自的用途、创建和销毁的时间,有些区域随虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束来建立和销毁。
Java虚拟机所管理的内存包括以下几个运行时数据区域,如图:
1、程序计数器:指向当前线程正在执行的字节码指令。线程私有的。
2、虚拟机栈:虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。
(1)栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接
局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。 返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。 操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。 动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
调用本地native的内存模型 线程独享。
是方法区的一部分 存放编译期生成的各种字面量和符号引用 Class文件中除了存有类的版本、字段、方法、接口等描述信息,还有一项是常量池,存有这个类的 编译期生成的各种字面量和符号引用,这部分内容将在类加载后,存放到方法区的运行时常量池中。
Java堆是虚拟机管理的内存中最大的一块 Java堆是所有线程共享的区域 在虚拟机启动时创建 此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组 Java堆是垃圾收集器管理的内存区域,因此很多时候称为“GC堆”
JMM Java内存模型
线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。 不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。 主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。
JMM决定一个线程对变量的写入何时对另一个线程可见。 线程之间共享变量存储在主内存中 每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。 JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。
当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。 保证线程的有序执行,这个为有序性。(保证线程安全)
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。 unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。 load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中 use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。 assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。 store(存储):把工作内存的变量的值传递给主内存 write(写入):把store操作的值入到主内存的变量中
6.1、注意:
不允许read、load、store、write操作之一单独出现 不允许一个线程丢弃assgin操作 不允许一个线程不经过assgin操作,就把工作内存中的值同步到主内存中 一个新的变量只能在主内存中生成 一个变量同一时刻只允许一条线程对其进行lock操作。但lock操作可以被同一条线程执行多次,只有执行相同次数的unlock操作,变量才会解锁 如果对一个变量进行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assgin操作初始化变量的值。 如果一个变量没有被锁定,不允许对其执行unlock操作,也不允许unlock一个被其他线程锁定的变量 对一个变量执行unlock操作之前,需要将该变量同步回主内存中
堆的内存划分
使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。 分为Eden、Survivor From、Survivor To,比例默认为8:1:1 内存不足时发生Minor GC
采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。
Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。 MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
GC垃圾回收
一、 判断对象是否要回收的方法:可达性分析法
虚拟机栈(栈帧中本地变量表)中引用的对象 方法区中静态属性引用的对象 方法区中常量引用的对象 本地方法栈中Native方法引用的对象
第一个阶段是可达性分析,分析该对象是否可达 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)
该类的所有实例被回收 加载该类的ClassLoader被回收 该类的Class对象没有被引用
GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存。 可以在该方法里面,指定一些对象在释放前必须执行的操作。
二、 发现虚拟机频繁full GC时应该怎么办:
full GC指的是清理整个堆空间,包括年轻代和永久代
首先用命令查看触发GC的原因是什么 jstat –gccause 进程id 如果是System.gc(),则看下代码哪里调用了这个方法 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令 如果是GC locker,可能是程序依赖的JNI库的原因
三、常见的垃圾回收算法:
思想:把堆分成新生代和老年代。(永久代指的是方法区)
因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。 由于老年代每次只回收少量的对象,因此采用mark-compact算法。 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量
当JVM无法为一个新的对象分配内存的时候,越容易触发Minor GC。所以分配率越高,内存越来越少,越频繁执行Minor GC 执行Minor GC操作的时候,不会影响到永久代(Tenured)。从永久代到年轻代的引用,被当成GC Roots,从年轻代到老年代的引用在标记阶段直接被忽略掉。
HotSpot 虚拟机详解
假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为“指针碰撞” 假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表“。 使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。 分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UseTLAB参数决定)
<init>
方法,把对象按照程序员的意愿进行初始化。使用句柄访问的好处是引用中存放的是稳定的句柄地址,当对象被移动(比如说垃圾回收时移动对象),只会改变句柄中实例数据指针,而引用本身不会被修改。 使用直接指针,节省了一次指针定位的时间开销。
HotSpot使用一组称为OopMap的数据结构。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在栈和寄存器中哪些位置是引用。这样子,在GC扫描的时候,就可以直接知道哪些是可达对象了。
HotSpot只在特定的位置生成OopMap,这些位置称为安全点。 程序执行过程中并非所有地方都可以停下来开始GC,只有在到达安全点是才可以暂停。 安全点的选定基本上以“是否具有让程序长时间执行“的特征选定的。比如说方法调用、循环跳转、异常跳转等。具有这些功能的指令才会产生Safepoint。
抢占式中断:在GC发生时,首先把所有线程中断,如果发现有线程不在安全点上,就恢复线程,让它跑到安全点上。 主动式中断:GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就自己中断挂起。轮询标记的地方和安全点是重合的。
因为GC先进行可达性分析。可达性分析是判断GC Root对象到其他对象是否可达,假如分析过程中对象的引用关系在不断变化,分析结果的准确性就无法得到保证。
初始标记:标记GC Root能直接引用的对象 并发标记:利用多线程对每个GC Root对象进行tracing搜索,在堆中查找其下所有能关联到的对象。 重新标记:为了修正并发标记期间,用户程序继续运作而导致标志产生变动的那一部分对象的标记记录。 并发清除:利用多个线程对标记的对象进行清除
对CPU资源占用比较多。可能因为占用一部分CPU资源导致应用程序响应变慢。 CMS无法处理浮动垃圾。在并发清除阶段,用户程序继续运行,可能产生新的内存垃圾,这一部分垃圾出现在标记过程之后,因此,CMS无法清除。这部分垃圾称为“浮动垃圾“ 需要预留一部分内存,在垃圾回收时,给用户程序使用。 基于标记-清除算法,容易产生大量内存碎片,导致full GC(full GC进行内存碎片的整理)
JVM优化
-XX:PetenureSizeThreshold=1000000
,单位为B,标明对象大小超过1M时,在老年代(tenured)分配内存空间。-XX:MaxTenuringThreshold
设置。如果想让对象留在年轻代,可以设置比较大的阈值。-XX:MinHeapFreeRatio
参数用于设置堆空间的最小空闲比率。默认值是40,当堆空间的空闲内存比率小于40,JVM便会扩展堆空间-XX:MaxHeapFreeRatio
参数用于设置堆空间的最大空闲比率。默认值是70, 当堆空间的空闲内存比率大于70,JVM便会压缩堆空间。当-Xmx和-Xmx相等时,上面两个参数无效
-XX:+UseParallelGC
:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能的减少垃圾回收时间。-XX:+UseParallelOldGC
:设置老年代使用并行垃圾回收收集器。
-XX:+LargePageSizeInBytes
设置内存页的大小jps(Java Process Status):输出JVM中运行的进程状态信息(现在一般使用jconsole) jstack:查看java进程内线程的堆栈信息。 jmap:用于生成堆转存快照 jhat:用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer) jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。 VisualVM:故障处理工具
类加载机制
一、 概念
类文件的结构检查:检查是否满足Java类文件的固定格式 语义检查:确保类本身符合Java的语法规范 字节码验证:确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法。 二进制兼容性验证:确保相互引用的类之间是协调一致的。
二、双亲委派模型
由同一个类加载器加载并且拥有相同包名的类组成运行时包 只有属于同一个运行时包的类,才能访问包可见(default)的类和类成员。作用是 限制用户自定义的类冒充核心类库的类去访问核心类库的包可见成员。
三、特点:
三、 类加载器:
C++编写的,程序员无法在程序中获取该类 负责加载虚拟机的核心库,比如java.lang.Object 没有继承ClassLoader类
Java编写的,从指定目录中加载类库 父加载器是根类加载器 是ClassLoader的子类 如果用户把创建的jar文件放到指定目录中,也会被扩展加载器加载。
Java编写的 父加载器是扩展类加载器 从环境变量或者class.path中加载类 是用户自定义类加载的默认父加载器 是ClassLoader的子类
Java.lang.ClassLoader类的子类 用户可以定制类的加载方式 父类加载器是系统加载器 编写步骤:
可以从指定位置加载class文件,比如说从数据库、云端加载class文件 加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。
问题:Java程序对类的执行有几种方式:
创建类的实例 读写某个类或者接口的静态变量 调用类的静态方法 同过反射的API(Class.forName())获取类 初始化一个类的子类 JVM启动的时候,被标明启动类的类(包含Main方法的类)
当Java虚拟机初始化一个类时,不会初始化该类实现的接口。 在初始化一个接口时,不会初始化这个接口父接口。 只有当程序首次使用该接口的静态变量时,才导致该接口的初始化。
调用Classloader的loadClass方法去加载一个类,不是主动使用,因此不会进行类的初始化。
有JVM自带的三种类加载器(根、扩展、系统)加载的类始终不会卸载。因为JVM始终引用这些类加载器,这些类加载器使用引用他们所加载的类,因此这些Class类对象始终是可到达的。 由用户自定义类加载器加载的类,是可以被卸载的。
JDK :Java Development Kit,开发的时候用到的类包。 JRE :Java Runtime Environment,Java运行的基础,包含运行时需要的所有类库。
作者:Huangy远
链接:https://segmentfault.com/a/1190000014395186