


1、运行时数据区域
数据区域的定义是:java虚拟机在执行java的过程当中会把它管理的内存划分为若干个不同的区域。
在JVM中、JVM的内存区域主要分为堆、方法区 、程序计数器、虚拟机栈、本地方法栈。
按照与线程的关系也可以分为
- 线程私有:每个线程单独拥有的一片内存区域
- 线程共享:被所有线程共享,只有一份
1.1线程私有
每个线程单独拥有的一片内存区域
1.1.1程序计数器
程序计数器是当前线程执行字节码指令的行号指示器,各个线程之间相互独立,互不影响。
主要用来记录各个线程执行的字节码的地址, 例如, 分支、 循环、 跳转、 异常、 线程恢复等都依赖于计数器 。
如果线程执行的是java的一个方法,那么程序计数器指向的是当前线程执行代码的字节码行数。
如果线程执行的是native方法,那么程序计数器的值为空。(执行native方法不是JVM执行,在操作系统也有相对应的程序计数器来记录本地代码的地址)
为什么会存在程序计数器:
cpu在执行指令时在一个确定的时候只会执行一条指令,一个线程当中会存在多个指令,而执行的线程数量超过cpu数量时,线程之间会根据时间片轮询机制进行争夺cpu资源,如果一个线程分配的时间用完了或者其他原因导致这个线程的cpu资源被抢夺,为了在下次该线程再次获得cpu资源时能恢复到正确的执行位置,那么该线程就需要一个程序计数器用于记录下一条运行的指令地址或行号。
1.1.2虚拟机栈Xss
栈(Stack) 作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表,遵循先进后出。先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
虚拟机栈是描述java方法执行的内存模型,线程在运行时每个方法都会被打包成一个栈帧,用于存储局部变量表、操作数栈、动态链接、完成出口等信息。然后将打包的栈帧放入到虚拟机栈中。而当前执行的方法就是虚拟机栈顶的栈帧。方法执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。
在编译程序代码的时候, 栈帧中需要多大的局部变量表, 多深的操作数栈都已经完全确定了, 并且写入到方法表的 Code 属性之中, 因此一个栈帧需要分配多少内存, 不会受到程序运行期变量数据的影响, 而仅仅取决于具体的虚拟机实现 。
栈的缺省大小在JDK1.5以后默认为1M ,可以通过JVM参数 -Xss调整大小,如-Xss2M。
栈帧 :在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。 一旦方法完成相应的调用,则出栈。
栈帧大体包含四个区域:局部变量表、操作数栈、动态链接、完成出口。
1、局部变量表
局部变量表用于存放方法当中的局部变量
2、操作数据栈
存放我们方法执行的操作数的
3、动态链接
既然是执行方法, 那么我们需要知道当前栈帧执行的是哪个方法, 栈帧中会持有一个引用(符号引用) , 该引用指向某个具体方法 。持有这个引用就是为了支持方法调用过程中的动态连接
4、完成出口
正常返回(调用程序计数器中的地址作为返回)
异常的话(通过异常处理器表<非栈帧中的>来确定)
1.1.3本地方法栈
本地方法栈与虚拟机栈发挥的作用十分相似,区别是虚拟机栈是用于执行java方法,而本地方法栈是执行native方法。
1.2线程共享
1.2.1方法区
方法区是用来存放已被虚拟机加载的类的相关信息。包括类信息,静态变量,常量、运行时常量池,字符串常量池等。
方法区是JVM的逻辑分区,在JDK1.7及以前称为永久代。在JDK1.8及以后称为元空间。
而当类加载到内存中后, JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中; 在解析阶段, JVM 会把符号引用替换为直接引用 。
常量池有很多概念, 包括运行时常量池、 class 常量池、 字符串常量池。
虚拟机规范只规定以上区域属于方法区, 并没有规定虚拟机厂商的实现。
严格来说是静态常量池和运行时常量池:
静态常量池是存放字符串字面量、 符号引用以及类和方法的信息。
运行时常量池存放的是运行时一些直接引用。运行时常量池是在类加载完成之后, 将静态常量池中的符号引用值转存到运行时常量池中, 类在解析之后, 将符号引用替换成直接引用。
在 HotSpot 虚拟机、 Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中, 其余部分则存储在 JVM 的非堆内存中(逻辑上还是属于方法区 ), 而 Java8 版本已经将方法区中实现的永久代去掉了, 并用元空间(class metadata) 代替了之前的永久代, 并且元空间的存储位置是本地内存
方法区的JVM参数设置:
jdk1.7及以前 永久代:
-XX:PermSize:初始值
-XX:MaxPermSize: 最大致
jdk1.8及以后 元空间:
-XX:MetaspaceSize:初始值
-XX:MaxMetaspaceSize : 最大值
1.2.2堆
堆(Heap)是JVM 上最大的内存区域, 我们创建的几乎所有的对象, 都是在这里存储的。
也是垃圾收集器进行垃圾收集的内存区域 。
堆空间一般在程序启动时就申请了,但并不一定会全部使用。
随着对象的频繁创建,堆空间的占用会越来越多,就需不定期对对不使用的对象进行回收。在java中,就叫做GC(Garbage Collection )
堆的JVM参数设置:
-Xms: 堆的最小值;
-Xmx: 堆的最大值;
-Xmn: 新生代的大小;
-XX:NewSize; 新生代最小值;
-XX:MaxNewSize: 新生代最大值;
-XX:SurvivorRatio: eden区与survivor区的比值。设置的值为多少就表示eden区占几份 survivor区固定2份(form/to各占一份),总份数为设置的值+2
新生代
eden
survivor to
survivor form
老年代
2、直接内存
操作系统上的除去被JVM占用的其他内存 ,剩余的物理内存
3、JVM占用内存信息
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
1 | nohup java -jar -Xms512M -Xmx2048M -XX:PermSize=512M -XX:MaxPermSize=1024M project.jar |
1 | -Xms 堆内存的初始大小,默认为物理内存的1/64 |
1 | 堆设置 |
jvm使用的最大内存是跟你自己电脑的物理内存有关,比如我电脑内存为8G,那么jvm最大的使用内存为8G的1/4约等于1797MB ,jvm的内存为8G的1/64约等于123MB
1 | public static void main(String[] args) { |
maxMemory()为JVM的最大可用内存,可通过-Xmx设置,默认值为物理内存的1/4,设值不能高于计算机物理内存;
totalMemory()为当前JVM占用的内存总数,其值相当于当前JVM已使用的内存及freeMemory()的总和,会随着JVM使用内存的增加而增加;
freeMemory()为当前JVM空闲内存,因为JVM只有在需要内存时才占用物理内存使用,所以freeMemory()的值一般情况下都很小,而JVM实际可用内存并不等于freeMemory(),而应该等于maxMemory()-totalMemory()+freeMemory()

线程占用内存信息
- jdk1.4默认的单个线程是占用256k的内存
- jdk1.5+默认的单个线程是占用1M的内存
- 可以通过-Xss参数设定,java线程占用jvm的内存

从图中看到当线程猛增时,堆内存也猛增,然后堆内存会迅速下降,这是因为堆上new了大量的对象,所以猛增,然后线程执行完后,对象被GC了,所以下降。
上面提到堆内存下降是因为线程执行完了,GC回收了new出来的对象。但从图中看出,堆内存下降后线程数并没有下降,这是为什么呢?
用过线程池的都知道,线程执行完后并不会立即销毁掉,会有一个保活时间,保活时间过了后才会销毁,so
我用的是jdk1.8,每个线程占用1M内存,如果是占用的堆内存,那堆内存应该会增加190M左右,但从图中看并没有,所以线程不是占用的堆内存空间。
实际上,java里每新起一个线程,jvm会向操作系统请求新起一个本地线程,此时操作系统会用空闲的内存空间来分配这个线程。所以java里线程并不会占用jvm的内存空间,而是会占用操作系统空闲的内存空间
4、1.8以后变化
Jdk1.6及之前: 有永久区, 常量池1.6在方法区
Jdk1.7: 有永久区,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后: 无永久区,常量池1.8在元空间,由元空间取代
为什么去掉永久代
- 现实使用中易出问题。
由于永久代内存经常不够用或者发生内存泄露,爆出异常 java.lang.OutOfMemoryError: PermGen 。
字符串存在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会位GC带来不必要的复杂度,而且回收效率偏低。
JDK 1.7 堆内存模型
JDK 1.8 堆内存模型
5、Jvm垃圾回收器
Young GC:只是负责回收年轻代对象的GC;
Old GC:只是负责回收老年代对象的GC;
Full GC:回收整个堆的对象,包括年轻代、老年代、持久带;
Mixed GC: 回收年轻代和部分老年代的GC (G1);
如何确定某个对象是“垃圾”?主流垃圾回收器都采用的是可达性分析算法来判断对象是否已经存活
在Java中采取了可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,但是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
常见垃圾回收算法
1.Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2.Copying(复制)算法(新生代使用多)
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
3.Mark-Compact(标记-整理)算法(压缩法)(老年代使用多)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)。
典型的垃圾回收器
主要的垃圾回收器参考:https://blog.csdn.net/u013568373/article/details/94383414

并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1

Serial 收集器(新生代)
Serial 即串行的意思,也就是说它以串行的方式执行,它是单线程的收集器,只会使用一个线程进行垃圾收集工作,GC 线程工作时,其它所有线程都将停止工作。
使用复制算法收集新生代垃圾。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率,所以,它是 Client 场景下的默认新生代收集器。
显式的使用该垃圾收集器作为新生代垃圾收集器的方式:-XX:+UseSerialGC

ParNew 收集器(新生代)
就是 Serial 收集器的多线程版本,但要注意一点,ParNew 在单核环境下是不如 Serial 的,在多核的条件下才有优势。
使用复制算法收集新生代垃圾。
Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。

Parallel Scavenge 收集器(新生代)
同样是多线程的收集器,其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是提高吞吐量(吞吐量 = 运行用户程序的时间 / (运行用户程序的时间 + 垃圾收集的时间))。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
使用复制算法收集新生代垃圾。
显式的使用该垃圾收集器作为新生代垃圾收集器的方式:-XX:+UseParallelGC

Serial Old 收集器(老年代)
Serial 收集器的老年代版本,Client 场景下默认的老年代垃圾收集器。
使用标记-整理算法收集老年代垃圾。
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseSerialOldGC

Parallel Old 收集器(老年代)
Parallel Scavenge 收集器的老年代版本。
在注重吞吐量的场景下,可以采用 Parallel Scavenge + Parallel Old 的组合。
使用标记-整理算法收集老年代垃圾。
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseParallelOldGC

CMS 收集器(老年代)
CMS(Concurrent Mark Sweep),收集器几乎占据着 JVM 老年代收集器的半壁江山,它划时代的意义就在于垃圾回收线程几乎能做到与用户线程同时工作。
使用标记-清除算法收集老年代垃圾。
工作流程主要有如下 4 个步骤:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(Stop-the-world)
- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿
- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(Stop-the-world)
- 并发清除: 清理垃圾,不需要停顿
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
但 CMS 收集器也有如下缺点:
- 吞吐量低
- 无法处理浮动垃圾
- 标记 - 清除算法带来的内存空间碎片问题
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseConcMarkSweepGC

G1 收集器(新生代 + 老年代)
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
使用复制 + 标记 - 整理算法收集新生代和老年代垃圾。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
显式的使用该垃圾收集器作为老年代垃圾收集器的方式:-XX:+UseG1GC

6、双亲委派机制
https://blog.csdn.net/nsx_truth/article/details/109145080
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且,加载某个类的class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式
工作原理
(1)如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行
(2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器;
(3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
(4)父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
双亲委派机制优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String (没用)
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
7、MinorGC过程
用的复制算法:
- 保证空间的连续性,不会出现”碎片”问题。
- 没有标记和清除过程,实现简单,运行高效
内存划为1个Eden区,2个Survivor区(一块叫From,一块叫To),其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存。
触发时机
Eden区满时会触发,但是Survivor区域满了不会引发GC
过程:
- 创建一个对象时候,对象会先放到Eden区中

假如新生代的Eden区或者Survivor1/2区都快满了,此时就会触发Minor GC,把存活对象转移到Survivor1/2区去。原来的对象年龄+1,此时就会把Eden区中的存活对象都一次性转移到一块空着的Survivor区。接着Eden区就会被清空。

进入老年带:
- 对象每次在新生代里躲过一次MinorGC然后被转移到一块Survivor区域中,那么它的年龄就会增长一岁,默认的设置下,当对象的年龄达到15岁的时候,也就是躲过15次GC的时候,他就会转移到老年代里去。这个具体是多少岁进入老年代,可以通过JVM参数-XX:MaxTenuringThreshold”来设置,默认是15岁。
- 或者是一批对象的总大小超过了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。就是实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代。
- 大对象直接进入老年带:**-XX:PretenureSizeThreshold**字节数,创建大于这个对象的字节数的对象也移动到老年带
