类加载子系统负责从文件系统或者网络中加载class信息。加载的信息存放于一块叫做方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,包括字符串字面量和数字常量。
几乎所有的对象都存放于java堆中。堆空间是所有线程共享的。
java的NIO库允许java程序使用直接内存。直接内存是在java堆外的,直接向系统申请的内存区域。通常访问的速度是优于java堆的,因此出于性能的考虑,读写频繁的场景可能会考虑使用直接内存。
垃圾回收器是java虚拟机的重要组成部分,它可以对方法区、堆和直接内存进行回收。
每一个java虚拟机线程都会有一个私有的java栈。java栈在线程创建的时候被创建、java栈保存栈帧信息,java栈中保存着局部变量、方法参数。
堆空间结构
以下代码jvm如何存放呢?
SimpleHeap实例本身存放在堆中,描述SimpleHeap类的信息存放在方法区,s1和s2局部变量存放在java栈中,并指向堆中两个实例。
栈
虚拟机通过-Xss来指定线程的最大栈空间,这个参数也直接决定了函数调用的最大深度。
局部变量表用于保存函数参数以及局部变量,由于局部变量表在栈帧中,所以如果函数的参数和局部变量较多,会使局部变量膨胀,从而每一次函数调用会占用更多的栈空间,进而导致函数的嵌套层数减少。
栈帧中局部变量表的槽位是可以复用的,如果一个局部变量过了其作用域,那么其作用域之后声明的新的局部变量就很有可能复用之前的槽位
通过-XX:+PrintGC打印堆大小。
栈上分配
对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将它们打散分配在栈上。分配在栈上的好处是可以在函数调用结束之后自行销毁,而不需要垃圾回收器的介入。
下面的示例中图一因为对象User u是类的成员变量,可以被任何线程访问。所以不会被分配在栈上。而图二u不属于逃逸对象,所以可能会被分配到栈上。
实例:
由于循环1亿次,User对象实例约占6字节,因此累计空间大约占1.5GB。如果堆空间小于这个值就会发生GC。我们使用以下参数运行代码:
参数:-Xmx指定最大分配10MB。-XX:+DoEscapeAnalysis表示开启逃逸分析。
-XX:+PrintGC表示打印GC日志。-XX:-UserTLAB表示关闭TLAB。
-XX:+EliminateAllocations表示允许将对象打散分配到栈上。
可以看到运行结果并没有打印GC日志,说明User对象分配被优化了,可以分配到栈上。
tlab介绍:https://www.jianshu.com/p/8be816cbb5ed
方法区
方法区主要存放系统的类信息,比如类的字段、方法、常量池等。
在jdk1.6、1.7中方法区可以理解为永久区。永久区可以使用-XX:PermSize和-XX:MaxPermSize指定。
在jdk1.8中,永久区被彻底移出,取而代之的是元数据区。元数据区的大小可以通过-XX:MaxMetaspaceSize指定。这是块堆外的直接内存。与永久区不同的是如果不指定大小会耗尽宿主机的所有可用内存。
掌握跟踪调试参数
java的一大特色就是支持自动的垃圾回收,但是有时候如果垃圾回收频繁出现,或者占用太长时间的cpu时间,就不得不引起重视。
代表在gc前堆空间的使用量约为4MB,GC后,堆空间的使用量为377KB。此时可用的堆空间总量为16MB。如果需要更加详细的信息可以使用-XX:+PrintGCDetails参数。输出如下:
从上图可以看出来,第一次新生代从8MB降到了1MB。整个堆从22MB左右降到了17MB。
第二次为full GC,新生代显示的是垃圾收集前后9M没有变化,但实际上full gc会把新生代清空,而老年代从16M到13M,整个堆也从26M变为了13M,老年代和整个堆大小相同也说明新生代被清空了。
由于GC会引起应用程序停顿,所以可以通过-XX:PrintGCApplicationConcurrentTime打印应用程序的执行时间。
-XX:PrintGCApplicationStoppedTime打印应用程序由GC而产生的停顿时间。
最大堆和初始堆的设置
上图中可以看到,当前最大堆是20GB=20971520字节,而打印的最大可用内存仅仅20316160字节。可以看到最大堆空间和最大可用内存并不相等,差值就是from/to的空间。
但是最大堆-from区的大小=20840448并不等于最大可用内存。这是因为虚拟机内部并没有直接使用新生代from/to的大小,而是进一步的对它们做了对齐操作。
在实际工作中,也可以将初始堆和最大堆设置相等,这样的好处在于减少程序运行时进行垃圾回收的次数,从而提高性能。
新生代的配置
-Xmn用于设置新生代的大小。一般这个值设置为整个堆大小的1/3或者1/4左右。
-XX:SurvivorRatio用来设置新生代中eden空间和from/to空间的大小关系。
设置如上参数代表eden区为512KB,from和to区也分别为256KB。可用新生代为=512+256=768KB,总新生代为1MB。
堆溢出处理
通过设置-XX:+HeapDumpOnOutOfMemoryError 可以在内存溢出时导出整个堆信息。
直接内存设置
最大可用直接内存: -XX:MaxDirectMemorySize.如果不设置,默认为最大堆内存:-Xmx。
直接内存的读写速度比堆内存快大约40%.若使用-server参数来执行上述代码,会比堆内存提升一个数量级。
但是在申请空间时,堆内存的速度远远高于直接内存。
所以结论就是:直接内存适用于申请次数较少,访问较为频繁的场景;如果内存空间需要频繁申请,并不适合直接内存。
Client和Server模式
Server模式启动会比较慢,因为Server模式会尝试收集更多的系统信息,使用更复杂的优化算法对程序进行优化。但是Server模式的执行速度要远远快于Client模式的。
String在虚拟机中的实现
String的三个特性:
1.不变性
不变性的作用在于当一个对象被多个线程共享,并且频繁访问的时候,可以省略同步和锁等待的时间,从而大幅度提高系统性能。
2.针对常量池的优化
是指如果两个String对象拥有相同的值时,它们只引用常量池中同一拷贝。
3.类的final定义
在虚拟机中有一块称为常量池的区间专门用来存放字符串常量。在jdk1.6之前,这块区域属于永久区的一部分,但是jdk1.7之后,它被移入到堆中进行管理。
深堆和浅堆
浅堆:浅堆是指对象本身占用的内存,并不包含其内部引用对象的大小
深堆:深堆是指只能通过该对象访问到的(直接或者间接)所有对象的浅堆之和,即对象被回收后可以释放的真实空间。
A的深堆大小为A和D之和,由于C还可以通过B访问到,所以不在对象A的深堆范围内。