基础
数据结构与算法基础
说一下几种常见的排序算法和分别的复杂度。
冒泡排序
具体的原理就是未排好,自上而下的比较,小的数就往上冒,大的数就往下沉,按理来说冒泡排序总共的次数最多为n
选择排序
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。
用Java写一个冒泡排序算法
//主方法
public static void main(String[] args) {
int[] arr = { 3, 5, 7, 1, 8, 11, 9}; //定义数组
bubbleSort (arr); //开始排序
}
//排序方法
public static void bubbleSort(int[] arrys) {
//定义临时变量 temp
int temp = 0;
//用j为下标,遍历数组
for (int j = 0; j < arrys.length; j++) {
//对于每一个数组元素,从0到还未来排序的最大下标,总是把最大的数字放在后面
for(int k = 0;k < arrys.length-j-1; k++){
if(arrys[k] > arrys[k+1]){ //判断当前数字与后面数字的大小
temp = arrys[k];
arrys[k] = arrys[k+1];
arrys[k+1] = temp; //用temp变量进行换值
}
}
}
bubblePrint(arrys); //打印
}
//打印方法
public static void bubblePrint(int[] before){
for(int i = 0;i < before.length; i++){ //遍历
System.out.print(before[i] + ""); //打印,以空格隔开
}
System.out.println();//换行
}
描述一下链式存储结构。
提起链式存储结构,其与数组是两个非常基础的数据结构,每当提到链式存储结构时,一般情况下我们都会将其与数组放到一块儿来比较。
对于数组与链表,从结构上来看,数组是需要一块连续的内存空间来存储数据,对内存的要求非常高,比如说我们申请一个100M大小的数组,而如果我们的内存可用空间大于100M,但是没有连续的100M可用空间,那即便是我们的内存空间充足,在申请空间时也会申请失败。
而对于链表来说,对内存空间的要求就不会有那么高,它并不需要一块连续的内存空间,只要内存空间充足,即使内存空间存在碎片,只要碎片的大小足够存储一个链表节点的数据,该碎片的空间都有可能被分配,链表通过指针或者引用的方式将一组零散的空间串联起来使用。所以如果一个链表需要100M的空间,内存空间充足,即使没有一个连续的空间大于100M,也不会影响链表的空间分配。
数组连续的存储空间
链表分散的空间串联
如何遍历一棵二叉树?
前序递归遍历算法:访问根结点-->递归遍历根结点的左子树-->递归遍历根结点的右子树
中序递归遍历算法:递归遍历根结点的左子树-->访问根结点-->递归遍历根结点的右子树
后序递归遍历算法:递归遍历根结点的左子树-->递归遍历根结点的右子树-->访问根结点
public static void main(String[] args) {
System.out.print("前序:");
Traversal.preOrder();
Traversal.preOrderRecursion(Traversal.createBinTree());
System.out.print("中序:");
Traversal.inOrder();
Traversal.inOrderRecursion(Traversal.createBinTree());
System.out.print("后序:");
Traversal.postOrder();
Traversal.postOrderRecursion(Traversal.createBinTree());
}
}
/**
* 节点数据结构
*
* @author bin.zhang
* @version 2017年8月30日 上午11:49:38
*/
class BinTreeNode {
BinTreeNode() {
}
BinTreeNode(char data, int flag, BinTreeNode lchild, BinTreeNode rchild) {
this.data = data;
this.flag = flag;
this.lchild = lchild;
this.rchild = rchild;
}
char data;
int flag;
BinTreeNode lchild, rchild;
}
class Traversal {
/**
* 创建一棵二叉树
*
* @return 根节点
* @author bin.zhang
*/
public static BinTreeNode createBinTree() {
BinTreeNode R3 = new BinTreeNode('F', 0, null, null);
BinTreeNode L2 = new BinTreeNode('D', 0, null, null);
BinTreeNode R2 = new BinTreeNode('E', 0, null, R3);
BinTreeNode L1 = new BinTreeNode('B', 0, L2, R2);
BinTreeNode R1 = new BinTreeNode('C', 0, null, null);
BinTreeNode T = new BinTreeNode('A', 0, L1, R1);
return T;
}
// 前序
public static void preOrder() {
BinTreeNode p = createBinTree();
Stack
while (p != null || !stack.empty()) {
if (p != null) {
System.out.print(p.data);
stack.push(p);
p = p.lchild;
} else {
p = stack.pop();
p = p.rchild;
}
}
System.out.println();
}
// 前序递归
public static void preOrderRecursion(BinTreeNode top) {
if (top != null) {
System.out.println(top.data);
preOrderRecursion(top.lchild);
preOrderRecursion(top.rchild);
}
}
// 中序
public static void inOrder() {
BinTreeNode p = createBinTree();
Stack
while (p != null || !stack.empty()) {
if (p != null) {
stack.push(p);
p = p.lchild;
} else {
p = stack.pop();
System.out.print(p.data);
p = p.rchild;
}
}
System.out.println();
}
// 中序递归
public static void inOrderRecursion(BinTreeNode top) {
if (top != null) {
inOrderRecursion(top.lchild);
System.out.println(top.data);
inOrderRecursion(top.rchild);
}
}
// 后序
public static void postOrder() {
BinTreeNode p = createBinTree();
Stack
int mark = 1; // 转向标志
while (p != null || !stack.empty()) { // 遍历
if (p != null && mark != 0) {
stack.push(p);
p = p.lchild;
}// 转向左子树
else {
p = stack.pop();
p.flag++; // 退栈
if (p.flag == 1) {
stack.push(p);
p = p.rchild;
mark = 1;
} // 转向右子树
else if (p.flag == 2 && !stack.empty()) { // 输出结点
System.out.print(p.data);
mark = 0;
} else if (p.flag == 2 && stack.empty()) { // 输出根结点并退出
System.out.print(p.data);
break;
}
} // if-else
} // while
System.out.println();
}
// 后序递归
public static void postOrderRecursion(BinTreeNode top) {
if (top != null) {
postOrderRecursion(top.lchild);
postOrderRecursion(top.rchild);
System.out.println(top.data);
}
}
倒排一个LinkedList。
templist.addFirst(m);
public static void main(String[] args) {
reverse();
}
public static void reverse() {
LinkedList
LinkedList
int i = 0;
while (i < 6) {
list.add(i);
i++;
}
System.out.println(list);
Iterator
int m;
while (it.hasNext() && i >= 0) {
m = it.next();
templist.addFirst(m);
i--;
}
list = templist;
System.out.println(list);
}
运行结果为:
[0, 1, 2, 3, 4, 5]
[5, 4, 3, 2, 1, 0]
用Java写一个递归遍历目录下面的所有文件。
private static int depth=1;
public static void find(String pathName,int depth) throws IOException {
int filecount=0;
//获取pathName的File对象
File dirFile = new File(pathName);
//判断该文件或目录是否存在,不存在时在控制台输出提醒
if (!dirFile.exists()) {
System.out.println("do not exit");
return ;
}
//判断如果不是一个目录,就判断是不是一个文件,时文件则输出文件路径
if (!dirFile.isDirectory()) {
if (dirFile.isFile()) {
System.out.println(dirFile.getCanonicalFile());
}
return ;
}
for (int j = 0; j < depth; j++) {
System.out.print(" ");
}
System.out.print("|--");
System.out.println(dirFile.getName());
//获取此目录下的所有文件名与目录名
String[] fileList = dirFile.list();
int currentDepth=depth+1;
for (int i = 0; i < fileList.length; i++) {
//遍历文件目录
String string = fileList[i];
//File("documentName","fileName")是File的另一个构造器
File file = new File(dirFile.getPath(),string);
String name = file.getName();
//如果是一个目录,搜索深度depth++,输出目录名后,进行递归
if (file.isDirectory()) {
//递归
find(file.getCanonicalPath(),currentDepth);
}else{
//如果是文件,则直接输出文件名
for (int j = 0; j < currentDepth; j++) {
System.out.print(" ");
}
System.out.print("|--");
System.out.println(name);
}
}
}
public static void main(String[] args) throws IOException{
find("D:", depth);
}
二、Java基础
接口与抽象类的区别?
接口(interface)和抽象类(abstract class)是支持抽象类定义的两种机制。
接口是公开的,不能有私有的方法或变量,接口中的所有方法都没有具体的实现,通过关键字interface修饰。
抽象类是可以有私有方法或私有变量的,通过把类或者类中的方法声明为abstract来表示一个类是抽象类,被声明为抽象的方法不能有具体的实现。
相同点:
(1)都不能被实例化 即不能使用new关键字来实例化对象;
(2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点:
(1)接口只有方法,不能有具体的实现,而抽象类可以有方法与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
(5)接口被用于常用的功能,便于日后维护和添加删除,而抽象类更倾向于充当公共类的角色,不适用于日后重新对立面的代码修改。功能需要累积时用抽象类,不需要累积时用接口。
Java中的异常有哪几类?分别怎么使用?
从根本上讲所有的异常都属于Throwable的子类,从大的方面讲分为Error(错误)和Exception(异常)。
Error是程序无法处理的异常,当发生Error时程序线程会终止运行。
我们一般意义上讲的异常就是指的Exception,这也是面试官常问的问题。
下面就简单说一下关于Exception(以下都简称异常)的一点理解。
异常分为运行时异常(RuntimeException,又叫非检查时异常)和非运行时异常(又叫检查异常)。
下面列举一下常见的运行时异常:
NullPointerException - 试图访问一空对象的变量、方法或空数组的元素
ArrayIndexOutOfBoundsException - 数组越界访问
NoClassDefFoundException - JAVA运行时系统找不到所引用的类
ArithmeticException - 算术运算中,被0除或模除
ArrayStoreException - 数据存储异常,写数组操作时,对象或数据类型不兼容
ClassCastException - 类型转换异常
IllegalArgumentException - 方法的参数无效
IllegalThreadStateException - 试图非法改变线程状态,比方说试图启动一已经运行的线程
NumberFormatException - 数据格式异常,试图把一字符串非法转换成数值(或相反)
SecurityException - 如果Applet试图执行一被WWW浏览器安全设置所禁止的操作
IncompatibleClassChangeException - 如改变了某一类定义,却没有重新编译其他引用了这个类的对象。
如某一成员变量的声明被从静态改变为非静态,但其他引用了这个变量的类却没有重新编译,或者相反。如删除了类声明中的某一域或方法,但没有重新编译那些引用了这个域或方法的类
OutOfMemoryException - 内存不足,通常发生于创建对象之时
IncompatibleTypeException - 试图实例化一个接口,Java运行时系统将抛出这个异常
UnsatisfiedLinkException - 如果所需调用的方法是C函数,但Java运行时系统却无法连接这个函数
InternalException - 系统内部故障所导致的异常情况,可能是因为Java运行时系统本身的原因。如果发现一可重现的InternalException,
前三种异常是我在开发中经常性遇到的问题,对于运行时异常通过它的别名(非检查时异常)我们可以知道这些异常不是我们写代码的时候可以检查到的,而是程序在运行的时候可能会发生的,也就是说这是在业务逻辑上可能会出现的问题。
对于这类异常就需要我们在开发的时候尽量的把业务逻辑可能会出现的问题考虑清楚,进行异常捕捉。
常用的方法包括:
try{}catch(Exception e){}finally{}和throws两种办法。
try{}catch(Exception e){}finally{}是在方法中对异常进行捕获,catch可以写多个,Java运行时系统从上到下分别对每个catch语句处理的例外类型进行检测,
直到找到类型相匹配的catch语句为止。(具体的就不过多赘述)
throws是出现在方法头部,个人理解算是在最外层抛出异常。关于对运行时异常的处理。
非运行时异常中我们常遇到的可能有IOException、SQLException等,
这类异常就是我们编写的代码、sql语句的问题了,这就需要我们回到代码中仔细检查了。
常用的集合类有哪些?比如List如何排序?
集合的两个顶级接口分别为:Collection和Map
Collection下有两个比较常用的接口分别是List(列表)和Set(集),
其中List可以存储重复元素,元素是有序的(存取顺序一致),可以通过List脚标来获取指定元素;
而Set不可以有重复元素,元素是无序的。
List接口中,比较常用的类有三个:ArrayList、Vactor、LinkedList。
ArrayList:线程不安全的,对元素的查询速度快。
Vector:线程安全的,多了一种取出元素的方式:枚举(Enumeration),但已被ArrayList取代。
LinkedList:链表结构,对元素的增删速度很快。
Set接口中,比较常用的类有两个:HashSet、TreeSet:
HashSet:要保证元素唯一性,需要覆盖掉Object中的equals和hashCode方法(因为底层是通过这两个方法来判断两个元素是否是同一个)。
TreeSet:以二叉树的结构对元素进行存储,可以对元素进行排序。
排序的两种方式:
1、元素自身具备比较功能,元素实现Comparable接口,覆盖compareTo方法。
2、建立一个比较器对象,该对象实现Comparator接口,覆盖compare方法,并将该对象作为参数传给TreeSet的构造函数(可以用匿名内部类)。
Map接口其特点是:元素是成对出现的,以键和值的形式体现出来,键要保证唯一性:常用类有:HashMap,Hashtable,TreeMap。
HashMap:线程不安全等的,允许存放null键null值。
Hashtable:线程安全的,不允许存放null键null值。
TreeMap:可以对键进行排序(要实现排序方法同TreeSet)。
Collection和Map两个接口对元素操作的区别:
存入元素:
Collection接口下的实现类通过add方法来完成,而Map下是通过put方法来完成。
取出元素:
Collection接口下:List接口有两种方式:1、get(脚标);2、通过Iterator迭代方式获取元素;而Vactor多了一种枚举(Enumeration)的方式。Set接口通过迭代的方式获取元素。
Map接口下:先通地keySet获取键的系列,然后通过该系列使用Iterator迭代方式获取元素值。
List集合序列排序的两种方法
Comparable自然规则排序
在自定义类里面实现Comparable接口,并重写抽象方法compareTo(Student o);
Collections.sort(集合);
Comparator专门规则排序(l临时排序)
新建一个实现了Comparator接口的类,并重写抽象方法compare(Student o1, Student o2);
Collections.sort(集合,实现了Comparator接口的类的实例化对象);
ArrayList和LinkedList内部的实现大致是怎样的?他们之间的区别和优缺点?
ArrayList:底层数组,线程不安全的,对元素的查询速度快。
LinkedList:链表结构,对元素的增删速度很快。
内存溢出是怎么回事?请举一个例子?
1、OutOfMemoryError: PermGen space
Permanent Generation space 这个区域主要用来保存加来的Class的一些信息,在程序运行期间属于永久占用的,Java的GC不会对他进行释放,所以如果启动的程序加载的信息比较大,超出了这个空间的大小,就会发生溢出错误;
解决的办法无非就是增加空间分配了——增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
2、OutOfMemoryError:Java heap space
heap 是Java内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC又来不及释放的时候,就会发生溢出错误。
Java中对象的创建是可控的,但是对象的回收是由GC自动的,一般来说,当已存在对象没有引用(即不可达)的时候,GC就会定时的来回收对象,释放空间。但是因为程序的设计问题,导致对象可达但是又没有用(即前文提到的内存泄露),当这种情况越来越多的时候,问题就来了。
针对这个问题,我们需要做一下两点:
1、检查程序,减少大量重复创建对象的死循环,减少内存泄露。
2、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。
3、StackOverFlowError
stack是Java内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。
针对这个问题,除了修改配置参数-Xss参数增加线程栈大小之外,优化程序是尤其重要。
==和equals的区别?
==用于判别两字符串在内存中的地址是否相同
equals用于判别两字符串的内容是否相同
hashCode方法的作用?
减少set集合添加元素的速度
哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。
当集合要添加新的元素时,先调用这个元素的hashCode方法,就能定位到它应该放置的物理位置。
如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较;
如果这个位置上已经有元素了,
就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。
这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
所以,Java对于eqauls方法和hashCode方法是这样规定的:
1、如果两个对象相同,那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同,上面说的对象相同指的是用eqauls方法比较。
NIO是什么?适用于何种场景?
NIO最核心的三个组件
Channel通道
Buffer缓冲区
Selector选择器
NIO是为了弥补IO操作的不足而诞生的,NIO的一些新特性有:
非阻塞I/O,选择器,缓冲以及管道。
管道(Channel),缓冲(Buffer) ,选择器( Selector)是其主要特征。
概念解释:
Channel——管道实际上就像传统IO中的流,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。
一个 Buffer 实质上是一个容器对象。
Selector——选择器用于监听多个管道的事件,使用传统的阻塞IO时我们可以方便的知道什么时候可以进行读写,而使用非阻塞通道,我们需要一些方法来知道什么时候通道准备好了,选择器正是为这个需要而诞生的。
NIO和传统的IO有什么区别呢?
1,IO是面向流的,NIO是面向块(缓冲区)的。
IO面向流的操作一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。导致了数据的读取和写入效率不佳;
NIO面向块的操作在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多,同时数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。通俗来说,NIO采取了“预读”的方式,当你读取某一部分数据时,他就会猜测你下一步可能会读取的数据而预先缓冲下来。
2,IO是阻塞的,NIO是非阻塞的。
对于传统的IO,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
而对于NIO,使用一个线程发送读取数据请求,没有得到响应之前,线程是空闲的,此时线程可以去执行别的任务,而不是像IO中那样只能等待响应完成。
NIO和IO适用场景
NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长,NIO也有缺点,因为NIO是面向缓冲区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。
那么NIO和IO各适用的场景是什么呢?
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,这时候用NIO处理数据可能是个很好的选择。
而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。
使用哪种处理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。
通俗解释,最后,对于NIO和传统IO,有一个网友讲的生动的例子:
以前的流总是堵塞的,一个线程只要对它进行操作,其它操作就会被堵塞,也就相当于水管没有阀门,你伸手接水的时候,不管水到了没有,你就都只能耗在接水(流)上。
nio的Channel的加入,相当于增加了水龙头(有阀门),虽然一个时刻也只能接一个水管的水,但依赖轮换策略,在水量不大的时候,各个水管里流出来的水,都可以得到妥
善接纳,这个关键之处就是增加了一个接水工,也就是Selector,他负责协调,也就是看哪根水管有水了的话,在当前水管的水接到一定程度的时候,就切换一下:临时关上当
前水龙头,试着打开另一个水龙头(看看有没有水)。
当其他人需要用水的时候,不是直接去接水,而是事前提了一个水桶给接水工,这个水桶就是Buffer。也就是,其他人虽然也可能要等,但不会在现场等,而是回家等,可以做
其它事去,水接满了,接水工会通知他们。
这其实也是非常接近当前社会分工细化的现实,也是统分利用现有资源达到并发效果的一种很经济的手段,而不是动不动就来个并行处理,虽然那样是最简单的,但也是最浪费
资源的方式。
HashMap实现原理,如何保证HashMap的线程安全?
HashMap 重复允许有空键和空值,不允许有null键和null值
使用 java.util.Hashtable 类,此类是线程安全的。
使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。
使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。
Map
为什么用HashMap
HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射
HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改
HashMap是非synchronized,所以HashMap很快
HashMap可以接受null键和值,而Hashtable则不能(原因就是equlas()方法需要对象,因为HashMap是后出的API经过处理才可以)
HashMap的工作原理是什么
HashMap是基于hashing的原理,我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。
当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node 。
JVM内存结构,为什么需要GC?
JVM主要包括四个部分:JVM结构、内存分配、垃圾回收算法、垃圾收集器。
1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。
2.执行引擎:负责执行class文件中包含的字节码指令;
3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域,
方法区(Method Area):
用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。
java堆(Heap):
存储java实例或者对象的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。
java栈(Stack):
java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是线程私有的。
程序计数器(PCRegister):
用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
本地方法栈(Native Method Stack):
和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。
4.本地方法接口:
主要是调用C或C++实现的本地方法及返回结果。
内存分配
我觉得了解垃圾回收之前,得先了解JVM是怎么分配内存的,然后识别哪些内存是垃圾需要回收,最后才是用什么方式回收。
Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。
java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。根据上面我们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。
总之Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题;
垃圾检测、回收算法
垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?
一般有以下几种方法:
引用计数法:给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。
好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:
可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象
本地方法中引用的对象等。
NIO模型,select/epoll的区别,多路复用的原理
select,poll,epoll都是IO多路复用的机制。
I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间
Java中一个字符占多少个字节,扩展再问int, long, double占多少字节
boolean 1 byte 1 char 2 int 4 float 4 long 8 double 8
一个字符两个字节,int 4 , long double 8
创建一个类的实例都有哪些办法?
new ,反射。反序列化,Object().clone()方法
final/finally/finalize的区别?
final是类,变量,方法的修饰。类被修饰后不能再被继承。变量和方法被修饰不能再被修改
finally是try...catch后执行的finally,
finalize,在Object中的方法名,在此对象被回收前会执行这个方法。(当使用文件流时,如果对象被回收,没有关闭流,在底层就会实现内存泄露)
Session/Cookie的区别?
Session存在服务器端。
Cookie存在客户端(浏览器上)。
HTTP是一种无状态的协议,为了分辨链接是谁发起的,就需要我们自己去解决这个问题。不然有些情况下即使是同一个网站我们每打开一个页面也都要登录一下。而Session和Cookie就是为解决这个问题而提出来的两个机制。
应用场景
日常登录一个网站,今天输入用户名密码登录了,第二天再打开很多情况下就直接打开了。这个时候用到的一个机制就是cookie。
session的一个场景是购物车,添加了商品之后客户端处可以知道添加了哪些商品,而服务器端如何判别呢,所以也需要存储一些信息,这里就用到了session。
Cookie
通俗讲,Cookie是访问某些网站以后在本地存储的一些网站相关的信息,下次再访问的时候减少一些步骤。另外一个更准确的说法是:Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器,是一种在客户端保持状态的方案。
Cookie的主要内容包括:名字,值,过期时间,路径和域。使用Fiddler抓包就可以看见,比方说我们打开百度的某个网站可以看到Headers包括Cookie,
可以看见是key, value的形式,也就是我们说的对应着的名字,值。过期时间是可以设置的,如果不设置,则浏览器关掉就消失了,是存储在内存当中的,否则就是按照我们设置的时间来存储在硬盘上的,当过期后自动清除,
Session
Session是存在服务器的一种用来存放用户数据的类HashTable结构。
当浏览器 第一次发送请求时,服务器自动生成了一个HashTable和一个Session ID用来唯一标识这个HashTable,并将其通过响应发送到浏览器。
当浏览器第二次发送请求,
会将前一次服务器响应中的Session ID放在请求中一并发送到服务器上,服务器从请求中提取出Session ID,并和保存的所有Session ID进行对比,找到这个用户对应的HashTable。
一般这个值会有一个时间限制,超时后毁掉这个值,默认是20分钟。
Session的实现方式和Cookie有一定关系。试想一下,建立一个连接就生成一个session id,那么打开几个页面就好几个了,这显然不是我们想要的,那么该怎么区分呢?这里就用到了Cookie,我们可以把session id存在Cookie中,然后每次访问的时候将Session id带过去就可以识别了,是不是很方便~
区别
通过上面的简单叙述,很容易看出来最明显的不同是一个在客户端一个在服务端。因为Cookie存在客户端所以用户可以看见,所以也可以编辑伪造,不是十分安全。
Session过多的时候会消耗服务器资源,所以大型网站会有专门的Session服务器,而Cookie存在客户端所以没什么问题。
域的支持范围不一样,比方说a.com的Cookie在a.com下都能用,而www.a.com的Session在api.a.com下都不能用,解决这个问题的办法是JSONP或者跨域资源共享。
String/StringBuffer/StringBuilder的区别,扩展再问他们的实现?
String:字符串常量
String是不可变对象,在每次对String类进行改变的时候其实都等于生成了一个新的String对象,然后指向新的String对象,
所以经常改变内容的字符串最好不要用String类型,因为每次声称对象都会对系统性能产生影响.
StringBuffer类:
StringBuffer是可变字符串,在每次对StringBuffer对象进行改变时,会对StringBuffer对象本身进行操作,而不是生成新的对象,
再改变对象引用.所以,在字符串对象经常改变的情况下推荐使用StringBuffer类.String实现了equals()方法和hashCode()方法,
而StringBuffer没有实现.StringBuffer对字符串拼接效率较高.StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,
因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。
StringBuilder类:
StringBuilder是JDK5.0以后提供的类,它和StringBuffer类等价,区别在于StringBuffer类是线程安全的,而StringBuilder是单线程的,
不提供同步,理论上效率更高.在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全,而StringBuffer则每次都需要判断锁,效率相对更低
StringBuffer StringBuilder 类继承自AbstractStringBuilder抽象类,实现Serializable序列化接口和CharSequence接口。
AbstractStringBuilder抽象类实现Appendabel,CharSequence接口。
Servlet的生命周期?
Servlet运行在Servlet容器中,其生命周期由容器来管理。
Servlet的生命周期通过javax.servlet.Servlet接口中的init()、service()和destroy()方法来表示
Servlet的生命周期包含了下面4个阶段:
1.加载和实例化
2.初始化
3.请求处理
4.服务终止
Web服务器在与客户端交互时Servlet的工作过程是:
1.在客户端对web服务器发出请求
2.web服务器接收到请求后将其发送给Servlet
3.Servlet容器为此产生一个实例对象并调用ServletAPI中相应的方法来对客户端HTTP请求进行处理,然后将处理的响应结果返回给WEB服务器.
4.web服务器将从Servlet实例对象中收到的响应结构发送回客户端.
servlet的生命周期:
1.加载和实例化
Servlet容器负责加载和实例化Servlet。当Servlet容器启动时,或者在容器检测到需要这个Servlet来响应第一个请求时,创建Servlet实例。当Servlet容器启动后,它必须要知道所需的Servlet类在什么位置,Servlet容器可以从本地文件系统、远程文件系统或者其他的网络服务中通过类加载器加载Servlet类,成功加载后,容器创建Servlet的实例。因为容器是通过Java的反射API来创建Servlet实例,调用的是Servlet的默认构造方法(即不带参数的构造方法),所以我们在编写Servlet类的时候,不应该提供带参数的构造方法。
2.初始化
在Servlet实例化之后,容器将调用Servlet的init()方法初始化这个对象。初始化的目的是为了让Servlet对象在处理客户端请求前完成一些初始化的工作,如建立数据库的连接,获取配置信息等。对于每一个Servlet实例,init()方法只被调用一次。在初始化期间,Servlet实例可以使用容器为它准备的ServletConfig对象从Web应用程序的配置信息(在web.xml中配置)中获取初始化的参数信息。在初始化期间,如果发生错误,Servlet实例可以抛出ServletException异常或者UnavailableException异常来通知容器。ServletException异常用于指明一般的初始化失败,例如没有找到初始化参数;而UnavailableException异常用于通知容器该Servlet实例不可用。例如,数据库服务器没有启动,数据库连接无法建立,Servlet就可以抛出UnavailableException异常向容器指出它暂时或永久不可用。
I.如何配置Servlet的初始化参数?
在web.xml中该Servlet的定义标记中,比如:
配置了两个初始化参数user和blog它们的值分别为username和http://。。。, 这样以后要修改用户名和博客的地址不需要修改Servlet代码,只需修改配置文件即可。
II.如何读取Servlet的初始化参数?
ServletConfig中定义了如下的方法用来读取初始化参数的信息:
public String getInitParameter(String name)
参数:初始化参数的名称。
返回:初始化参数的值,如果没有配置,返回null。
III.init(ServletConfig)方法执行次数
在Servlet的生命周期中,该方法执行一次。
IV.init(ServletConfig)方法与线程
该方法执行在单线程的环境下,因此开发者不用考虑线程安全的问题。
V.init(ServletConfig)方法与异常
该方法在执行过程中可以抛出ServletException来通知Web服务器Servlet实例初始化失败。一旦ServletException抛出,
Web服务器不会将客户端请求交给该Servlet实例来处理,而是报告初始化失败异常信息给客户端,该Servlet实例将被从内存中销毁。如果在来新的请求,
Web服务器会创建新的Servlet实例,并执行新实例的初始化操作
3.请求处理
Servlet容器调用Servlet的service()方法对请求进行处理。要注意的是,在service()方法调用之前,init()方法必须成功执行。在service()方法中,Servlet实例通过ServletRequest对象得到客户端的相关信息和请求信息,在对请求进行处理后,调用ServletResponse对象的方法设置响应信息。在service()方法执行期间,如果发生错误,Servlet实例可以抛出ServletException异常或者UnavailableException异常。如果UnavailableException异常指示了该实例永久不可用,Servlet容器将调用实例的destroy()方法,释放该实例。此后对该实例的任何请求,都将收到容器发送的HTTP 404(请求的资源不可用)响应
。如果UnavailableException异常指示了该实例暂时不可用,那么在暂时不可用的时间段内,对该实例的任何请求,都将收到容器发送的HTTP 503(服务器暂时忙,不能处理请求)响应。
I. service()方法的职责
service()方法为Servlet的核心方法,客户端的业务逻辑应该在该方法内执行,典型的服务方法的开发流程为:
解析客户端请求-〉执行业务逻辑-〉输出响应页面到客户端
II.service()方法与线程
为了提高效率,Servlet规范要求一个Servlet实例必须能够同时服务于多个客户端请求,即service()方法运行在多线程的环境下,
Servlet开发者必须保证该方法的线程安全性。
III.service()方法与异常
service()方法在执行的过程中可以抛出ServletException和IOException。其中ServletException可以在处理客户端请求的过程中抛出,
比如请求的资源不可用、数据库不可用等。一旦该异常抛出,容器必须回收请求对象,并报告客户端该异常信息。IOException表示输入输出的错误,
编程者不必关心该异常,直接由容器报告给客户端即可。
编程注意事项说明:
1) 当Server Thread线程执行Servlet实例的init()方法时,所有的Client Service Thread线程都不能执行该实例的service()方法,更没有线程能够执行该实例的destroy()方法,因此Servlet的init()方法是工作在单线程的环境下,开发者不必考虑任何线程安全的问题。
2) 当服务器接收到来自客户端的多个请求时,服务器会在单独的Client Service Thread线程中执行Servlet实例的service()方法服务于每个客户端。此时会有多个线程同时执行同一个Servlet实例的service()方法,因此必须考虑线程安全的问题。
3) 请大家注意,虽然service()方法运行在多线程的环境下,并不一定要同步该方法。而是要看这个方法在执行过程中访问的资源类型及对资源的访问方式。分析如下:
i. 如果service()方法没有访问Servlet的成员变量也没有访问全局的资源比如静态变量、文件、数据库连接等,而是只使用了当前线程自己的资源,比如非指向全局资源的临时变量、request和response对象等。该方法本身就是线程安全的,不必进行任何的同步控制。
ii. 如果service()方法访问了Servlet的成员变量,但是对该变量的操作是只读操作,该方法本身就是线程安全的,不必进行任何的同步控制。
iii. 如果service()方法访问了Servlet的成员变量,并且对该变量的操作既有读又有写,通常需要加上同步控制语句。
iv. 如果service()方法访问了全局的静态变量,如果同一时刻系统中也可能有其它线程访问该静态变量,如果既有读也有写的操作,通常需要加上同步控制语句。
v. 如果service()方法访问了全局的资源,比如文件、数据库连接等,通常需要加上同步控制语句。
4.服务终止
当容器检测到一个Servlet实例应该从服务中被移除的时候,容器就会调用实例的destroy()方法,以便让该实例可以释放它所使用的资源,保存数据到持久存储设备中。当需要释放内存或者容器关闭时,容器就会调用Servlet实例的destroy()方法。在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器所回收。如果再次需要这个Servlet处理请求,Servlet容器会创建一个新的Servlet实例。 在整个Servlet的生命周期过程中,创建Servlet实例、调用实例的init()和destroy()方法都只进行一次,当初始化完成后,Servlet容器会将该实例保存在内存中,通过调用它的service()方法,为接收到的请求服务。
如何用Java分配一段连续的1G的内存空间?需要注意些什么?
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024*1024*1024);
Java有自己的内存回收机制,但为什么还存在内存泄露的问题呢?
JAVA是支持垃圾回收机制的,在这样的一个背景下,内存泄露又被称为“无意识的对象保持”。如果一个对象引用被无意识地保留下来,那么垃圾回收器不仅不会处理这个对象,而且也不处理被这个对象引用的其它对象。“内存泄露”就是内存中某些内存不可被回收。内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)
举个例子:
如果对一个栈(Stack类)先是进行入栈操作,之后再进行出栈操作,那么弹出来的对象将不会被当做垃圾回收,即使使用栈的客户程序不再引用这些对象,因为栈内部存在着对这些已弹出对象的引用,这是Stack类自己管理内存的机制所决定的。
什么是java序列化,如何实现java序列化?(写一个实例)?
序列化:
可以将一个对象保存到一个文件,所以可以通过流的方式在网络上传输,可以将文件的内容读取,转化为一个对象。
处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
序列化是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现:
将需要被序列化的类实现Serializable 接口,该接口没有需要实现的方法,implements Serializable 只是为了标注该对象是可被序列化的,
然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,
接着,使用ObjectOutputStream 对象的writeObject(Object obj)方法就可以将参数为obj 的对象写出(即保存其状态),要恢复的话则用输入流。
import java.io.Serializable;
public class Person implements Serializable {
//本类可以序列化
private String name ;
private int age ;
public Person(String name,int age){
this.name = name ; this.age = age ;
}
public String toString(){
return "姓名:" + this.name + ",年龄" + this.age ;
}
}
String s = new String("abc");创建了几个 String Object?
2
1.在虚拟机栈中为String类型的s分配内存
2.在堆中为分配一块内存用来保存"abc"
3.将堆中指向"abc"的地址赋值给s
String temp="abc";
String s=new String(temp)
三、JVM
JVM堆的基本结构。
JVM中内存
JVM中内存通常划分为两个部分,分别为堆内存与栈内存,栈内存主要用执行线程方法存放本地临时变量与线程中方法执行时候需要的引用对象地址。JVM所有的对象信息都存放在堆内存中,相比栈内存,堆内存可以大的多,所以JVM一直通过对堆内存划分不同的功能区块实现对堆内存中对象管理。
堆内存不够最常见的错误就是OOM(OutOfMemoryError)
栈内存溢出最常见的错误就是StackOverflowError,程序有递归调用时候最容易发生
在JDK7以及其前期的JDK版本中,堆内存通常被分为三块区域
Nursery内存(younggeneration)、长时内存(old generation)、永久内存(Permanent Generation forVM Matedata),
显示如下图:
Eden 伊甸园
Survivor 幸存者
Old Generation for older obiects老一辈的老年人
Permanent Generation for VM metadata永久生成虚拟机元数据
其中最上一层是Nursery内存,一个对象被创建以后首先被放到Nursery中的Eden内存中,如果存活期超两个Survivor之后就会被转移到长时内存中(OldGeneration)中永久内存中存放着对象的方法、变量等元数据信息。通过如果永久内存不够,我们就会得到如下错误:java.lang.OutOfMemoryError: PermGen,而在JDK8中情况发生了明显的变化,就是一般情况下你都不会得到这个错误,原因在于JDK8中把存放元数据中的永久内存从堆内存中移到了本地内存(nativememory)中,JDK8中JVM堆内存结构就变成了如下:
这样永久内存就不再占用堆内存,它可以通过自动增长来避免JDK7以及前期版本中常见的永久内存错误java.lang.OutOfMemoryError:PermGen),也许这个就是你的DK升级到JDK8的理由之一吧。当然JDK8也提供了一个新的设置Matespace内存大小的参数,通过这个参数可以设置Matespace内存大小,这样我们可以根据自己项目的实际情况,避免过度浪费本地内存,达到有效利用。
-XX:MaxMetaspaceSize=128m 设置最大的元内存空间128兆
注意:如果不设置JVM将会根据一定的策略自动增加本地元内存空间如果你设置的元内存空间过小,你的应用程序可能得到以下错误:
java.lang.OutOfMemoryError: Metadata space
JVM的垃圾算法有哪几种?CMS垃圾回收的基本流程?
常用的垃圾回收算法有如下四种:
标记-清除、复制、标记-整理和分代收集。
标记-清除算法
从算法的名称上可以看出,这个算法分为两部分,标记和清除。
首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象。
这个算法简单,但是有两个缺点:一是标记和清除的效率不是很高;二是标记和清除后会产生很多的内存碎片,导致可用的内存空间不连续,当分配大对象的时候,没有足够的空间时不得不提前触发一次垃圾回收。
复制算法
这个算法将可用的内存空间分为大小相等的两块,每次只是用其中的一块,当这一块被用完的时候,就将还存活的对象复制到另一块中,然后把原已使用过的那一块内存空间一次回收掉。这个算法常用于新生代的垃圾回收。
复制算法解决了标记-清除算法的效率问题,以空间换时间,但是当存活对象非常多的时候,复制操作效率将会变低,而且每次只能使用一半的内存空间,利用率不高。
标记-整理算法
这个算法分为三部分:一是标记出所有需要被回收的对象;二是把所有存活的对象都向一端移动;三是把所有存活对象边界以外的内存空间都回收掉。
标记-整理算法解决了复制算法多复制效率低、空间利用率低的问题,同时也解决了内存碎片的问题。
分代收集算法
根据对象生存周期的不同将内存空间划分为不同的块,然后对不同的块使用不同的回收算法。一般把Java堆分为新生代和老年代,新生代中对象的存活周期短,只有少量存活的对象,所以可以使用复制算法,而老年代中对象存活时间长,而且对象比较多,所以可以采用标记-清除和标记-整理算法。
CMS:Concurrent Mark Sweep。并发标记扫描
看名字就知道,CMS是一款并发、使用标记-清除算法的gc。
CMS是针对老年代进行回收的GC。
CMS有什么用?
CMS以获取最小停顿时间为目的。在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。
CMS如何执行?
总体来说CMS的执行过程可以分为以下几个阶段:
1 初始标记(STW)
2 并发标记
3 并发预清理
4 重标记(STW)
5 并发清理
6 重置
CMS缺点?
1.CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间
2.CMS需要更多的CPU资源。
3.CMS需要更大的堆空间。
4.CMS回收器减少了回收的停顿时间,但是降低了堆空间的利用率。
JVM有哪些常用启动参数可以调整,描述几个?
-Xms:设置jvm内存的初始大小
-Xmx:设置jvm内存的最大值
-Xmn:设置新域的大小(这个似乎只对jdk1.4来说是有效的,后来就废弃了)
-Xss:设置每个线程的堆栈大小(也就是说,在相同物理内存下,减小这个值能生成更多的线程)
-XX:NewRatio:设置新域与旧域之比,如-XX:NewRatio=4就表示新域与旧域之比为1:4
-XX:NewSize:设置新域的初始值
-XX:MaxNewSize:设置新域的最大值
-XX:MaxPermSize:设置永久域的最大值
-XX:SurvivorRatio=n:设置新域中Eden区与两个Survivor区的比值。(Eden区主要是用来存放新生的对象,而两个Survivor区则用来存放每次垃圾回收后存活下来的对象)
JVM启动参数使用中常见的错误:
java.lang.OutOfMemoryError相信很多开发人员都用到过,这个主要就是JVM参数没有配好引起的,但是这种错误又分两种:java.lang.OutOfMemoryError:Javaheapspace和java.lang.OutOfMemoryError:PermGenspace,其中前者是有关堆内存的内存溢出,可以同过配置-Xms和-Xmx参数来设置,而后者是有关永久域的内存溢出,可以通过配置-XX:MaxPermSize来设置。
如何查看JVM的内存使用情况?
jinfo:可以输出并修改运行时的java 进程的opts。
jps:与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号。
jstat:一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
jmap:打印出某个java进程(使用pid)内存内的所有'对象'的情况(如:产生那些对象,及其数量)。
jconsole:一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM。
详细:在使用这些工具前,先用JPS命令获取当前的每个JVM进程号,然后选择要查看的JVM。
jstat工具特别强大,有众多的可选项,详细查看堆内各个部分的使用量,以及加载类的数量。使用时,需加上查看进程的进程id,和所选参数。以下详细介绍各个参数的意义。
jstat -class pid:显示加载class的数量,及所占空间等信息。
jstat -compiler pid:显示VM实时编译的数量等信息。
jstat -gc pid:可以显示gc的信息,查看gc的次数,及时间。其中最后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。
jstat -gccapacity:可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小,如:PGCMN显示的是最小perm的内存使用量,PGCMX显示的是perm的内存最大使用量,PGC是当前新生成的perm内存占用量,PC是但前perm内存占用量。其他的可以根据这个类推, OC是old内纯的占用量。
jstat -gcnew pid:new对象的信息。
jstat -gcnewcapacity pid:new对象的信息及其占用量。
jstat -gcold pid:old对象的信息。
jstat -gcoldcapacity pid:old对象的信息及其占用量。
jstat -gcpermcapacity pid: perm对象的信息及其占用量。
jstat -util pid:统计gc信息统计。
jstat -printcompilation pid:当前VM执行的信息。
除了以上一个参数外,还可以同时加上 两个数字,如:jstat -printcompilation3024 2506是每250毫秒打印一次,一共打印6次,还可以加上-h3每三行显示一下标题。
jmap是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。
命令:jmap -dump:format=b,file=heap.bin
file:保存路径及文件名
pid:进程编号
jmap -histo:live pid| less :堆中活动的对象以及大小
jmap -heap pid : 查看堆的使用状况信息
jinfo:的用处比较简单,就是能输出并修改运行时的java进程的运行参数。用法是jinfo -opt pid 如:查看2788的MaxPerm大小可以用 jinfo -flag MaxPermSize 2788。
jconsole是一个用java写的GUI程序,用来监控VM,并可监控远程的VM,非常易用,而且功能非常强。使用方法:命令行里打 jconsole,选则进程就可以了。
JConsole中关于内存分区的说明。
Eden Space (heap): 内存最初从这个线程池分配给大部分对象。
Survivor Space (heap):用于保存在eden space内存池中经过垃圾回收后没有被回收的对象。
Tenured Generation (heap):用于保持已经在 survivor space内存池中存在了一段时间的对象。
Permanent Generation (non-heap): 保存虚拟机自己的静态(refective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的,
Code Cache (non-heap):HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)
jstack ( 查看jvm线程运行状态,是否有死锁现象等等信息) : jstack pid : thread dump
jstat -gcutil pid 1000 100 : 1000ms统计一次gc情况统计100次;
Java程序是否会内存溢出,内存泄露情况发生?举几个例子。
OutOfMemoryError: PermGen space
Permanent Generation space 这个区域主要用来保存加来的Class的一些信息,在程序运行期间属于永久占用的,Java的GC不会对他进行释放,所以如果启动的程序加载的信息比较大,超出了这个空间的大小,就会发生溢出错误;
解决的办法无非就是增加空间分配了——增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
2、OutOfMemoryError:Java heap space
heap 是Java内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC又来不及释放的时候,就会发生溢出错误。
Java中对象的创建是可控的,但是对象的回收是由GC自动的,一般来说,当已存在对象没有引用(即不可达)的时候,GC就会定时的来回收对象,释放空间。但是因为程序的设计问题,导致对象可达但是又没有用(即前文提到的内存泄露),当这种情况越来越多的时候,问题就来了。
针对这个问题,我们需要做一下两点:
1、检查程序,减少大量重复创建对象的死循环,减少内存泄露。
2、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。
3、StackOverFlowError
stack是Java内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。
针对这个问题,除了修改配置参数-Xss参数增加线程栈大小之外,优化程序是尤其重要。
你常用的JVM配置和调优参数都有哪些?分别什么作用?
堆配置
-Xms:初始堆大小
-Xms:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3表示Eden: 3 Survivor:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
1、一般初始堆和最大堆设置一样,因为:现在内存不是什么稀缺的资源,但是如果不一样,从初始堆到最大堆的过程会有一定的性能开销,所以一般设置为初始堆和最大堆一样。64位系统理论上可以设置为无限大,但是一般设置为4G,因为如果再大,JVM进行垃圾回收出现的暂停时间会比较长,这样全GC过长,影响JVM对外提供服务,所以不能太大。一般设置为4G。
2、-XX:NewRaio和-XX:SurvivorRatio这两个参数,都是设置年轻代和年老代的大小的,设置一个即可,第一是设置年轻代的大小,第二个是设置比值,理论上设置一个既可以满足需求