1:类加载子系统
见另一篇独立文章
2:执行引擎
解释器 -- JVM根据定义规范对字节码采用逐行解释的方式执行。
HotSpot中 Interpreter模块 :实现了解释器的核心功能。
Code 模块:用于管理 HotSpot 在运行时生成的本地机器指令
JIT 编译器 (后端运行期编译器)-- jvm 将源码直接编译成和本地机器平台相关的语言。 把字节码转换成机器码。
JIT 平台支持一种叫即时编译的技术,将整个函数体编译成机器码,每次执行时直接执行机器码即可。提升效率。
JAVAC(sun公司,前端编译器) 把 java文件转化为 .class文件。
3:java 运行时数据区域:
![](https://i-blog.csdnimg.cn/blog_migrate/5ce203293442f80653802fc09b4972cf.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bfaf828f6bd4d517c8e344acfa85efc8.png)
程序计数器:
私有
内存空间小,私有线程,字节码解释器工作就是通过改变计数器的值来选取下一条需要执行的字节码指令,同时 分支,循环,跳转,异常处理,线程恢复
都需要依赖计数器来完成。
如果线程正在执行一个 java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,即(非java方法编译成DLL由JAVA调用)
则值为 Undefined,此内存区域是java唯一未进行javaOutMemoryError处理的区域。
Java 虚拟机栈: 私有
一种数据结构模型,存储 局部变量表,操作数栈,动态链接,方法出口等信息。线程独享。
线程私有,描述的是java方法执行的内存模型。
每个方法在执行的时候都会创建一个栈帧(Stack Frame) 用于存储 局部变量表,操作数栈,动态链接,方法出口等信息。
问题: int m = 6 6 存在堆还是在栈中???
局部变量表:存放编译期可知的数据类型,对象引用和字节码指令地址。 但是如果方法中的局部变量用final修饰后,不在放进局部变量表,而是堆中。
如 int m = 6, 数据类型 int ,对象引用 6 , 字节码 指令地址
0x2d
从局部变量
3
中装载引用类型值入栈。
每个方法从调用到结束,对应一个栈帧从虚拟机栈中由入栈到出栈的过程。正因为这样,栈才有后进先出的特点。比如方法内方法的执行。
StackOverFlowError:线程请求的栈深度大于虚拟机栈的深度。
在方法调用的时候由创建的栈帧压入虚拟机栈的过程中,操作系统给JVM分配的内存固定,JVM 给虚拟机栈分配内存固定,如果方法调用过多,
导致虚拟机栈内存满后溢出。虚拟机栈深度就是入栈的栈帧数量大小。
OutOfMemeoryError:如果虚拟机栈可以扩展,但在扩展的过程中无法申请到足够的内存报错。
首先 JVM 是无法实现虚拟机栈动态扩展的。
动态扩展通常有两种方法:Segmented Stack 和 Stack Copying
Segmented Stack :一个双向链表把多个栈连接起来,一开始只分配一个栈,当空间不够的时候再分配一个,用链表连接起来。
Stack Copying:在栈不够的时候,分配一个更大的栈,然后把原来的栈复制过去。
其次 一般栈大小 默认1M,一般设置256K,过大会浪费内存空间,导致没法新建更多线程。
配置栈内存:
-Xss
例子:
public class Memory {
public static void main(String[] args) { //开始执行main方法,在虚拟栈中压入栈底(入栈)
int i=1; //创建局部变量 i = 1 放入 main 方法的方法栈中
Object obj = new Object(); // 在堆中 新建一个 Object 对象,栈里存放引用 Refrence
Memory mem = new Memory(); // 在堆中 新建一个 Memory 对象,栈里存放引用 Refrence
mem.foo(obj); //新建一个 foo 方法的栈帧 包含局部变量 obj参数等
} // main 执行结束,销毁堆栈,如果是线程复用,不会销毁堆栈,只是全部出栈结束。
private void foo(Object param) { // 因为java值传递 所以新建一个新的 param 引用
String str = param.toString(); //在堆上创建一个字符串对象,并在栈上新建一个引用
System.out.println(str);
} // foo 执行完毕
}
本地方法栈: 私有
是为虚拟机使用到的Native方法服务,即
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。
然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
Java堆:
线程共享
JVM管理的内存中最大的一块,线程共享,主要存放对象实例和数组。
OutMemoryError:如果堆中没有内存来进行对象实例分配,并且堆再也无法扩展时报异常。
一个进程只有一个JVM,一个JVM实例只有一个堆(head),该进程下所有线程共享该堆。几乎所有对象实例都在堆分配,还有些在栈上分配(逃逸分析)。
如:
![](https://i-blog.csdnimg.cn/blog_migrate/e9dce6466fa9635ab529137bcb3fb65e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/505796f4ff2d04fdf94b00a3d97a75e1.png)
设置堆大小:
-Xms10m -Xmx20m 起始内存 10m 最大内存 20m 超过 OutOfMemoryError,通常将这俩值配置相同,为了能在GC清理完堆区后不需要重新分割计算堆区大小。
默认: Xms = 物理内存/64 Xmx = 物理内存/4
检查堆内存 使用 jdk 自带工具 java VisualVM
long
initialMemory
=
Runtime
.
getRuntime
(
)
.
totalMemory
(
)
/
1024
/
1024
;
//返回Java虚拟机中的堆内存总量 M
long
maxMemory
=
Runtime
.
getRuntime
(
)
.
maxMemory
(
)
/
1024
/
1024
;
//返回Java虚拟机试图使用的最大堆内存量 M
java 堆区对象: 年轻代: Eden空间 和 Survivor0空间,Survivor1空间 默认比例为8:1,
年轻代垃圾回收算法是复制算法,基本思想是将内存分为两块,每次只用一块,用完后将还活着的复制到另一块上。不会生成内存碎片。
老年代 清理算法--整理算法,即让所有存活的对象都向一端移动,直接清理掉边界以外的内存。
JVM新创建对象基本都在 Eden区,经过第一次 Minor GC后,存活的放到 Survivor(from)区,在 from每活一次增加一岁,到一定年纪移动到老年代。
每次GC都会清空 Eden 和From,都放到to,然后将to和from调换。
所以说每次GC后Eden都会清空。?
分代是为了优化GC性能,不分代也可以。
优化:对象晋升老年代的年龄阀值,可以通过选项**-XX:MaxTenuringThreshold**来设置
![](https://i-blog.csdnimg.cn/blog_migrate/687431ba46e24380ce266822f3efbfa9.png)
方法区: 线程共享
保存被加载过的每一个类信息,是类加载器在加载类的时候从类源文件中抽取出来的 class的元数据, static 变量信息也保存在这里。同样有垃圾收集机制。
包含内容:
配置方法区内存大小: -XX:MaxPermSize
类型信息 |
类的完整名称,如 java.lang.String
|
|
类的直接父类的完整名称
|
|
类的直接实现接口的有序列表,(一个类直接实现接口不止一个)
|
|
类的修饰符
|
类型的常量池
(运行常量池)
|
每一个class文件中,都维护着一个常量池,(不要与方法区的运行常量池混),里面存放编译时期生成的各种 字面值和符号引用,这个常量池的内容在类加载的时候被复制到方法区的运行常量池中。
|
字段信息
|
声明的顺序,修饰符,类型,名称
|
方法信息
|
声明的顺序 , 修饰符, 返回值类型, 名字, 参数列表, 异常表, 方法字节码, 操作数栈和局部变量表大小。
|
类变量
|
static 变量 非 final类变量,JVM使用一个类前,必须在方法区为每个类变量分配空间。final类放在常量池中。
|
方法表
|
|
如:
![](https://i-blog.csdnimg.cn/blog_migrate/30c459738e315dc92491beb1d470c06a.png)
4:对象内存分配规则
![](https://i-blog.csdnimg.cn/blog_migrate/b0adfff205117d01b1690f0341cd02cf.png)
新对象初次存入堆中,会判断Eden是否吃下,吃不下触发YGC(Minor GC)进行垃圾整理,将Eden 清空,from 放入to中。然后再将对象放入Eden,
如果放不下判断to是否放下,放得下就放在to(这里如果to放不下,新生代直接晋升老年代),然后等待YGC之后将 to变为 from。同时判断原来to中有超过阀值升级为老年代。
如果 to 还放不下判断为超大对象,直接放入老年代,如果老年代放不下触发 FGC,之后再放不下 OOM。
5: 垃圾回收
System.gc() 或者 Runtime。getRuntime().GC() 会 触发 FullGC,同时对老年代和新生代进行回收。
4.1 垃圾回收机制(尽量避免垃圾回收)
分代收集思想: Minor GC(年轻代) : Eden 和 Survivor 区域
Major GC:清理老年代
Full GC:清理整个堆空间 触发情况 1 System.gc() 2 老年代空间不足 3 方法区空间不足 4 Minor GC后进入老年代大小大于老年代空间。
当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程。
4.2 垃圾回收器
名称
|
特点
|
算法
|
指定方法
| 描述 |
Serial GC
|
1999年jdk1.3.1 出生,串行回收
|
新生代--复制算法
老年代--整理算法
|
-XX:+UseSerialGC
| 无交互开销,速度快,对于交互较强 应用不适用。 |
ParNewGC
|
和 Serial 一样,并行回收区别
|
新生代--并行
老年代--串行
|
-XX:+UseParNewGC
|
|
Parallel GC
|
吞吐量优先并可控。自适应调节策略
|
标记压缩算法
|
-XX:+UseParallelGC 手动指定年轻代使用此收集器
-XX:+useParallelOldGC 手动指定老年代,jdk8 默认开启
-XX:+useParallelGCThreads 设置年轻代收集器线程数,一般和cpu数量相同,>8 = 3+(5*n/8)
-XX:MaxGCPauseMillis 设置收集器最大停顿时间(毫秒) 慎用
-XX:GCTimeRatio 垃圾收集占总时间比,默认99 0-100,不超过1%
-XX:+UseAdaptiveSizePolicy 开启自适应调节策略
|
|
CMS 回收器
|
并发收集,低延迟,jdk 1.5 推出 Concurrent Mark Sweep 并发标记清除,让GC和线程一同工作。
当内存使用率到一定阀值开始工作,并非老年代满
|
|
-XX:+useConcMarkSweepGC 手动指定CMS收集器 开启后自动打开 ParNew GC(处理yong区) + CMS(处理Old区)+SerialGC 组合
-XX:CMSInitiatingOccupanyFraction 设置堆内存使用率阀值, jdk5 默认68 ,jdk6 以上默认92%
|
会有内存碎片,对CPU资源敏感,并发阶段会占用一部分资源,无法处理浮动垃圾。
|
G1
|
区域化分代式 在延迟可控情况下,获取尽可能高的吞吐量,全功能收集器。并行回收器。 jdk1.7 后正式使用。jdk9 后默认垃圾回收器。
|
|
|
|
|
1:把堆内存分割为很多不相关的区域 Region,使用不同的Region来表示Eden,s0,s1。 G1跟踪每个Region并根据垃圾大小维护一个优先列表,优先回收价值最大的Region
|
Region之间复制算法
整体 标记压缩算法
|
-XX:useG1GC 在 jdk1.8 启用
-XX:G1HeapRegionSize Region大小设置,值是2 的幂,范围 1-32m之间,默认是堆内存的2000/1
-XX:MaxGCPauseMillis 期望最大GC停顿时间,默认200ms
-XX:ParallelGCThread STW线程值,最大8
|
两种算法都避免碎片,尤其是java 堆很大的时候优势更加明显。
|
|
常见调优: 1 开启G1
2 设置堆内存
3 设置最大停顿时间
|
|
|
|
Shenandoah GC
|
jdk 12 低延迟时间,会造成吞吐量下降。
|
|
|
|
说明: 如果想要最小化使用内存和并行开销,选择 Serial GC
如果最大化应用程序的吞吐量,选择 ParallelGC
如果想要最小化的GC中断或停顿时间,选择CMS。
G1 不具备全方位优势,小内存用 CMS,大内存用G1,平衡点 6-8G。
![](https://i-blog.csdnimg.cn/blog_migrate/505796f4ff2d04fdf94b00a3d97a75e1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1c1be489d8a710064be161dae8043529.png)
![](https://i-blog.csdnimg.cn/blog_migrate/eab4f929711ad8fc0dc96f1ed9cc89d3.png)
6: 逃逸分析与 TLAB
逃逸: 是指在方法体内创建的对象,除了在方法体内被引用外, 在方法体外还被其他变量引用到,这样带来的后果是在该方法执行完毕后, 该方法创建的
对象将无法被GC回收。
逃逸分析: 是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java HotSpot 可以分析出一个新的对象的引用使用范围从而决定是否要将这个对象分配到堆上。
参数设置:
-XX:+DoEscapeAnalysis //使用 (JDK8中,逃逸分析默认开启。)
-XX:-DoEscapeAnalysis //不用
-XX:+PrintEscapeAnalysis //结果展示
静态编译:javac之类的编译
动态编译:JIT等即时编译
逃逸状态: 1 GlobalEscape(全局逃逸) 一个对象的引用逃出了方法或线程。
2 ArgEscape(参数逃逸) 在方法调用过程中传递一个对象的引用给下一个方法
3 NoEscape(无逃逸) 一个可以进行标量替换的对象,可以不将这种对象分配在传统的堆上。
class Main {
public static void main(String[] args) {
example();
}
public static void example() {
Foo foo = new Foo(); //alloc
Bar bar = new Bar(); //alloc
bar.setFoo(foo); //在本例当中,编译器通过逃逸分析,可以知道Bar对象没有逃出example()方法,因此这也意味着Foo也没有逃出example方法 因此,编译器可以将这两个对象分配到栈上。
}
}
class Foo {}
class Bar {
private Foo foo;
public void setFoo(Foo foo) {
this.foo = foo;
}
}
编译期针对逃逸结果分析的优化:
1 堆分配对象变成栈分配对象,极大减轻了堆内存分配压力。
2 消除同步。逃逸分析可判断某个对象是否始终被一个线程访问,如果确定的话转换成没有同步保护的操作,提高并发操作和性能,
因为线程同步的代价是很高的。
3 矢量替代。 如果逃逸分析方法发现对象的内存存储结构不需要连续进行,可将对象部分甚至全部都保存在CPU寄存器中,大大提高访问速度。
程序编写时: 尽量做到避免对象的逃逸。
如:
![](https://i-blog.csdnimg.cn/blog_migrate/8357106e4c10f17749e08f19fb02a155.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9d837e10330baa6973e19f96c5d9da9c.png)
逃逸分析未开启:
-
server
-verbose:gc
JVM执行了GC操作
开启逃逸分析
:-server -
verbose:gc -
XX:+DoEscapeAnalysis
在开启逃逸分析情况下,JVM并没有执行GC操作。同时,操作时间上,开启逃逸分析的程序运行时间是未开启逃逸分析时间的1/5
逃逸分析的效果只能在特定场景下,满足高频和高数量的容量比较小的变量分配结构,才可以生效。
jdk1.6 开启逃逸: -server -XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10 Eclipse/MyEclipse自身的JVM参数配置是在ini的文件中
关闭逃逸: -server -XX:-DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m
LTAB:在java 堆的 Eden中开辟一小块私有区域。默认设定为占用 Eden Space 的1%,java中很多小对象适合快速GC不存在线程共享,会优先分配在 TLAB上。
Java中每个线程都会有自己的缓冲区称作TLAB。
![](https://i-blog.csdnimg.cn/blog_migrate/768e486fc681a4e1e261cf418c0dcfeb.png)
7:StringTable 字符串常量池
两种创建方式: String s = "123";//字面量定义方式 直接存储在常量池中
String s = new String("123");//new 对象方式 存储在堆中 可以用String 提供的intern() 方法
intern方法:当前new出来的字符串对象可以使用 intern 方法从常量池中获取,如果常量池不存在的话就新建一个这样的字符串放在常量池。
如果适当的使用String s=new String("1").intern();
能够适当的获取常量池的常量。
特点: final 类型,不可被继承-- 对字符串重新赋值时需要重写指定内存区域赋值。
实现了Serializable 接口,支持序列化。实现了Comparable接口,可以比较大小。
在 jdk1.8 以及以前内部定义 final char[] value 存储字符串数据,jdk9 改为 byte[]
底层结构: Hashtbale
jdk1.6 Stringtable是固定的,长度为 1009 ,使用 -XX:StringTablesize 可以设置 如
-XX:StringTableSize=1009
jdk1.7 StringTable 默认长度 60013,Stringtablesize 无要求
jdk1.8 StringTable 默认长度 600013,StringTablesize 最低 1009
内存分配:
jdk1.6 存放在永久代中
jdk1.7 保存在 java 堆中
jdk1.8 元空间,字符串常量池在堆
![](https://i-blog.csdnimg.cn/blog_migrate/5343fe81d7ae7cb0fcac4662bc9a3945.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1c3942da16f89266635002db119d2bac.png)
如:
![](https://i-blog.csdnimg.cn/blog_migrate/1910e7890b23ff2fc26c2c2e0e1a23cc.png)
注意 s3.intern() 方法已经将 字符串 "11" 放到常量池中,所以和s4一样。
![](https://i-blog.csdnimg.cn/blog_migrate/68b250f7009249838bf784a37b3450c7.png)
![](https://i-blog.csdnimg.cn/blog_migrate/db8611ef7931624d4551b457e18e5332.png)
为了不浪费heap的内存StringPool中的S3就直接引用了对象S3,所以S3与S4的地址是一致的
注意:== 和 equals == 是比较的两个变量本身的值,即两个对象在内存中的首地址 equals 比较的是字符串内容。
1.7 中在调用 intern() 方法时候,如果str 不存在与常量池中,则会把str在堆中的引用存入常量池中,节省内存。