写在前面
在虚拟机中,内存被分为不同的区域,大致包括:
- 堆
- 栈
- 方法区
- 运行时常量池
- 本机直接内存
- 程序计数器
在c/c++编程的时候我们需要把自己申请的内存回收掉,不然会导致内存泄露。但是在java中是不需要的,因为有gc线程会帮我们将不需要的对象回收掉,但这样经常会出现问题。
我们最开始知道的Java的字节码是解释执行的,然后会对热的代码编译成本地机器码执行。这样做有两个好处:
- 解释执行使得启动的速度非常快
- 选择性地编译成本地代码又使执行的效率没有太差
这样的处理方式使得java能适应跟多的应用场景。
数据
在JVM中有很多种数据,下面依次来看。
对象
- E
- S0
- S1
- O
- Perm
对象基本上都是在堆上分配的,相关的参数有:
- -Xms:初始堆大小
- -Xmx:堆的最大值
- -Xmn:年轻代的大小
分配对象时,现在TLAB(Thread Local Allocation Buffer)中分配:
- -XX:TLABWasteTargetPercent:TLAB挪借的E的空间的百分比(默认1%)
- -XX:+UseTLAB
- -XX:SurvivorRatio:E、S的比例,默认是8
- -XX:NewRatio:E和O的比例
有时候我们认为大的对象一般活的比较久(如果不是这样的话是不是代码有问题?),那么新对象可能直接进O区了:
- -XX:PretenureSizeThreshold:直接在O中分配的阀值
下面接着来看类信息。
类信息
首先来看类信息包含的内容:
- 类的结构信息
- 字段描述
- 方法描述
- 常量表
这些东西都放在方法区,需要注意的是在运行期也可以往方法区塞东西,比较常见的有:
- ClassLoader不停地加载类
- String.intern()
可以在jvm启动的时候来设置方法区的大小:
- -XX:PermSize
- -XX:MaxPermSize
这部分数据相关的GC参数有:
- -XX:+CMSClassUnloadingEnabled
- -XX:+UseCMSInitiatingOccupancyOnly
- -XX:CMSInitiatingPermOccupancyFraction=80
变量
简单类型变量的生命周期随着方法的返回而结束,那么对应的内存也会被释放掉,所以这部分不会涉及到GC,栈的大小可以设置:
- -Xss:每个线程的堆栈的大小
- 局部变量区
- 操作数
- 帧数据
垃圾回收
和GC相关的参数很多,首先如果想观察GC的信息可以设置参数:
- -verbose:gc
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -XX:+PrintGCApplicationStoppedTime
- -XX:+PrintGCApplicationConcurrentTime
- -XX:+PrintHeapAtGC
- -XX:+PrintClassHistogram
- -XX:+PrintTLAB
在发生OOM的时候我们需要查看到底发生了什么,那么可以将堆dump到文件中:
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=~/heap.hprof
- -XX:ParallelGCThreads:并行GC时进行内存回收的线程数
- -XX:GCTimeRatio:GC时间占总时间的比率
- -XX:MaxGCPauseMills:GC最大的停顿时间
- -XX:CMSInitiatingOccupancyFraction:出发GC的老年代使用率,默认为68%
- -XX:UseCMSCompactAtFullCollection:在CMS完成后进行碎片整理
- -XX:CMSFullGCsBeforeCompaction:CMS多少次之后进行一次碎片整理
- -XX:HandlePromotionFailure:设置是否允许分配担保失败
- -XX:ParallelGCThreads:并行GC时进行内存回收的线程数
我们要查看GC日志的时候基本是先从日志里面看,YoungGC的日志如下:
[ParNew: 2327071K->122000K(2403008K), 0.0942260 secs] 3080564K->891474K(3975872K), 0.0944310 secs] [Times: user=0.39 sys=0.00, real=0.10 secs]
他们的含义依次为:
- gc前新生代占用的内存
- gc后新生代占用的内存
- 新生代大小
- jvm暂停的时间
- jvm堆gc前的大小
- jvm堆gc后的大小
- jvm堆大小
- jvm消耗的时间
然后在看CMS的GC日志,首先是初始标记:
[GC [1 CMS-initial-mark: 1259285K(1572864K)] 1406035K(3975872K), 0.0697870 secs] [Times: user=0.07 sys=0.00, real=0.08 secs]
并发标记:
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.871/0.871 secs] [Times: user=1.76 sys=0.03, real=0.87 secs]
预先清理:
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.012/0.015 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[CMS-concurrent-abortable-preclean-start]
并发清理:
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.933/0.933 secs] [Times: user=0.93 sys=0.01, real=0.93 secs]
重置线程:
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.006/0.006 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
执行
这里我们主要总结的是class文件在jvm中怎么跑起来的。
加载
在遇到下面几种情况的时候会去加载class文件:
- 创建对象
- 使用类的静态属性
- Class.forName
- 启动类
- 初始化子类
大致分为三步完成:
- 加载:找到Class文件并读取里面的内容
- 连接
- 验证:确保Class文件的正确性
- 准备:对静态变量分配内存、设置默认值
- 解析:将符号引用替换成直接引用
- 初始化:为静态变量赋予正确的初始值。
和ClassLoader相关的东西可以看这里。
解释和编译
生成的class文件中有字节码,这些字节码描述了方法的行为,总体上来看是以栈为基础的。
编译分为c1(client)和c2(server),刚开始代码是解释执行,当出现“热代码”的时候,会JIT会使用编译器将字节码编译成本地代码执行,在具体实现时有几个层次:
- 解释执行
- 编译成本地代码并进行简单优化
- 进行耗时比较长的、比较激进的优化
常见的优化技术有:
- 逃逸分析
- 同步消除
- 标量替换
多态等特性使得优化变得很复杂。和编译相关的常用参数有:
- -XX:ReservedCodeCacheSize:设置代码缓存区的大小
- -XX:CICompilerCount:设置编译器的线程数
- -XX:-CITime:打印花在JIT上的时间
- -XX:-PrintCompilation:当有方法被编译时,打印出相关信息